Thinkphp3.2.3及以下版本漏洞整理
欢迎搜索公众号:白帽子左一
每天分享更多黑客技能,工具及体系化视频教程
概述
网站为了提高访问效率往往会将用户访问过的页面存入缓存来减少开销。
而Thinkphp 在使用缓存的时候是将数据序列化,然后存进一个 php 文件中,这使得命令执行等行为成为可能。
就是缓存函数设计不严格,导致攻击者可以插入恶意代码,直接getshell。
redhat6+apache2+Mysql+php5+thinkphp3.2.3
将 application/index/controller/Index.php 文件中代码更改如下:
访问 http://localhost/tpdemo/public/?username=xxx%0d%0aphpinfo();//, 即可将 webshell 等写入缓存文件。
先找到Cache.class.php文件,也就是缓存文件,关键代码:
这里读入配置,获取实例化的一个类的路径,路径是:Think\\Cache\\Driver\\
这里我尝试了var_dump($class)和echo $class直接浏览器访问Cache.class.php都无法像那篇帖子一样打印出$class,后来才发现添加数据写入缓存页面跳转才打印了Think\\Cache\\Driver\\File。关键代码:
这里实例化了那个类,我们重点关注set方法,接着直接找到这个路径下的File.class.php吧。
关键代码:
这就是写入缓存的set方法,对传入的数据进行了序列化和压缩,重点看这两句:
简单拼接一下就写入文件了,Bug就出现在这里,这时来看看我们payload:
解码后就是:
就写入恶意代码了。最后看看文件名:
文件名就是md5加密值。
- 总结:这个thinkphp缓存函数设计bug,利用起来不难,但是感觉还是挺鸡肋。
- 原因是:1.要开启缓存2.虽然文件名是md5固定值,但是TP3可以设置 DATA_CACHE_KEY 参数来避免被猜到缓存文件名3.缓存使用文件方式4.缓存目录暴露在web目录下面可被攻击者访问。
影响版本:Thinkphp 2.x、3.0-3.1
这段代码主要就是用explode把url拆开,然后再用implode函数拼接起来,接着带入到preg_replace里面。
preg_replace的/e模式,和php双引号都能导致代码执行的。
这句正则简化后就是 /(\\w+)\\/([^\\/\\/]+)/e注:\\w+ 表示匹配任意长的[字母数字下划线]字符串,然后匹配 / 符号,再匹配除了/符号以外的字符。其实就是匹配连续的两个参数。
eg:www.dawn.com/index.php?s=1/2/3/4/5/6每次匹配 1和2,3和4,5和6。、
\\1是取第一个括号里的匹配结果,\\2是取第二个括号里的匹配结果
也就是\\1 取的是 1 3 5,\\2 取的是 2 4 6。
那么就是连续的两个参数,一个被当成键名,一个被当成键值,传进了var数组里面。
而双引号是存在在 \\2 外面的,那么就说明我们要控制的是偶数位的参数。环境较为难找,本地模拟一下
本地模拟:
thinkphp是国内著名的php开发框架,有完善的开发文档,基于MVC架构,
其中Thinkphp3.2.3是目前使用最广泛的thinkphp版本,虽然已经停止新功能的开发,但是普及度高于新出的thinkphp5系列
由于框架实现安全数据库过程中在update更新数据的过程中存在SQL语句的拼接,并且当传入数组未过滤时导致出现了SQL注入。
thinkphp系列框架过滤表达式注入多半采用I函数去调用think_filter
一般按照官方的写法,thinkphp提供了数据库链式操作,其中包含连贯操作和curd操作。
在进行数据库CURD操作去更新数据的时候,利用update数据操作。
where来制定主键的数值,save方法去更新变量传进来的参数到数据库的指定位置。
通过where方法获取where()链式中进来的参数值,并对参数进行检查,是否为字符串,tp框架默认是对字符串进行过滤的。
再来到save方法,通过前面的数据处理解析服务端数据库中的数据字段信息,字段数据类型,再到_parseOptions 表达式分析,获取到表名,数据表别名,记录操作的模型名称,再去调用回调函数进入update。
我们这里可以直接看框架的where子函数,之前网上公开的exp表达式注入就是从这里分析出来的结论:Thinkphp/Library/Think/Db/Driver.class.php
其中除了exp能利用外还有一处bind,而bind可以完美避开了think_filter:
这里由于拼接了$val参数的形式造成了注入,但是这里的bind表达式会引入:符号参数绑定的形式去拼接数据,通过白盒对几处CURD操作函数进行分析定位到update函数,insert函数会造成sql注入,于是回到上面的update函数。
Thinkphp/Library/Think/Db/Driver.class.php
可以继续跟进execute函数。
这里有处对$this->queryStr进行字符替换的操作,具体是怎么更新的,我们可以进一步操作一下。
Application/Home/Controller/UserController.class.php
通过这个代码,根据进来的id更新用户的名字和钱,构造一个简单一个poc:id[]=bind&id[]=1’&money[]=1123&user=liao 当走到execute函数时sql语句为
然后$that = $this。
接下来下面的替换操作是将”:0”替换为外部传进来的字符串,这里就可控了。替换后的语句就会发现之前的user参数为:0
然后被替换为了我们传进来的东西(liao),这样就把:替换掉了。
替换后的语句:
但是id后面的 :1 是替换不掉的。
那么我们将id[1]数组的参数变为0尝试一下。
测试代码
Poc:id[]=bind&id[]=0%27&money[]=1123&user=liao
这样就造成了注入。继续构造poc。money[]=1123&user=liao&id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))
select 和 find 函数
以find函数为例进行分析(select代码类似),该函数可接受一个$options参数,作为查询数据的条件。
当$options为数字或者字符串类型的时候,直接指定当前查询表的主键作为查询字段:
同时提供了对复合主键的查询,看到判断:
要进入复合主键查询代码,需要满足$options为数组同时$pk主键也要为数组,但这个对于表只设置一个主键的时候不成立。
那么就可以使$options为数组,同时找到一个表只有一个主键,就可以绕过两次判断,直接进入_parseOptions 进行解析。
之后跟进_parseOptions方法,(分析见代码注释)
$options我们可控,那么就可以控制为数组类型,传入$options[‘table’]或$options[‘alias’]等等,只要提层不进行过滤都是可行的。
同时我们可以不设置$options[‘where’]或者设置$options[‘where’]的值为字符串,可绕过字段类型的验证。
可以看到在整个对$options的解析中没有过滤,直接返回,跟进到底层ThinkPHP\\Libray\\Think\\Db\\Diver.class.php,找到select方法,继续跟进最后来到parseSql方法,对$options的值进行替换,解析。
因为$options[‘table’]或$options[‘alias’]都是由parseTable函数进行解析,跟进:
当我们传入的值不为数组,直接进行解析返回带进查询,没有任何过滤。
同时$options[‘where’]也一样,看到parseWhere函数
delete 函数有些不同,主要是在解析完$options之后,还对$options[‘where’]判断了一下是否为空,需要我们传一下值,使之不为空,从而继续执行删除操作。
下载地址:http://www.thinkphp.cn/download/610.html
自己配置一下数据库路径:
test_thinkphp_3.2.3\\Application\\Common\\Conf\\config.php
自己安装,安装完以后访问一下:
http://127.0.0.1/thinkphp_3.2.3/index.php/Home/Index/index没有报错就可以了。
开启debug 方便本地测试
路径:test_thinkphp_3.2.3\\index.php
路径:test_thinkphp_3.2.3\\Application\\Common\\Conf\\config.php
3处注入利用方法都是一样的,所以就演示一个 find 注入Select 与 delete 注入同理
明显转成整型,无法进行注入了。
这样就可以直接控制where了。
ThinkPHP在处理order by排序时,当排序参数可控且为关联数组(key-value)时,
由于框架未对数组中key值作安全过滤处理,攻击者可利用key构造SQL语句进行注入,该漏洞影响ThinkPHP 3.2.3、5.1.22及以下版本。
ThinkPHP3.2.3漏洞代码(/Library/Think/Db/Driver.class.php):
从上面漏洞代码可以看出,当$field参数为关联数组(key-value)时,key值拼接到返回值中,SQL语句最终绕过了框架安全过滤得以执行。
测试代码
构造poc:?order[updatexml(1,concat(0x3e,user()),1)]=1
EXP表达式支持SQL语法查询 sql注入非常容易产生。
可以改成:
exp查询的条件不会被当成字符串,所以后面的查询条件可以使用任何SQL支持的语法,包括使用函数和字段名称。
查询表达式不仅可用于查询条件,也可以用于数据更新。
表达式查询
- $map[\’字段1\’] = array(\’表达式\’,\’查询条件1\’);$map[\’字段2\’] = array(\’表达式\’,\’查询条件2\’);$Model->where($map)->select();
跟到\\ThinkPHP\\Library\\Think\\Db\\Driver.class.php 504行
parseSQl组装 替换表达式:
parseKey()
filter_exp
I函数中重点代码:
那么可以看到这里是没有任何有效的过滤的 即时是filter_exp,如果写的是
filter_exp在I函数的fiter之前,所以如果开发者这样写I(‘get.id’, ‘’, ‘trim’),那么会直接清除掉exp后面的空格,导致过滤无效。
返回:
直接在IndexController.class.php中创建一个测试代码
数据库配置:
Poc:?id[0]=exp&id[1]==updatexml(0,concat(0x0e,user(),0x0e),0)
首先全局搜索 destruct, 这里的 $this->img 可控,可以利用其来调用 其他类的destroy() 方法,或者可以用的call() 方法,__call() 方法并没有可以利用的
那就去找 destroy() 方法。
注意这里,destroy() 是有参数的,而我们调用的时候没有传参,这在php5中是可以的,只发出警告,但还是会执行。
但是在php7 里面就会报出错误,不会执行。
所以漏洞需要用php5的环境。
继续寻找可利用的delete()方法。
在Think\\Model类即其继承类里,可以找到这个方法,还有数据库驱动类中也有这个方法的,thinkphp3的数据库模型类的最终是会调用到数据库驱动类中的。
先看Model类中。
还需要注意这里!!如果没有 $options[‘where’] 会直接return掉。
跟进 getPK() 方法
$pk 可控 $this->data 可控 。
最终去驱动类的入口在这里
下面是驱动类的delete方法
我们在一开始调用Model类的delete方法的时候,传入的参数是
而后面我们执行的时候是依靠数组的,数组是不可以用 字符串连接符的。参数控制不可以利用$this->sessionName。
但是可以令其为空(本来就是空),会进入Model 类中的delete方法中的第一个if分支,然后再次调用delete方法,把 $this->data[$pk] 作为参数传入,这是我们可以控制的!
看代码也不难发现注入点是在 $table 这里,也就是 $options[‘table’],也就是 $this->data[$this->pk[‘table’]];
直接跟进 driver类中的execute() 方法
跟进 initConnect() 方法
跟进connect() 方法
数据库的连接时通过 PDO 来实现的,可以堆叠注入(PDO::MYSQL_ATTR_MULTI_STATEMENTS => true ) 需要指定这个配置。
这里控制 $this->config 来连接数据库。
driver类时抽象类,我们需要用mysql类来实例化。
到这里一条反序列化触发sql注入的链子就做好了。
poc
这里可以连接任意服务器,所以还有一种利用方式,就是MySQL恶意服务端读取客户端文件漏洞。
利用方式就是我们需要开启一个恶意的mysql服务,然后让客户端去访问的时候,我们的恶意mysql服务就会读出客户端的可读文件。
这里的hostname 是开启的恶意mysql服务的地址以及3307端口
下面搭建恶意mysql服务
修改port 和filelist
执行python脚本后,发包,触发反序列化后,就会去连接恶意服务器,然后把客户端下的文件带出来。
下面就是mysql.log 中的 文件信息(flag.txt)
当脚本处于运行中的时候,我们只可以读取第一次脚本运行时定义的文件,
因为mysql服务已经打开了,我们需要关闭mysql服务,然后才可以修改脚本中的其他文件。
然后依次 kill 就好。
PHP 中最常用的 100 个函数
PHP 静态分析引擎 Exakat 分析了 1900 个 PHP 开源项目,整理了最常用的 100 个 PHP 函数:
从这最常用的 100 个 PHP 函数,总结一下:
- 这 100 个函数近期都没有被废弃的计划,所以可以放心使用,并加强学习。
- 最常用的是字符串函数,然后是数组函数和文件函数,有相当多的调用是为了知道值的类型。
- md5 是最常用的加密函数,其次是 Sha1 (#147),print_r 出现在 1/3 的项目的代码中。
- 由于 dirname(dirname(dirname())) 的调用方式,dirname 的排名变得异常的高。
- 在非内置库中,mbstring 排名第一、curl 第二,然后是 gd、filter 和 iconv。
- 数组中排序中使用键比使用值更频繁。
- 读取文件的函数比写入文件的函数应用的多,另外通常使用 file_get_contents 读取文件,使用 fwrite 写入文件。
- array, echo, print, empty, isset 和其他语言结构,因为不能算作 PHP 函数,所以没有纳入此排名,但是它们的使用度肯定是非常高的。
- array_push, is_object, func_get_arg, chr, call_user_func 这些函数应该用运算符替代 。
- 数据库函数没有在这里排名,因为经常使用的是类,但数据库的功能是使用度很高的。
- 最后许多函数在新版中有了新的功能,比如 count() 和 dirname() 有了第二个参数,以及 preg_match() 和 str_replace() 接受数组作为参数等。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。