目录
前言
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 联合查询注入,只能可以进行布尔盲注。