前言
首发:
在刚结束的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如下:
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,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_functions和之前比赛的时,发现这里ban的iconv确实有点莫名其妙,于是就找到了这篇文章,但是这篇文章的姿势也是需要iconv的。那这里是否存在绕过呢?
首先来动态调一下php iconv的流程,理解文章中姿势的原理。php的iconv本质上还是调用了glibc中的iconv,因此需要gdb调试一下glibc。这里直接装了带符号的glibc,然后LD_LIBRARY_PATH设置成带符号libc.so的地址,就可以开始调试了。
1 | gdb php |
从php对iconv的调用开始看。为了防止和libc命名冲突,iconv.c用PHP_NAMED_FUNCTION将iconv注册为php_if_iconv,在iconv_functions中也能看到PHP_RAW_NAMED_FE创建的iconv到php_if_iconv的映射。
1 | 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
1 | module PAYLOAD// INTERNAL ../../../../../../../../tmp/payload 2 |
payload.c
1 | #include <stdio.h> |
1.php
1 | <?php |
然后gcc payload.c -o payload.so -shared -fPIC
,再php 1.php即可。
PS:
推荐下AntSword的ant.so,LD_PRELOAD设置一下劫持system后,执行命令方便很多。
线上非预期(from CNSS)
在官方wp中写了:
1 | https://github.com/mm0r1/exploits/blob/master/php7-backtrace-bypass/exploit.php |
除此之外,我们还可以看到在线下赛题目的disable_functions和disable_classes中,多了很多很多Exception和Error类…这些类都是可以触发这个uaf的。比赛时还觉得出题人会不会ban漏一个,结果发现出题人直接从get_declared_classes()中选出来ban了(
探索过的失败的非预期
LD_PRELOAD
先让我们回忆下通过LD_PRELOAD实现bypass的方式。
LD_PRELOAD设置一个在程序运行前优先加载的动态链接库,利用attribute((attribute-list)),可以通用的劫持php中新启动进程的函数,比如mail系列、Imagick等。但题目环境中这些都被ban了,imap模块也没开,因此都不能用。那么有没有其他的还没被发现的启动新进程的函数呢?
线上时我们通过get_defined_function拿到所有环境中的变量,然后参考安全客这篇文章的fuzz方式,对这些函数进行了fuzz,结果并没有找到…
写/proc/self/mem
参考之前对宝塔rsap绕过的文章,直接往/proc/self/mem写shellcode劫持got表,看起来也是可以的。
Kirin爷爷测试用php-cli也确实是可以覆盖的,exp如下:
1 | <?php |
然而测试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:
- 读/proc/self/maps找到php和libc在内存中的基址
- 解析/proc/self/exe找到php文件中readfile@got的偏移
- 找个能写的地址写shellcode
- 向readfile@got写shellcode地址覆盖
- 调用readfile
PHP Bugs
之前的很多bypass是uaf等pwn下来的,就让Kirin爷爷帮忙调了几个7.2.24的uaf,发现都不行,有的只能leak,期待Sndav的姿势学习一下orz
总结
web狗有空还是得看点bin…
参考链接
1 | https://bytectf.feishu.cn/docs/doccnqzpGCWH1hkDf5ljGdjOJYg?login_redirect_times=1#l1Qx86 |