一文速通thinkphp3.2.3代码审计
ThinkPHP是一个快速、简单的基于MVC和面向对象的轻量级PHP开发框架,遵循Apache2开源协议发布,从诞生以来一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,尤其注重开发体验和易用性,并且拥有众多的原创功能和特性,为WEB应用开发提供了强有力的支持。
3.2版本则在原来的基础上进行一些架构的调整,引入了命名空间支持和模块化的完善,为大型应用和模块化开发提供了更多的便利。
详细见:https://www.kancloud.cn/manual/thinkphp/1679
目录结构
其中框架目录Thinkphp结构如下:
开始使用
整个网站的入口文件就是www/index.php,index.php包含了框架的入口文件,所以访问后可以直接加载thinkphp框架

访问成功运行,框架会自动在www/Application/Home/Controller/目录下生成IndexController.class.php,这里修改内容如下:

再次访问

配置文件
thinkphp的配置文件在www/ThinkPHP/Conf/convention.php

URL模式
入口文件是应用的单一入口,对应用的所有请求都定向到应用入口文件,系统会从URL参数中解析当前请求的模块、控制器和操作:
这是3.2版本的标准URL格式。
url默认是大小写敏感的,也可以通过修改convertion.php,达到url不区分大小写的目的
在访问入口文件时,如果没有指定模块、控制器、方法,默认会访问HOME模块下面的Index控制器的index方法,所以下面访问是等效的,这也是为什么修改了index方法后可以直接体现出来
这种URL模式就是系统默认的PATHINFO模式,不同的URL模式获取模块和操作的方法不同,ThinkPHP支持的URL模式有四种:普通模式、PATHINFO、REWRITE和兼容模式,可以在convertion.php设置URL_MODEL参数改变URL模式。

实战练习,ctfshow569
提示flag在Admin模块,Login控制器,ctfshowLogin方法。前面看的配置文件可知默认url模式为pathinfo,所以构造url:
路由
要使用路由功能,前提是你的URL支持PATH_INFO(或者兼容URL模式也可以,采用普通URL模式的情况下不支持路由功能),并且在应用(或者模块)配置文件中开启路由:

然后就是配置路由规则了,在模块的配置文件中使用URL_ROUTE_RULES参数进行配置,配置格式是一个数组,每个元素都代表一个路由规则,例如:
规则路由
规则路由是一种比较容易理解的路由定义方式,采用ThinkPHP设计的规则表达式来定义。
实战ctfshow570
下载好源码,看到common内的全局路由配置如下:
因为url里不方便传斜杠,使用post传命令,访问url:
Thinkphp的渲染机制
Thinkphp的控制器中有show函数,可以将文本渲染成html的网页

如果渲染时传入的变量未经过滤,则会导致代码执行,跟进show函数,调用了display函数

继续跟进,代码在fetch函数中,完成了解析渲染

fetch函数中重点关注输出缓冲区中的代码

如果引擎类型是php,则使用eval渲染,会直接执行代码。但是thinkphp的默认渲染引擎是Think,所以重点关注listen函数,listen函数中,exec存在输出的可能居高

跟进exec发现是一个类的动态创建

模板的代码传递给了这个类,跟进这个类

run内没有其他输出的地方,而且模板内容和模板的缓存位置被传递进load,所以跟进load函数,有call_user_func_array,可以动态的调用系统函数,这里调用的就是load函数

load里有include包含了缓存代码,之前传入的php就是在这里执行,然后包含结果存入输出缓冲区中

后面被读取到后,返回给了浏览器。
实战ctfshow571
题目拿到文件目录,发现Index控制器存在无过滤参数:


读取flag

信息泄露
1、ThinkPHP在开启DEBUG的情况下会在Runtime目录下生成日志,而且debug很多网站都没有关
2、ThinkPHP默认安装后,也会在Runtime目录下生成日志
THINKPHP3.2 结构:Application\Runtime\Logs\Home\16_09_09.log
THINKPHP3.1结构:Runtime\Logs\Home\16_09_09.log
日志存储结构是 :项目名\Runtime\Logs\Home\年份_月份_日期.log
参考链接:https://juejin.cn/post/7028872177424269343
实战ctfshow web572

