0%

从ByteCTF到bypass_disable_function

从ByteCTF到bypass_disable_function

起初只是简单的看了一下去年ByteCTF中那个bypass_disable_fuction的题,然后发现原来PHP有这么多bypass的方案我不知道,以及一些零碎的其他知识点,统一学习了

然后马上蓝帽杯遇到一个bypass发现完全不会做,进行大型update

putenv

在环境变量可控时执行的两大操作,需要disable_function里面给用putenv

LD_PRELOAD

这个是最老的操作之一了,我掌握的也就这一种,通过LD_PRELOAD这个变量可以指定动态链接库(.so文件)在libc之前被加载,编写一个恶意的动态链接库,使用GNU的特殊语法,在调用main函数之前对构造属性进行执行,由于是调的C库这边的函数,自然不受disable_function的影响。只要PHP启动新进程时,恶意动态链接库被加载,且其构造方法直接在一切之前运行,构造方法中写一个命令执行即可。
简单的说就是能劫持PHP启动的新进程

mail

PHP使用mail函数的时候会用execve来启动sendmail,启动进程时成功命令执行

imap_open

也是一个启动进程的函数

Imagick

这个库在远古版本是能直接命令执行的,能直接绕过disable function,不过那个也太远古了。但是这个库也能启动新的进程,而非常有意思的一点是,LD_PRELOAD那篇文章的作者却提到他没能在imagick中启动新的进程,不过0CTF2019中的这道Wallbreaker_Easy考的就是这个点(这个题用的FPM模式,估计用FPM也能打通),Imagick再进行图片类型转换时,需要启动外部程序进行转换,这样就会启动新的进程,实现LD_PRELOAD的命令执行,这里放一个替换mail的payload

$a=new Imagick();
$a->readImage('123.png');
$a->writeImage('123.wdp');

可以用之前的bypass脚本打通

iconv

这个是ByteCTF的考点,基本原理也是最后的底层实现调用的是C的动态链接库。
PHP在使用iconv()时最后一路调用,进入libc函数iconv_open(),再一波操作调用到.so文件的方法,实现RCE
同样,系统不会无缘无故的调用我们自己上传的路径下的.so文件,其支持使用GCONV_PATH的自定义编码转换模块,所以可以putenv将该变量设置为目标路径,同时上传两个文件以建立该编码模块并实现利用
glibc iconv文档 讲解了gconv-modules的基本语法

这里默认后缀是.so所以不需要指定.so,默认认为动态链接库和gconv-modules在同一目录下,但是也允许相对目录,编码名后面加的//是glibc实现的问题,文档里说你听他的就行了

gconv-modules

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

payload.c,使用gcc payload.c -o payload.so -shared -fPIC编译为payload.so

#define _GNU_SOURCE

#include 
#include 
#include 

extern char** environ;

void gconv() {}

void gconv_init() {
    const char* cmdline = getenv("EVIL_CMDLINE");
    int i;
    for (i = 0; environ[i]; ++i) {
            if (strstr(environ[i], "LD_PRELOAD")) {
                    environ[i][0] = '\0';
            }
    }

    system(cmdline);
}

此时在iconv中遇到payload编码时,即会调用我们这个恶意的动态链接库完成命令执行

除了iconv()这个函数,所有能最终调用到libc的iconv_open()操作均能触发RCE,比如iconv_strlen,或者php://filterconvert.iconv过滤器等
这道题的预期解应该就是用php://filter
lfy神仙还提到了可以再套一层LD_PRELOAD劫持system这个函数启动的进程方便执行命令,但是我感觉只要自己把payload里面写好一点靠环境变量作为参数执行命令应该也很方便吧?这里直接抄那个LD_PRELOAD的payload靠环境变量命令执行也很不错

写/proc/self/mem

好像以前见到过类似的题目,偏二进制
PHP主进程是root的,但是子进程是www-data的,/proc/self/mem属于www-data且权限是600,但/proc/self目录是root的,并且www-data无权限,所以正常情况下不能写入。
但是对于Nginx+php,且为低版本的php-fpm(PHP<5.6),/proc/self/属于www-data,可以通过写入GOT表的RCE

