Thinkphp框架 3.2.x sql注入漏洞分析(每周一洞)
2018-04-18

0x01 前言

Thinkphp 3.2.x用的也挺多的,以前的程序大部分都是用这边版本的,如果移动到2版本又挺麻烦,而且小程序不说,大程序就复杂很多了。

这次的漏洞是出现在WHERE这这个地方。

0x02 环境搭建和漏洞复现

程序下载地址:http://www.thinkphp.cn/donate/download/id/610.html
PHPstudy:Apache+php7.1+MySQL
工具:PHPstorm

  1. 首先建立一个数据库名为:thinkphp
  2. 建立一个表名为:user
  3. 添加两个字段:name,pass

  1. thinkphp3.2版本和之前的5版本略有不同,它的数据库信息文件是在
    这个文件:thinkphp/ThinkPHP/Conf/convention.php
    在这里面填上我们刚才建立的数据库信息:

  2. 打开thinkphp的调试模式:
    在刚才的文件:thinkphp/ThinkPHP/Conf/convention.php
    然后修改为true:
    'SHOW_ERROR_MSG' => true, // 显示错误信息

  3. 这个3.2版本的控制器位置又是和5版本有点区别。具体的可以在本文的参考链接详细查看thinkphp3.2版本的开发手册。
    文件位置:thinkphp/Application/Home/Controller/IndexController.class.php
    在里面写一个简单的update的例子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?php
    namespace Home\Controller;
    use Think\Controller;
    class IndexController extends Controller
    {
    public function index()
    {
    $tj['name'] = I('name');
    $data['pass'] = '111111';
    $res = M("user")->where($tj)->save($data);


    }
    }

I函数和5版本的input助手函数差不多,具体可以看:http://document.thinkphp.cn/manual_3_2.html#input_var

因为我们仅仅执行基本的CURD操作,所以用M方法来实例化数据库对象就行了。
实例化模型可以看:http://document.thinkphp.cn/manual_3_2.html#model_instance

  1. 那么现在就开始执行我们的Payload:
    http://127.0.0.1/thinkphp/index.php?name[0]=bind&name[1]=0 and updatexml(2,concat(0x7e,user()),0)

0x03 漏洞分析

  1. 这里也是用phpstorm+debug进行动态分析,配置方法在我上一篇的5版本注入文章里面也写过了,如有不懂可以去查阅。
    我们先在I函数这里下一个断点,看下I函数有没有过滤掉我们的输入。

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

  1. F7跟进,它会跳到文件:thinkphp/ThinkPHP/Common/functions.php这里。
    判断传入的值是用什么方式:

接下来就是对我们传入的值进行过滤:

对我们传入的bind值进行过滤:

  1. 然后执行我们的数据库更新的操作:

    我们F7继续跟下去,有些影响不大的函数可以直接F8跳过去,到save函数:

    执行到update函数我们F7跟进去:
  2. 我们继续往下,F7进入parseSet函数,可以看到赋值了一个占位符0给pass,代替我们的密码111111
  3. parseSet函数执行完后,我们继续F7进入parseWhere函数。

    我们再往下面的parseWhereItem函数进行分析,这里写到,$exp=$val[0]也就是我们传入的bind
    1
    $exp	=	strtolower($val[0])
    这里判断如果我们传入的数组[0]为bind就进行拼接:
  4. 从拼接这里也能看出我们为什么要把第二个数组构造的时候填0,因为是要对应上面parseSet函数,赋值了一个占位符为0,用来代替上面的密码111111,而接下来就没有这种赋值操作了,所以我们如果填其他值得话执行SQL语句的时候就会报错,到后面我会点出在哪里的。
  5. 我们继续,F7进去执行预处理语句的地方:

    我们可以看到这里用到了bindValue,绑定一个值到一个参数,也就是把111111密码绑定到:0这里。
  6. 继续跟下来,到SQL语句执行报错的地方,可以很清楚看到直接了我们的报错语句updatexml的值:

我们再往error函数跟,可以看到替换掉占位符的语句:

  1. 如果我们把0换成1会是怎么样的结果呢?
    访问payload,注意已经把数组第二个0改成1了。
    http://127.0.0.1/thinkphp/index.php?name[0]=bind&name[1]=1 and updatexml(2,concat(0x7e,user()),0)

我们把断点直接断在SQL执行的地方:

F7进去然后F8直接走,走到执行报错的地方,我们就会看到如果为1的话那SQL就无法执行下去了。

0x04 结束

如果调试的时候觉得跟得有点乱的时候,可以耐心的重头调试多几次就清楚了。

0x05 参考

http://php.net/manual/en/book.pdo.php

http://document.thinkphp.cn/manual_3_2.html

http://mp.weixin.qq.com/s?__biz=MzU2NzE3MTU0Mw==&mid=2247483723&idx=1&sn=33f444a594803fc0cd787a46281eefe4&chksm=fca001f3cbd788e5e972f9c9865400d21770d844a2b42a948f9218ba449941f280ab4d769c11&mpshare=1&scene=23&srcid=0416RyLN0oA31jzv9DnqM5cb%23rd