Thinkphp3.2.3-漏洞审计汇总
2022-9-25 22:58:32 Author: www.freebuf.com(查看原文) 阅读量:10 收藏

在这里插入图片描述

基础知识

0x00:为什么要使用框架

使用框架相当于别人已经帮助完成一些基础工作,开发者只需要集中精力在系统的业务逻辑设计上即可。而且相较于原生代码开发更稳定、安全、易扩展。

0x01:TP3.2.3程序目录结构

  • 入口文件(应用对外提供的接口)

  • 核心框架目录

  • 模块集合(Common模块优先于其他模块执行)

  • 缓存目录(./Application/Runtime

  • 公共资源目录(./Public

更详细的可以看官方手册上描述的:

@http://document.thinkphp.cn/manual_3_2.html#directory_structure

0x02:MVC分层架构

  • 控制器(Controller):负责用户请求的调度和处理业务逻辑

  • 模型(Model):负责业务数据的处理和与数据库的交互

  • 视图(View):提供了展示数据的各种方式

请添加图片描述

0x03:模板渲染&入口绑定

在TP3.2.3模板中先建立一个Admin文件夹,内容及文件夹名称Copy前端文件夹Home即可

请添加图片描述

如果想要更改默认首页内容,在Application\Admin\Controller\IndexController.class.php控制器)进行修改

在这里插入图片描述

在开发时一般后台所使用的Public文件夹是独立开的,因此这里后台也就需要更改一下默认的Public文件夹,需要先在应用入口文件中定义应用目录

在这里插入图片描述

然后在Admin\Conf\config.php处进行配置

在这里插入图片描述

最后将JS/CSS/HTML视图)文件进行引入,修改路径即可成功访问

在这里插入图片描述

在这里插入图片描述

除此之外,还是有很多知识是没有接触到的,上面控制器中代码有M,D,U,I这些方法,具体功能参考大师傅的文章:

@https://www.cnblogs.com/kenshinobiy/p/9165662.html

代码则位于Thinkphp/Common/functions.php

请添加图片描述

例如I方法主要用于更加方便和安全的获取系统输入变量,用法如下:

I('get.id'); <==> $_GET['id']
I('id'); <==> I('param.id') 
param	自动判断请求类型获取GET、POST或者PUT参数

其他函数的用法看手册即可

0x04:TP3路由格式:

请添加图片描述

这里通过一个简单的例子来了解常用的四种路由模式:

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index($name)
    {
        echo "hello ".$name;
    }
}

普通模式:

http://localhost/index.php?m=Home&c=Index&f=index&name=Sn0w
http://网址/index.php?m=模块名称&c=控制器&a=方法

兼容模式:

http://localhost/index.php?s=Home/Index/index/name/Sn0w
http://网址/index.php?s=/模块/控制器/方法

REWRITE模式:

http://localhost/Home/Index/index/name/Sn0w/
http://网址/模块/控制器/操作方法

PATHINFO模式:

http://localhost/index.php/Home/Index/index/name/Sn0w/
http://网址/index.php/模块/控制器/操作方法

最终呈现的效果都一样:

在这里插入图片描述

TP3.2.3 SQL注入

环境搭建:

先去官网上下载一份TP3.2.3源码,再创建一个数据库thinkphp和数据表users

@https://www.thinkphp.cn/Down

请添加图片描述

添加好数据后,将数据库配置设置好

请添加图片描述

访问一下Application目录下便会自动生成目录结构,接下来将控制器给配置好

Application/Home/Controller/IndexController.class.php
#更改内容为:
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function shy()
    {
        $data = M('users')->find(I('GET.id'));
        dump($data);
    }
}

测试一下效果:

在这里插入图片描述

下断点,至于如何设置PHPstorm调试的可以参考如下文章:

@https://blog.csdn.net/Xxy605/article/details/120973447

接下来就输入正常的SQL测试语句?id=1' or 1=1%23,看一下传入的参数所走的流程:

在这里插入图片描述

M方法先实例化一个数据库操作对象,F8跳过此方法到I方法中,不影响传入参数的代码就不再叙述,默认的filter是htmlspecialchars()

在这里插入图片描述

