Thinkphp框架 < 5.0.16 sql注入漏洞分析(每日一洞)
2018-04-11

0x01 前言

前天在公众号看到石大神发的一篇审计thinkphp的文章,就想写一个分析流程,delay到了今天。昨天在先知也看到了chybeta发的一篇分析文章感觉也不错。分析过程,我也会做thinkphp部分功能的解析。
废话不多说,开始吧!

0x02 环境搭建和漏洞复现

程序下载地址:http://www.thinkphp.cn/down/1126.html
PHPstudy:Apache+php5.6+MySQL
工具:PHPstorm

  1. 首先建立一个数据库名为:thinkphp
  2. 建立一个表名为:user
  3. 添加三个字段:id,name,password
  1. 在thinkphp的数据库文件填上刚才我们建立的数据库信息:
    文件位置:\thinkphp\application\database.php
  2. 打开thinkphp的调试模式:
    文件位置:\thinkphp\application\config.php
  3. 简单写一个update功能,石大神用到了模型,这里就简单写一个例子就行了。
    文件位置:\thinkphp\application\index\controller\Index.php
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <?php
    namespace app\index\controller;

    class Index
    {
    public function index()
    {
    $password = input('get.password/a');
    db('user')->where(['id'=> 1])->update(['password'=>$password]);
    }
    }
    这里用到了thinkphp的助手函数input(),是专用来接收get,post等的值。具体可以看:https://www.kancloud.cn/manual/thinkphp5/118044

还有就是thinkphp的数据库操作,框架本身写好了我们调用就比较方便。所以为什么那么多人用框架去开发程序,快捷而且安全,不过也会有安全问题,就像今天这个sql漏洞,不过如果是新手的话总比自己写的好对吧哈。
具体可以看链接:https://www.kancloud.cn/manual/thinkphp5/135178

  1. 现在就可以访问我们的payload了:
    1
    http://thinkphp.test/thinkphp/public/index.php?password[0]=inc&password[1]=updatexml(2,concat(0x7e,user()),0)&password[2]=1

    0x03 漏洞分析

  2. 这里用phpstorm+debug来动态分析,有不懂配置的可以访问我写一篇配置文章:利用phpstorm+xdebug进行断点调试
    我们在主函数下一断点:

然后访问我们payload,它会跳到入口文件,我们只要分析的是sql执行的地方,所以我们直接F8跳到执行sql地方:

  1. 我们继续跟进到loader.php它会包含thinkphp的Db.php文件,

    接下还会包\thinkphp\library\think\db\connector\Mysql.php文件,主要是连接数据库的操作,这里就直接跳过了。别分析分析把自己绕进去了,我只是在这里讲诉下过程,我们还是直接分析sql执行的部分吧。
  2. 我们跳到update执行的部分,文件位置:\thinkphp\library\think\db\Query.php

    继续往下看,这句是执行我们sql的地方:
  3. 我们F7跟进去,跳到文件位置\thinkphp\library\think\db\Builder.php
    parseTable函数直接F8往下执行了,这函数是处理table分析的,主要还是parseData函数,我们继续F7跟进
  4. 我们继续往下跟进

    我们看到了这里如果传入的值为数组形式的话,并且第一个参数为inc就执行switch所对应的的语句。

可以看到这里函数对我们传入的值没有做任何处理,返回内容仍然是我们的语句:

跟到后面返回的执行sql语句:

执行完,我们跟进到报错的地方,说明我的语句执行成功:

6.到这里就差不多结束了,有人问,为什么要这样给数组三个字段?
我们可以看到我们刚才传入数组的地方,分别有三个数组

到后面返回$result的时候就组合在一起了:

下面是parseData的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
protected function parseData($data, $options)
{
if (empty($data)) {
return [];
}

// 获取绑定信息
$bind = $this->query->getFieldsBind($options['table']);
if ('*' == $options['field']) {
$fields = array_keys($bind);
} else {
$fields = $options['field'];
}

$result = [];
foreach ($data as $key => $val) {
$item = $this->parseKey($key, $options);
if (is_object($val) && method_exists($val, '__toString')) {
// 对象数据写入
$val = $val->__toString();
}
if (false === strpos($key, '.') && !in_array($key, $fields, true)) {
if ($options['strict']) {
throw new Exception('fields not exists:[' . $key . ']');
}
} elseif (is_null($val)) {
$result[$item] = 'NULL';
} elseif (is_array($val) && !empty($val)) {
switch ($val[0]) {
case 'exp':
$result[$item] = $val[1];
break;
case 'inc':
$result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
break;
case 'dec':
$result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
break;
}
} elseif (is_scalar($val)) {
// 过滤非标量数据
if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
$result[$item] = $val;
} else {
$key = str_replace('.', '_', $key);
$this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
$result[$item] = ':data__' . $key;
}
}
}
return $result;
}

0x04 结束

如果看不懂的,可以先去了解一下thinkphp这个框架,其他框架大同小异。如果里面一些PHP代码看不懂的话,可以去复习下PHP。

0x05 参考

https://www.kancloud.cn/manual/thinkphp5/

https://mp.weixin.qq.com/s?__biz=MzU2NzE3MTU0Mw==&mid=2247483720&idx=1&sn=973bd7daa287a9e9c0171e852db6cb6b&chksm=fca001f0cbd788e6ed3119de5d3c4bfc3325205c5dd7f296e55a2ba313f73b3808525d1022e6&mpshare=1&scene=23&srcid=04104vXPMKRNVfrVzzzg7HXg#rd

https://chybeta.github.io/2018/04/10/Thinkphp%E6%A1%86%E6%9E%B6-5-0-16-sql%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/