burp抓包爆破日志文件
发现后门

SQL注入
1.使用find查询
在执行用户传入的参数前,会先执行
获取列名和类型,然后再生成用户的sql语句。实例化M的时候,查询列的语句不可控,这里重点看生成用户sql语句的具体流程:

获取了GET提交的id参数,然后传进find函数

这里查询的列是id,实例化M实际上是创建一个Model类,pk的值在Model类里已经预定义为id,定义位置在/tp3.2.3/ThinkPHP/Library/Think/Model.class.php

thinkphp把碎片化的参数组合成一个sql语句大体原理,其存在语句模板,执行查询操作就使用查询模板。获取全部参数后,再替换模板原本的字段。
获取参数的最后一步是解析参数,即分析用户提交的参数。具体的做法是把用户提交的参数传入_parseOptions函数:

在_parseOptions函数里会获取用户查询的目标表、列的信息,后面就会看到为什么获取这些信息了:

在这里进入了_parseType函数,解析用户提交参数的类型

这里依据之前获得的表、列信息,对参数类型进行转换,如果数据库中的id列是int类型,则进行intval转换,float则floatval

所以如果数据库中的id是int类型(mysql的id列默认是int(3)),在这里payload会被转换成纯数字,这里我把数据库id类型改成了varchar(255),后面没有什么操作,返回了参数

再传进select函数,生成sql语句:

buildSelectSql函数生成了sql语句,query函数执行了语句,读取结果。先进buildSelectSql函数

没有操作,进parseSql函数看看:

这里就是之前提到的模板替换,如果不是要查询的参数,就替换成空,这里id是在where后面,所以进入parseWhere函数

在判断完参数名后,进入parseWhereItem函数检查参数值,然后进入了parseValue函数

进入parseValue后,进入了escapeString函数,在这里执行了addslashes函数,所以直接单引号闭合是不行的。
网上addslashes绕过方法没成功,所以想办法让程序跳过过滤。在最开始看到如果传入的参数是数组,就会直接跳过获取列名,列值这一步:

这样就可以操控parseSql内替换的值

原本where的是一个键值对的数组,我们令他是一个字符串,这样进入parseWhere函数后会跳过过滤

这样就直接在where后面拼接了值,所以构造payload思路就来了:

实战ctfshow 573
题目源码:
https://blog.csdn.net/miuzzx/article/details/119424071
payload:


2.使用where查询(传入字符串)
生成的sql语句是:
直接闭合即可造成sql注入
实战ctfshow web574

3.反序列化注入
thinkphp3.2.3反序列化用户参数的时候,会导致sql注入。
参考:https://www.freebuf.com/articles/web/329045.html
寻找可以直接触发的魔术方法,首选__destruct,因为触发门槛最低,看网上大佬做法是全局搜索所有析构函数,再逐个去看,这里搜索到了12个:

但是大部分参数直接传入了ftp_close,fclose这类函数,或者调用了一个不可控参数的方法,所以都不去看,只剩下了一个/tp3.2.3/ThinkPHP/Library/Think/Image/Driver/Imagick.class.php,里面调用了destory

但是找不到这个destory的来源,只能全局搜索

调用destroy方法的时候没有传参,所以这个delete语句注入不了,查看其他方法
在/tp3.2.3/ThinkPHP/Library/Think/Session/Driver/Memcache.class.php调用了delete方法

同样是找不到来源,继续全局搜索delete,存在8个结果

这里排除掉2个抽象函数,剩下的6个类文件,其中4个是继承了Driver类和Model类,Driver类是一个抽象类,所以也跳过,先看Model类

1.变量pk是预定义的列名,这里可以操控。
2.由于调用destory方法的时候没有参数,变量options是字符串类型。
3.这里假设payload里没有逗号,这样就直接创建了where的sql条件数组(列名=>列值),列名和列值都可控。
4._parseOptions上面sql注入提到过,会对比列的类型,由于列名和列值可控,先不考虑。
5.最后调用了delete方法,这个delete方法执行了sql语句,最后返回了结果,delete后面调试定位。
动态调试一下,poc:

