# ThinkPHP5 SQL注入漏洞 && 敏感信息泄露【通过】

**URL:** <https://forum.90sec.com/t/topic/407>\
**Category:** 账号审核\
**Created:** [2019 年9 月 18 日 03:05 UTC](https://forum.90sec.com/t/topic/407 "2019-09-18T03:05:56Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![K5ye533](https://forum.90sec.com/user_avatar/forum.90sec.com/k5ye533/32/2792_2.png) [@K5ye533](https://forum.90sec.com/u/K5ye533)\
发布日期： [2019 年9 月 18 日 03:05 UTC](https://forum.90sec.com/t/topic/407/1 "2019-09-18T03:05:56Z")

</div>

# ThinkPHP5 SQL注入漏洞 && 敏感信息泄露

# 介绍：

ThinkPHP是一个快速、兼容而且简单的轻量级国产PHP开发框架，诞生于2006年初，原名FCS，2007年元旦正式更名为ThinkPHP，遵循Apache2开源协议发布，从Struts结构移植过来并做了改进和完善，同时也借鉴了国外很多优秀的框架和模式，使用面向对象的开发结构和MVC模式，融合了Struts的思想和TagLib（标签库）、RoR的ORM映射和ActiveRecord模式。

Poc ： [http://172.20.0.3/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1](http://172.20.0.3/index.php?ids%5B0,updatexml(0,concat(0xa,user()),0)%5D=1)

影响： \<5.0.9

# 前言

这是第一篇漏洞复现文章，学了php但是在之前还没有接触过thinkphp框架

一个鸡肋的sql注入漏洞

以下是执行大概流程，一遍看一边调试

 ![image](https://forum.90sec.com/uploads/default/original/2X/3/3de27bbff120b59e5265823a7834827970a7218f.png)
## 开始

先在kali安装vulhub

> **[使用 Docker 快速搭建靶场 - 开发者头条](https://toutiao.io/posts/4a8rfn/preview)**

  
教程文章

 ![image](https://forum.90sec.com/uploads/default/original/2X/f/f51452386929f659853812455a9cd47d308f63e3.png)

第一次用打开发现出问题了，去网上搜索了半天没发现解决方法，就去安全群问了一下发现是没有启动docker

 ![image](https://forum.90sec.com/uploads/default/original/2X/8/8890f7fdde2ad8c50282e696fb319ffcbfd54597.png)

打开复现环境执行payload

 ![image](https://forum.90sec.com/uploads/default/original/2X/7/70b02867cd5532b2a1ad76e532d728c1811ba0c5.png)

先看漏洞的形成原因，‘控制了预编译SQL语句中的键名’

 ![image](https://forum.90sec.com/uploads/default/original/2X/4/4f874e5eba0bab083b557b060eb998a653b09d01.png)

搭建本地环境用于调试 这里用的demo 是

> **[vulhub/vulhub](https://github.com/vulhub/vulhub/tree/master/thinkphp/in-sqlinjection/www)**
>
> Pre-Built Vulnerable Environments Based on Docker-Compose - vulhub/vulhub

![image](https://forum.90sec.com/uploads/default/original/2X/8/89e1eb1250bbd0a4078868a56d8273c1689f6034.png)

主要分析以上的内容

 ![image](https://forum.90sec.com/uploads/default/original/2X/7/7401dfe37e27fe09417a9579ad8a72cb62601608.png)

$ids = input('ids/a');

Ids/a 是将input接受到的数据强行转换成数组

![image](https://forum.90sec.com/uploads/default/original/2X/d/dfe5543d9155c33934d36a219fa04f0f4c716d95.png)

$ids值 就是上面浮窗的数组

$t 是以下

![image](https://forum.90sec.com/uploads/default/original/2X/d/d44c0c3a0c7bc18ebc1b771cbcb09d00f58e1714.png)

下断点进入

![image](https://forum.90sec.com/uploads/default/original/2X/3/37c0bf9ac787fc47f34810d5b7fe9390a98a3810.png)

$t调用了where和select 函数，先进入select函数分析 位置在（thinkphp\library\think\db\Query.php）

重点是2306行的生成sql语句 不过在这之前先看个变量后面才好理解

$opthins =

![image](https://forum.90sec.com/uploads/default/original/2X/9/97321f8b4f601d689e03bd95607e9979ffb67c2f.png)

分析成了用于方便sql的对象

进入2306行

```
$sql = $this->builder->select($options);

```

进入了（`thinkphp\library\think\db\Builder.php`）

 ![image](https://forum.90sec.com/uploads/default/original/2X/b/bce792261bc5bf7669865206a1d12c8818a68e88.png)

会执行637行的代码进入parseWhere函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/9/99e62c1ad8a88d58f8bf66e0c228a4f010dd1f76.png)

224行调用了buildWhere函数

进入243行的buildWhere函数

buildWhere函数中主要有以下重要的(主要是IN查询)

```
    $bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field);
    if (preg_match('/\W/', $bindName)) {
        // 处理带非单词字符的字段名
        $bindName = md5($bindName);
    }

} elseif (in_array($exp, ['NOT IN', 'IN'])) {
        // IN 查询
        if ($value instanceof \Closure) {
            $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value);
        } else {
            $value = is_array($value) ? $value : explode(',', $value);
            if (array_key_exists($field, $binds)) {
                $bind = [];
                $array = [];
                foreach ($value as $k => $v) {
                    if ($this->query->isBind($bindName . '_in_' . $k)) {
                        $bindKey = $bindName . '_in_' . uniqid() . '_' . $k;
                    } else {
                        $bindKey = $bindName . '_in_' . $k;
                    }
                    $bind[$bindKey] = [$v, $bindType];
                    $array[] = ':' . $bindKey;
                }
                $this->query->bind($bind);
                $zone = implode(',', $array);
            } else {
                $zone = implode(',', $this->parseValue($value, $field));
            }
            $whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';
        }
    } 
    ....
    return $whereStr;
}

```

一开始他是有对$bindName进行过滤的，但是$value是数组他会用foreach把$bindName和键名进行拼接

![image](https://forum.90sec.com/uploads/default/original/2X/e/e7e58e26ab5d3071e94bdda7e244b9ec8fc08bc4.png)

最后形成

![image](https://forum.90sec.com/uploads/default/original/2X/a/aa5314eebd6592f04f297add1726eef7bcdc1473.png)

导致在编译SQL语句的时候发生错误，从而产生报错

为什么叫鸡肋漏洞呢，查了原因是因为thinkphp用了PDO来连接数据库的

通常，PDO预编译执行过程分三步：

- prepare($SQL) 编译SQL语句
- bindValue($param, $value) 将value绑定到param的位置上
- execute() 执行

我们在第二部的$param那里用报错语句，在执行到第二时直接抛出错误无法执行到第3，  
在$SQL中也一样 ，如果$SQL等于报错语句就会直接报错无法执行二和三

还有一个主要原因是thinkphp默认配置PDO::ATTR\_EMULATE\_PREPARES =\> false

引用大佬的话：  
这个选项涉及到PDO的“预处理”机制：因为不是所有数据库驱动都支持SQL预编译，所以PDO存在“模拟预处理机制”。如果说开启了模拟预处理，那么PDO内部会模拟参数绑定的过程，SQL语句是在最后execute()的时候才发送给数据库执行；

# 远程执行代码漏洞

Poc:

```
index?s=index/\think\container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

```

版本：5.0.5-5.0.22  
5.1.0-5.1.30

调试注意要点:因为提前设置了断点导致提前跳转到固定的函数中，导致调试失败参数失败无法运行

# 复现

 ![image](https://forum.90sec.com/uploads/default/original/2X/2/2f48f75029d507be071b39c6f8a60fe07d1691d4.png)

下载5.0.22的thinkphp  
下载地址：

```
http://www.thinkphp.cn/down/1260.html

```

```
http://localhost/thinkphp_5.0.22_with_extend/public/index.php?s=captcha
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami

```

```
_method=__construct&filter[]=system&method=get&get[]=whoami 

```

这里2个都能达到同样的效果，如果是调试建议第2个

一开始复现的时候以为要demo,结果不用

设置断点进入App.php

![image](https://forum.90sec.com/uploads/default/original/2X/f/f601a8e5a3eab62e945d83dad2a645e896ad5e33.png)

创建了一个http请求实例

 ![image](https://forum.90sec.com/uploads/default/original/2X/0/0c3fde4ed58cb021d3ce44319fe7b25f3b8908fe.png)

继续往下

来到了115行，这里会进去路由检测传入的参数是，新建的http实例，和配置信息  
参数赋值在79和81

 ![image](https://forum.90sec.com/uploads/default/original/2X/5/52d2a8d0d215ca738d11cb36300e9f1828a4aa62.png)

进入后发现$request-\>path()函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/e/e39f204c12a1fa601d96d4d8f9ba18bd4c9f5226.png)

我们进入path函数在 433行

![image](https://forum.90sec.com/uploads/default/original/2X/3/382123d11c1477244baced1fe355d4ad7c82eeeb.png)

这里this是http实例$request调用的  
437会进入用this调用进入pathinfo函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/5/5ae670218b4ec79dc95e8034da2e9a8b5e5b9a1e.png)

进入后发现会用`(isset($_GET[Config::get('var_pathinfo')])) `进行检测

 ![image](https://forum.90sec.com/uploads/default/original/2X/3/35601789ce990b6f5274e4f55c74f4095ab3da3e.png)

这里会进入，说明一下Config::get('var\_pathinfo')是配置文件中的设置的参数，默认值为s，从GET中获取键值这里给的是captcha

![image](https://forum.90sec.com/uploads/default/original/2X/a/a735e593bcd368648c4f938caa6a3e86d46dd879.png)

 ![image](https://forum.90sec.com/uploads/default/original/2X/9/93bd14c34409ea52bc11423a18a20da405b09de0.png)

这里把给this.pathinfo赋值

 ![image](https://forum.90sec.com/uploads/default/original/2X/e/edbab6df9f243a790f8e7fe54eba916c3cb2fd01.png)

这里把返回值赋值给$pathinfo , 有赋值给this.path，最后return this.path

 ![image](https://forum.90sec.com/uploads/default/original/2X/d/d13fe76916ceaf72bb518f0eade3eef107221bfb.png)

继续执routeCheck函数，会在642行进入，check函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/f/f8169ecf2380fbf1491459ef6b7889653d997f7e.png)

![image](https://forum.90sec.com/uploads/default/original/2X/3/3db5a0430b3d0867e0d615e7d6d69b87a921708f.png)

在Route.php中第839行进入check

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/cabf017de725d70231d490c9c098b822b0fdb0fb.png)

857会调用method函数

![image](https://forum.90sec.com/uploads/default/original/2X/e/eba8c0d71f0c6ca718ef365a77a239d4f45858a7.png)

进入method，

![image](https://forum.90sec.com/uploads/default/original/2X/f/fc68a3fb94070337f9867ec281c61c1dd1d59205.png)

在524中的` isset($_POST[Config::get('var_method')]) var_method`和上面的一样，配置文件中的设置的参数 获取\_method的值，这里是`__construct`  
给`this->method `赋值大写的`__construct`，然后用this调用构造函数参数为发送post数据的数组形式

![image](https://forum.90sec.com/uploads/default/original/2X/0/0fc4d743a33dee6c94113342feb1a4e7c5c5bb77.png)

 ![image](https://forum.90sec.com/uploads/default/original/2X/1/184af43d33cb4c71265f960a7dc53a9ca8bc7b3e.png)

进入\_\_construct函数，这里的foreach会遍历所有变量，把http实例中存在的覆盖，不存在的添加 method为get

![image](https://forum.90sec.com/uploads/default/original/2X/b/b8a1becdaa6bc26f4b6c1125d5d287042b9a96f9.png)

返回method函数，把getthis-\>method返回，也就是get

![image](https://forum.90sec.com/uploads/default/original/2X/1/1ed7d871daa2e87c8ef19abad4abe22ec78d30f7.png)

返回check函数，最后执行并返回

![image](https://forum.90sec.com/uploads/default/original/2X/7/736562bf436632205bea2c214cac0679c6257b3d.png)

![image](https://forum.90sec.com/uploads/default/original/2X/0/0adf8429e91a2bc9ec2036651d9f8c49c175cd9e.png)

返回内容

在这调用完成之后routeCheck将返回method赋值给dispatch

![image](https://forum.90sec.com/uploads/default/original/2X/c/cf74aaf1a5c4f3ae26faecda5f962b4d53dd8da6.png)

继续执行run到，138行会执行exec

![image](https://forum.90sec.com/uploads/default/original/2X/6/69ca66e852f3a4577cd6464a81db3c62987477d7.png)

到exec函数里，这里会因为type为method执行相应的分支。

468行，有调用param函数 634

![image](https://forum.90sec.com/uploads/default/original/2X/4/43b2c6d62297dac740eb0e60c3b342d3a2e740cd.png)

突然发现这里的method已经是返回执行whoami后的数据了，

不过不能显示出来  
继续执行到了，661 执行了input函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/9/9b980f3240bdc7c6f371dfdfa57773d75a7b0f66.png)

 ![image](https://forum.90sec.com/uploads/default/original/2X/7/76a32d2e9947eabdcc9a9f61893e2bdf18f3c120.png)

要执行array\_walk\_recursive函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c168b29dd6dd5241aa4b35c39c0688bcb60a29c3.png)

 ![image](https://forum.90sec.com/uploads/default/original/2X/4/476e03cc8a4c36f79679f49312e4890fa12f1b7e.png)

这里用system做参数，$tada为null,执行filterValue  
Foreach循环 $

$value值为whoami $key是0 $filters是

![image](https://forum.90sec.com/uploads/default/original/2X/d/dac3c0903fa97b68a35c16eb216147bd25c2f8d4.png)

![image](https://forum.90sec.com/uploads/default/original/2X/4/4b345272500e9e4be142a3eaa076b4371bf869cb.png)

到1083就是一个回调函数，$filter当作函数，$value当作参数执行

 ![image](https://forum.90sec.com/uploads/default/original/2X/6/69f33f944195decf180de68678242fa4e3dbc4ec.png)

完成

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/cc8e60723cf4e66eb4f399c749a702493bd118f3.png)

首先$filters是一个数组而且至少2个值而且值要是一个函数或者方法才能执行call\_user\_func函数

往上

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c0b9bd7bf9ce8c5653459dc77abdc03d6d007d5f.png)

这里$data也要是数组才能执行array\_walk\_recursive函数  
这里this要指向一开始创建的http实例，然后$filter要是一个空值

_Poll ([view on site](https://forum.90sec.com/t/topic/407/1))_
