[代码审计] 中环CMS v2.0 的SQL注入漏洞分析

目录

前言

漏洞复现

漏洞分析


前言

SQL 注入的本质原因是数据和语句不分离,直接形成一条完整的 SQL 语句字符串,传入到数据库中执行。参数化查询能有效防御 SQL 注入就是针对这一点入手,它将数据和语句分离,语句就是语句,数据就是数据,两者不会混合。

审计时发现一套网站程序使用了参数化查询技术防御 SQL 注入,思路仍然是抓住漏洞的本质,也就是寻找数据和语句混合的处理过程。

漏洞复现

(1)注入点在主页面的搜索文章功能的 sertype 字段:

 这里有两种搜索类型(标题或关键字),接下来可以看到它作为一个参数传输给后端。

(2)在 hackbar 构造POST请求包,发送:

sertype表示搜索类型,search就是搜索的关键词。可以看到正常访问会返回搜索到的文章。

(3)在 sertype 字段注入:1 and 1=1;#

页面显示搜索到的文章。

(4)在 sertype 字段注入:1 and 1=0;#

页面没有返回一篇文章,可以证明这里存在 SQL 注入。

漏洞分析

形成 SQL 注入的过程是在请求参数(键值对)转换成 where 条件子句时,直接将参数的键名拼接到 where 子句中,然后拼接到 SQL 语句中,最后才进行预编译。简单说,恶意数据在预编译之前被插入到了 SQL 语句中

l漏洞入口点在 /home/IndexAction.class.php 文件,即 index 控制器的 search 方法:

public function search(){
	if(!empty($_POST['search'])){
		$sers = !empty($_POST['search']) ? $_POST : $_GET;
	    $where = array($sers['sertype']=>"%{$sers["search"]}%", "views"=>0);  // 1

		$args = "sertype/{$sers["sertype"]}/search/{$sers["search"]}";
		$total = $this->article->arttotal($where);      // 2

		$size = $this->set->list_one("size");
		$page = new Page($total, $size['size'], $args);
		$this->assign("total", $total);
		$this->assign("all", $this->article->search($where, $page->limit));   // 3
		$this->assign("search", $sers['search']);
		$this->assign("page", $page->pageinfo());
	}else{
		$this->msg('请输入要搜索的内容');
	}
	....
}

 1. 这处代码将从 POST 请求参数的 sertype 和 search 作为一个数组元素的名称和值,在后续的处理中,能看到只有值被过滤和参数化,名称不会被过滤和参数化,而是直接拼接到 SQL 语句中。

 2. 第 1 处定义的 $where 数组作为实参传入 $this->article->arttotal(),article 属性是 article 模型对象。如果理解 MVC,就会知道数据模型负责与数据库打交道,操作数据模型就是操作数据库,所以这里执行的方法就包含执行 SQL 语句的过程。

 3. $where 数组被作为实参传入 $this->article->search(),这里第二次执行 SQL 查询。

注意到 $where 数组两次被传入数据模型的方法,也就是说 $where 数组的元素被拼接到两句不同的 SQL 查询语句中,这导致无法使用 union 联合查询注入(因为列数不同,反正笔者是想不到)。

跟进第 2 处代码 $this->article->arttotal($where):

// ArticleModel.class.php
class ArticleModel extends Model{
    ...
    public function arttotal($where){
	    return $this->where($where)->total();
    }
    ...
}

$ths->where() 和 total() 方法都属于父类 Model。$this->where() 不存在,所以会调用从父类继承的魔术方法 __call()。跟进 Model 类的定义:

class Model extends Db {
    ...
    protected $query = array("field"=>"", "where"=>"", "order"=>"",  "limit"=>"");
    ...

    
    public function __call($metthod, $args){
		$metthod=strtolower($metthod);
		if(array_key_exists($metthod, $this->query)){
			if(empty($args[0]) || (is_string($args[0]) && trim($args[0])==='')){
				$this->query[$metthod] = "";
			}else{
				$this->query[$metthod] = $args;       // 1
			}

			if($metthod == "limit"){
				if($args[0] == "0")
					$this->query[$metthod]=$args;
			}
		}else{
			val::mess("调用".get_class($this)."中的方法{$metthod}()不存在", "close");
		}
		return $this;
	}


    //获取数据库总数
	protected function total(){
		var_dump($this->query);
		exit();
		$where = "";
		$data = array();

		$args=func_get_args();
		if(count($args) > 0){
			$where = $this->towhere($args);
			$data = $where["data"];
			$where = $where["where"];
		}else if($this->query["where"] != ""){
			$where = $this->towhere($this->query["where"]);   // 2
			$data = $where["data"];
			$where = $where["where"];
		}

		$query = "SELECT COUNT(*) AS count FROM {$this->tabname}{$where}";  // 3
		return $this->query($query, __FUNCTION__, $data);
	}
}

__call() 的作用是设置 $query 属性的值,执行后,$query 属性值是这样的结构:

array(4) {
  ["field"]=>
  string(0) ""
  ["where"]=>
  array(1) {
    [0]=>
    array(2) {
      ["title"]=>
      string(6) "%test%"
      ["views"]=>
      int(0)
    }
  }
  ["order"]=>
  string(0) ""
  ["limit"]=>
  string(0) ""
}

前面的 $where 数组的元素都被“塞”到了 $this->query['where'][0] 中,将被用于组装 where 条件子句,具体执行位置在第 2 处代码 $where = $this->towhere($this->query["where"]),跟进:

private function towhere($args){
	$where = ' WHERE ';
	$data = array();
    ....
	foreach ($args as $option) {	
		if(empty($option)){
			...
		}else if(is_string($option)){
			....

		}else if(is_numeric($option)){
			....
		}else if(is_array($option)){
			.....

			foreach ($option as $key => $val) {
				if(is_array($val)){
					.....
				}else if(strpos($key, ' ')){
					$where .="{$key}?";
					$data[] = $val;
				}else if(isset($val[0]) && $val[0] == '%' && substr($val, -1) == '%'){	
					$where .="{$key} LIKE ?";
					$data[] = $val;
				}else{
					$where .= "{$key}=?";
					$data[] = $val;
				}

				$where .=" AND ";
			}

			$where =rtrim($where, "AND ");
			$where.=" OR";
			continue;
		}
	}
	$where=rtrim($where, "OR ");
	return array("where"=>$where, "data"=>$data);
}

$option 的结构是这样:

array(2) {
  ["title"]=>
  string(6) "%test%"
  ["views"]=>
  int(0)
}

 这两个数组元素经过 foreach 部分的处理,数组元素的值被分离,名称就会被组装成:

where title like ? and views=?

接着返回这部分,拼接到 SQL 语句中,见第 3 处代码。SQL语句就形成:

SELECT COUNT(*) AS count FROM zh_article where title like ? and views=?

然而,"title" 这里是可控,所以就导致了 SQL 注入。但是前面说过,又因为 "title" 还拼接到另一句 SQL 查询:

select id, pid, title, thumb, keyword, info, count, nums, posttime from zh_article where <注入点>

导致无法 union 联合查询注入,只能可以进行布尔盲注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值