二进制基础

二进制文件执行函数的时候要查两个表,一个PLT一个GOT。PLT是在编译时就确定下来了的,加载进内存的时候位于代码段。但由于动态链接之类的存在,在编译的时候并不能确定所有函数的地址,因为它甚至都还没加载进来,所以PLT表项并不存放函数的地址,而是指向GOT表的对应项,再由GOT表指向函数的真实地址。GOT表就是在运行时当调用一个函数的时候临时去查询的,因为PLT处于代码段不可修改,所以查到之后回填进GOT表,下次调用该函数就可以直接查询GOT表获取到地址。期间还有各种复杂的操作,比如什么GOT表前几项是就是用来进行函数地址查询的函数之类的,还有一种程序运行时不是等需要用到再去填GOT表,而是直接全部加载完然后把GOT表也变得只读防止被修改之类的

利用

从上面已经可以看出来函数调用时其所用的是GOT表指向的地址,那么只要修改GOT表中某个函数,在执行那个函数的时候变成执行system之类的就搞定了(修改GOT表应该是pwn那边的常见操作才是。。。。)

抄一个攻击流程

写一下劫持GOT表的步骤,这里直接写shellcode:
1.读/proc/self/maps找到php和libc在内存中的基址
2.解析/proc/self/exe找到php文件中readfile@got的偏移
3.找个能写的地址写shellcode
4.向readfile@got写shellcode地址覆盖
5.调用readfile

利用脚本

攻击PHP-FPM

先了解一下前置知识

PHP运行类型

PHP运行一般来说几个类型,CLI,php-cgi,php-fpm,Apache2.0handler
CLI是Command Line Interface,命令行情况下使用,不怎么常见
CGI是Common Gateway Interface,webserver和其他软件通信的协议(感觉是配合Nginx做反带之类的时候使用的),有一个强化版本fast-cgi。php的cgi就是来一个请求PHP起一个解释器进程处理,处理完了关掉,就很慢很憨批,所以fast-cgi就是开局直接启动多个解释器进程常驻内存,等待接收请求,处理完了也不关,继续等下一次的请求
php-cgi是早期的cgi管理器,因为cgi不太行所以这个也不太行
php-fpm是fast-cgi的管理器,一个master进程和一堆worker进程,master接收请求分配给worker,能动态调度启动worker进程,性能upup

使用Apache搭建PHP的时候使用的是Apache2handler,这个时候Apache是把PHP作为一个module加载进来的,属于是Apache自己内嵌PHP的解释器,就少了一步进程间通信,Apache直接自己开PHP解释器进行处理

协议字段

在通信时有几个比较有意思的字段是可以指定的
SCRIPT_FILENAME,指定PHP执行的脚本文件路径,不过php5.3.9之后加入了fpm增加了security.limit_extensions,只允许执行如下后缀的文件.php .php3 .php4 .php5 .php7
PHP_VALUE,可以覆盖一些php.ini里面定义的属性,只能用于PHP_INI_ALLPHP_INI_PERDIR类型的指令,具体看这个php.ini 配置选项列表,好用的比如auto_prepend_file,open_basedir
PHP_ADMIN_VALUE,和上面这个差不多,区别在于这个字段设置的属性不能在用户层面上被修改,也就是不能被ini_set()之类的函数在应用里被重写,也不能被.htaccess这种配置文件覆盖,常用的有allow_url_include,启用后支持include url形式的文件,经典php://input伪协议打通,以及extension_dir,指定扩展的.so直接bypass disable_function打通,safe_mode,在PHP5.4之后就被删除了的东西,启用后会限制某些函数的使用,比如move_upload_file,copy这些能把远程文件下到本地的函数,和一些system,shell_exec这类直接的命令执行函数,也不允许进行dl函数加载扩展文件(.dll or .so)