控制器:


跟进去看运行时的变量值,在每个类调用处打上断点:


前面推测的没有问题,重点看下面delete的方法做了什么

抛出异常了,因为db未定义,想办法构造出一个db变量,db变量修改的位置

继续找_db变量修改的位置

这里实例化了数据库连接

config变量如下:

配置传入了一个动态类

所以db变量实际上就是创建的Mysql对象,Mysql类继承了Driver类,所以delete方法是来自Driver类,把这个Mysql类加入poc。
成功执行


现在想办法sql注入,刚才的流程里产生的参数会经过addslashes转换,所以不方便注入。回到Model的delete方法,看到开头实际上有一个分支可以令options是一个数组

我们令pk变量的值和data数组的键名相同,就可以通过data传入payload,根据上面find函数的sql注入我们知道,只要传入的数组满足以下关系,就可以绕过大量检查
所以修改一下我们的poc,第一次传入delete方法的options直接为空,同时Model类里的options也为空,直接进入条件分支
重新运行一下

再次运行到这里执行删除操作的时候,我们已经可以控制表名和条件

ctfshow 575题目试一下
题目部分源码
这里可以用show方法的代码执行打,但是这里直接用反序列化打,poc如下,在当前目录写入chanra.txt
把payload复制到cookie,运行:
写入成功

尝试写shell

访问成功

4.comment注入
控制器代码如下:

这个方法没有过滤,可以直接执行payload。简单追踪下

在comment打上断点,跟进去

直接赋值给了options,返回this,之后继续执行find方法,find方法之前看到过,后面会构造一个sql语句,跟进去看看:

用options中的comment值替换原本的%comment%字段,跟进去看看有没有过滤

没有过滤,直接拼接了
实战ctfshow web576
题目源码
发送payload

访问webshell

5.使用where查询(传入数组)
之前传入where字符串时,没有经过过滤就被数据库执行了,这次看下传入数组会怎样,先看看where方法做了什么
源码:
打上断点运行

直接在options的where键创建了一个数组,在find方法里我们讲到过,如果where的值是数组,那么会有addslashes转义的,进行跟进看看怎么绕过。中间过程和find方法重复,就略过了,直接到构造sql语句的方法来看

跟进

这里面的条件分支我们都不可控,只能再去其他函数看

parseKey是用反引号包裹列名的,没想出怎么利用,略过

正常情况下参数在这里是字符串,直接addslashes转义后就返回了,但是这里条件分支可以控制,控制了exp变量,可以做的事情就多了

这里令id的值是一个数组

成功进入这个分支

又有一个分支


最后还是逃不出addslashes的命运,当时我想着这不是没漏洞了,后来看了别人文章才反应过来,没有引号的paylaod不就可以执行了。。。。

明显这个分支更容易满足,构造一个二维数组传进去

传进去之后就绕过了单引号包裹,且payload里没有引号

所以解析后没有引号包裹

成功爆出数据

上题目 ctfshow web577
当成数字型注入

其他漏洞
6.php原生引擎下,assign方法变量覆盖导致的RCE
默认情况下tp框架使用的是THINK引擎渲染网页,但是开发者可以手动设置/tp3.2.3/ThinkPHP/Conf/convention.php的TMPL_ENGINE_TYPE值为PHP,来使用原生引擎。

漏洞代码:

传入assign的如果是数组,则会创建一个数组array($name[key]=>$name[value])。
传入assign的如果是字符串,则会创建一个数组array($name=>$from)。

漏洞代码

display方法是渲染用的,没有参数就渲染自身名称的html文件模板,我这里已经创建了/tp3.2.3/Application/Admin/View/Login/index.html文件,内容是123。

我们跟进几个方法后就会看到渲染位置


关于extract函数的说明


把数组里的键值对提取成变量,flags为EXTR_OVERWRITE时,会覆盖已有变量。
修改url

再跟踪到这里,看到已经覆盖了变量


poc有两种写法
上题目 ctfshow 578
篇幅过长,超出上限,发不出图片了,大家自己试下。。。。