先经过htmlspecialchars()的处理,此函数默认是不转义单引号的,之后再回调think_filter函数进行过滤

在这里插入图片描述

跟踪一下这个函数,是黑名单过滤,看都过滤了哪些字符

在这里插入图片描述

最终参数输出为

在这里插入图片描述

I方法结束后,便会进入到find方法中去,这个方法作用是查询数据,F7跟着代码走一下,到分析表达式这块,这里调用了_parseOptions()这个函数

在这里插入图片描述

此时id还是之前传入的1' or 1=1#,跟进这个函数看做了什么处理,如下图进入字段类型验证这段代码

在这里插入图片描述

又有一个_parseType函数对我们传入的参数(即$options['where'])进行处理,此时id依旧是我们传入的,跟进这个函数继续看

在这里插入图片描述

这里的代码把我们传入的id进行了强制类型转换,使用了intval这个函数,到这一步我们传入的参数就变成了1,然后再返回给_parseOptions()这个函数,再进行查询便不会出现SQL注入

请添加图片描述

梳理一下:

id=1' or 1=1# -> M() -> find() -> _parseOptions() -> _parseType()

传入的参数便是在_parseType()这个函数处被处理了,但具体是怎么处理的还是没搞清楚,因为前面创建数据库id列使用的是int类型,所以到intval这个函数就会直接被处理,所以将id列改为varchar,F7继续跟进

在这里插入图片描述

进入buildSelectSql这个函数前,传入的参数还是正常的,跟进这个函数,进入到parseSql函数,作用是对原初的SQL语句进行替换,构造的idwhere那里,跟进一下parseWhere()

在这里插入图片描述

跟进到此,又发现调用了两个函数,此时传入的还是之前的

在这里插入图片描述

跟进parseWhereItem这个函数,发现到这一步又调用了parseValue

在这里插入图片描述

进行跟进看一下这个函数对传入的参数进行了什么处理

/**
     * value分析
     * @access protected
     * @param mixed $value
     * @return string
     */
protected function parseValue($value) {
        if(is_string($value)) {
            $value =  strpos($value,':') === 0 && in_array($value,array_keys($this->bind))? $this->escapeString($value) : '\''.$this->escapeString($value).'\'';
        }elseif(isset($value[0]) && is_string($value[0]) && strtolower($value[0]) == 'exp'){
            $value =  $this->escapeString($value[1]);
        }elseif(is_array($value)) {
            $value =  array_map(array($this, 'parseValue'),$value);
        }elseif(is_bool($value)){
            $value =  $value ? '1' : '0';
        }elseif(is_null($value)){
            $value =  'null';
        }
        return $value;
    }

那这就很明显了,第一个if条件判断成功,进入escapeString函数,使用addslashes()转义传入的单引号,后面就无需再跟进了就是拼接SQL语句

在这里插入图片描述

但代码中还有很多判断数组的代码,因此就要考虑一下id传数组,再跟进代码看一下是否存在SQL注入,最后用一张图来总结便是:

在这里插入图片描述

Thinkphp3.2.3 where注入

Payload:

thinkphp_3.2.3/index.php/home/index/shy?id[where]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23

在这里插入图片描述

先放出Payload,下面跟着代码看是怎么出现的这个where报错注入,这次传入的参数为数组?id[where]= 1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23I方法在最后的代码会对数组参数的每个成员使用think_filter函数

在这里插入图片描述

刚才跟着正常的SQL测试语句知道,知道think_filter函数是一个黑名单,过滤了一些特殊字符,但明显过滤的不是很全,updatexml、extractvalue这些报错函数都未过滤

在这里插入图片描述

继续跟进便进入了find函数到_parseOptions这个函数中,在进入此函数前$options

在这里插入图片描述

刚才正常传入的SQL测试语句只有进入到_parseType()后才会被intval函数给强制转换,但这里这个if判断中的is_array($options['where'])没有满足,此时的where的值不是数组

在这里插入图片描述

和上面正常传入的SQL测试语句对比一下,便会发现不同,最后通过此函数处理后传入的payload依旧没有变

在这里插入图片描述

F7继续跟进

在这里插入图片描述