(找不到资料,但是感觉也许可以把低安全等级的变量也设置成PHP_ADMIN_VALUE不让动态的修改,不过PHP_INI_SYSTEM这个类型的属性理论上只允许在php.ini里面设置,这里能改真是玄幻)
与之对位的还有PHP(_ADMIN)_FLAG这么个属性,其区别在于其值只能为布尔值

可惜的是这些属性并不能覆盖disable_function

PHP配置值通过 php_value 或者 php_flag 设置,并且会覆盖以前的值。请注意 disable_functions 或者 disable_classes 在 php.ini 之中定义的值不会被覆盖掉,但是会将新的设置附加在原有值的后面。
使用 php_admin_value 或者 php_admin_flag 定义的值,不能被 PHP 代码中的 ini_set() 覆盖。

修改PHP_(ADMIN_)VALUE会直接使得当前处理请求的fastCGI进程收到影响,如果多次请求就可能污染掉所有的进程,在重启fpm之前可能所有进程都会受到影响

利用

因为之前说到使用CGI的时候,是webserver和CGI直接进行通信的,所以就存在进程间通信的问题,在Nginx的设置中有一个fastcgi_pass的设置,指定fastcgi所在的ip端口(或Unix socket),PHP的fpm设置中可以配置fastcgi的监听位置,可以通过配置使得PHP也能前后端分离
fpm并不验证数据的来源,只要是发送到监听端口的数据就一律接受
PHP在fpm配置中若将监听端口写成0.0.0.0:9000,则接受来自任意ip的通信,可以通过伪装成Nginx服务器与php-fpm通信来执行命令,如果配置的是127.0.0.1:9000这种情况的可能就要靠SSRF打了
P神的利用脚本

SSRF也分两种,如果是监听的127.0.0.1这种ip地址就可以用gopher发TCP包打,如果直接监听Unix socket的话就得专门起一个socket去连接了

利用方案:1.自己随便上传一个其他位置的马然后用script_name指定进行利用
2.随便找一个PHP文件然后设置auto_prepend_file为php://input,allow_url_include为On打通

bypass_disable_function

直接抄着那个脚本打并不能绕过Disable_function,只是做到了命令执行。因为这样子发过去的请求还是由原来的PHP解释器进行解析

但是可以通过FastCGI协议去让php-fpm加载我们自定义的扩展(.so文件),而这个扩展肯定是不受disable_function限制的,做到任意命令执行
即之前提到的extension_dirextension

蚁剑有一个玄幻的插件,直接生成一个调用system函数的.so文件,然后输这么个命令php -n -S 127.0.0.1:port -t /var/www/html在目标机器上新开一个web服务,当然权限是www-data的,但是添加了-n参数指定不使用php.ini,可以绕过其设置的disable_function,并且还上传了一个antproxy.php文件将请求转发过去,实现比较方便的命令执行(不然应该是执行一次传一个.so吧,要我说继续搞那种接受环境变量做命令的方法也挺好的)
这次这个蓝帽杯本身是一个打.so扩展的pwn题,但是我看的wp成功的拿fpm打了一个.so加载的非预期打通了

FFI

Foreign Function Interface,外部函数接口
PHP7.4的新特性,可以直接调用外部代码,RCTF 2019的Nextphp出的就是这个
需要ffi.enable=true才能任意使用,否则只能使用在对应文件中使用

参考链接

ByteCTF WP-无需mail bypass disable_functions
PHP 突破 disable_functions 常用姿势以及使用 Fuzz 挖掘含内部系统调用的函数
php之CGI、FastCGI、APACHE2HANDLER、CLI运行模式的详解
攻击PHP-FPM 实现Bypass Disable Functions
RASP攻防 —— RASP安全应用与局限性浅析
bypass disable_function多种方法+实例
浅析php-fpm的攻击方式
从一道CTF学习Fastcgi绕过姿势
PHP 连接方式介绍以及如何攻击 PHP-FPM
Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写
PHP绕过open_basedir列目录的研究