select函数后,发现buildSelectSql的作用是拼接SQL语句

在这里插入图片描述

跟进这个函数

在这里插入图片描述

第一个if语句就是计算limit,这里不是重点,发现后面又调用了parseSql这个函数

在这里插入图片描述

构造的参数是在where,跟进看一下

在这里插入图片描述

此时的$whereStr是字符串,所以就没有经过中间的代码,直接返回此语句

在这里插入图片描述

F7跟进就会发现最终拼接的语句便还是我们传入的,并没有被过滤掉,便导致了thinkphp3.2.3 where注入

在这里插入图片描述

官方修补方法:

@https://github.com/top-think/thinkphp/commit/9e1db19c1e455450cfebb8b573bb51ab7a1cef04

在这里插入图片描述

v3.2.4$options$this->options进行了区分,从而传入的参数无法污染到$this->options,也就无法控制sql语句了

Thinkphp3.2.3 exp注入

payload:

?username[0]=exp&username[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)

在这里插入图片描述

把环境重新更改一下,使用全局数组进行传参,这里之所以不用I函数来获取参数,是因为I函数会回调think_filter()函数

function think_filter(&$value)
{
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if (preg_match('/^(NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}

过滤了EXP字符串,并会在后面拼接一个空格,这个点会影响exp注入,到后面便能了解了,打上断点,先进入where函数中

在这里插入图片描述

只有最后的代码对传入的参数发挥了作用,其作用便是把数组array('username' => $_GET['username'])传给$this->options['where'],继续跟进到find函数,一直到parseWhere()函数,观察到这一步和上面的where注入有哪些区别

在这里插入图片描述

传入的参数会进入到parseWhereItem()函数中,而where注入的是当作字符串直接跳过了这一段代码,先判断该变量是否为数组,再判断索引为0的值是否为字符串,到下面的代码还要验证该索引值是否等于exp

在这里插入图片描述

关键点在于下面的代码:

在这里插入图片描述

正常来说传入的参数是字符串即$val=test,但这里传入了数组,$exp便是$val[0]

在这里插入图片描述

又满足了elseif语句(跟到这里便也能理解为什么要用超全局数组,而不用I函数了,如果使用I函数不满足条件,便会异常抛出,从而影响注入),把where条件直接用点拼接,这时传入

username[0]=exp&username[1]==1 and test

便会造成SQL注入,最终拼接出来的语句便是

select * from users where `username`  $val[1]  limit 1

在这里插入图片描述

thinkphp3.2.3 bind注入

简述:由于框架实现安全数据库过程中在update更新数据的过程中存在SQL语句的拼接,并且当传入数组未过滤时导致出现了SQL注入

将环境更改一下

在这里插入图片描述

payload:

?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&password=1

传入参数,跟进一下代码,看一下为什么这样的payload能造成报错注入,跟前面几条链一样,参数会先赋值给$this->options['where']

在这里插入图片描述

再跟进到save方法中去,调用_parseOptions方法

在这里插入图片描述

到这一步$val为数组,不属于标量,所以不经过_parseType方法验证类型

在这里插入图片描述

从该方法中出来后$this->options赋值给$options,接下来便调用$this->db->update方法

在这里插入图片描述

在这里出现一段SQL语句和参数绑定,跟进parseSet这个方法查看

在这里插入图片描述

再跟进到$this->bindParam($name,$val);

在这里插入图片描述

因为$this->bind为空,所以$name为0,而$val为1

在这里插入图片描述

经过此方法的处理,SQL语句便存在一个:阻断了注入,最终绑定参数为:

在这里插入图片描述

产生的SQL语句为:

UPDATE `users` SET `password`=:0

再调用parseWhereItem方法拼接where部分处理后的语句,当$exp为bind时,$whereStr部分可控

在这里插入图片描述

两段SQL语句再进行合并,最终形成的SQL语句

UPDATE `users` SET `password`=:0 WHERE `id` = :0 and updatexml(1,concat(0x7e,user(),0x7e),1)

最后跟进execute方法,该方法中有对绑定参数的处理:

if(!empty($this->bind)){
   $that   =   $this;
   $this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
 }

这两行代码便是起到替换的作用,代码先是创建一个闭包,调用array_map,对$this->bind这个数组中的每个参数都调用这个闭包,对$this->bind进行处理,未处理前为array(":0"=>"1");,处理后为:array(":0"=>"'1'");,最后再通过strtr函数处理$this->queryStr语句,得到的结果如下:

在这里插入图片描述

:0替换为外部传进来的字符串,所以将传入参数等于0,这样就拼接了一个:0,然后会通过strtr()被替换为1,SQL语句便可以正常执行,这也就是为什么payload中id[1]=0

在这里插入图片描述

官方修复:

过滤bind即可

在这里插入图片描述

TP3.2.3 RCE漏洞

业务代码中如果模板赋值方法assign的第一个参数可控,则可导致模板文件路径变量被覆盖为携带攻击代码的文件路径,造成任意文件包含,执行任意代码

在这里插入图片描述

程序会进入模板渲染方法中,需要先创建对应的模板文件(View),模板文件位置为:\Application\Home\View\Index\index.html,这里的模板渲染方法除了display,也可以为fetch、show

在这里插入图片描述

但使用fetch会有一些区别,如上图其程序逻辑会使用到ob_start()打开缓冲区,所以PHP代码的数据块和echo()输出都会进入缓冲区而不会立刻输出,如果想要fetch方法对应的攻击代码输出的话,需要在攻击代码末尾带上exit()die()

Debug开启和关闭是有一些区别的,具体如下:

Log记录目录:

若开启debug模式日志会到:\Application\Runtime\Logs\Home\

若未开启debug模式日志会到:\Application\Runtime\Logs\Common\

这里以Debug关闭为例(define('APP_DEBUG',false)

构造请求包:

在这里插入图片描述

构造攻击请求:

index.php?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Common/22_08_03.log

在这里插入图片描述

Debug开启,正确的请求日志也会被记录到日志中,只是日志路径不 一样了而已

在这里插入图片描述

下面跟着代码来走一遍,看看此payload如何触发文件包含导致RCE

先传入参数会先进入assign函数中,再赋值给$this→tVar

在这里插入图片描述

之后会进入到display方法中,display方法开始解析并获取模板文件内容,此时模板文件路径和内容为空

在这里插入图片描述

再进入到fetch方法中,此时传入的参数为空,程序会根据配置去获取默认模板文件的位置

在这里插入图片描述

TMPL_ENGINE_TYPE配置为php时,会采用PHP原生模板,默认的为Think,便进入到else分支中,获取$this→tVar变量值赋值给$params,之后再进入到Hook::listen方法中

请添加图片描述

进入exec方法中,处理后调用Behavior\ParseTemplateBehavior类中的run方法处理$params这个带有日志文件路径的值

请添加图片描述

程序进入run方法中,一系列判断后,进入else分支,调用Think\Template类中的fetch方法对变量$_data(带有日志文件路径的变量值)进行处理

请添加图片描述

进入Think\Template类中的fetch方法,获取缓存文件路径后,再进入Storage的load方法中

在这里插入图片描述

$_filename为之前获取的缓存文件路径,$var则为之前带有_filename=日志文件路径的数组,$vars不为空则使用extract方法的EXTR_OVERWRITE默认描述对变量值进行覆盖,之后include该日志文件路径,造成文件包含,最终导致包含文件造成RCE

Thinkphp3.2.3 日志泄露

前提:开启了Debug

THINKPHP3.2 结构:Application/Runtime/Logs/Home/年份_月份_日期.log

参考博客:

@https://y4er.com/post/thinkphp3-vuln/

@https://blog.csdn.net/rfrder/article/details/114024426

@https://www.cnblogs.com/zpchcbd/p/12552185.html

@https://paper.seebug.org/573/

@https://mp.weixin.qq.com/s/_4IZe-aZ_3O2PmdQrVbpdQ

@https://blog.csdn.net/Mruos/article/details/109802121

@https://blog.csdn.net/qq_33382313/article/details/51702787

@https://www.cnblogs.com/lingzhisec/p/15728886.html


文章来源: https://www.freebuf.com/articles/web/345544.html
如有侵权请联系:admin#unsafe.sh