It's important to keep in mind the challenge is using an old version of PHP.
Matching the first part of the condition is really easy, we just have to send b[]=admin&b[]=oloco or b[0]=admin&b[1]=oloco in a POST request.
But the problem is the second part of the condition $__ni[0] != 'admin' because there is a strict equality $_POST['b'] === ['admin','oloco'] and a loose comparison $_POST['b'][0] != admin.
This sounds pretty impossible to bypass even if the second part of the if statement uses a loose comparison.
In PHP === between arrays means same values of same types and in the same order.
The problem comes from an integer overflow, the array index is an integer that needs to fit into a 32bits variable so any integer larger than 2^32 - 1 will be truncated. 2^32 = 4294967296 will be truncated to 0 so.
Then we can provide the following valid payload b[4294967296]=admin&b[1]=oloco into a POST request.
We still need to match a bunch of other conditions to get the author shell:
There is a foreach loop reviewing all GET arguments we provided.
First and easy one, we need to provide a x parameter ($__dgi = $_GET['x'];) with an argument longer than 17 chars (strlen($__dgi)>17). Example of payload: x=azertyuiopqsdfghjklmwxcvbn.
$_p == 3 indicates that the payload will be included in the 3rd parameter.
The second is useless but we can use it to match $_k_o == $k_Jk and so have a full liberty over the 3rd argument.
As $k_Jk is used like that $k_Jk($_v,$_k_o); we can deduce that the variable is stocking an anonymous function, we don't know which one because it is declared in oshit.php.
So to bypass if($_k_o == $k_Jk) we can use a type juggling. Example of payload: 0=dontcare.
Then we can focus on the 3rd param to get an RCE.
In $k_Jk($_v,$_k_o), $_v will be the system command to be executed and $_k_o the PHP function that will lead to command execution.
So to sum up all payload we have:
Resulting into:
Now that we have a way to execute some system commands we can try a few things.
With id we have uid=33(www-data) gid=33(www-data) groups=33(www-data), so we can confirm that we are using the apache account that is in the apache group.
We quickly checked env to see if there is interesting stuff but there isn't, we only know we must be jailed in a restricted shell:
A problem is that we can't add options to our commands because we can't inject spaces. But Aikari found a solution for that $IFS = so let's go!
Let's try a recursive file listing ls$IFS-lR$IFS/app=passthru:
We were not able to use cat but head was working.
We checked if the flag was in oshit.phphead$IFS-100$IFS/app/another/oshit*:
Just to be curious, let's try head$IFS-100$IFS/app/admin/index*:
By sending &ls$IFS-lA we can see there is a .dockerenv but it's useless.
We tried head$IFS-100$IFS/create_mysql_admin_user* too without success.
Let's do recursive file listing again ls$IFS-lR$IFS/var so we can see there is a /var/flag file.
And finally &head$IFS-10$IFS/var/flag gave us ASIS{f52c5a0cf980887bdac6ccaebac0e8428bfb8b83}.
Our goal is to buy the asis flag but we have no money.
First I thought we had to find the Flask secret_key to be able to forge a valid signed cookie like this {"coupons":[],"credit":9999} that would have been accepted by the server. But the LFI didn't allow us to find anything else and no SSTI seemed available.
But look, the credit check is done like this if credit < 0: and the json data is parsed with request.get_json().
We can try to compare credit with something else than a number like NaN. But NaN is not part of the JSON specs.
I think that the Flask method request.get_json() is calling the python method json.dumps.
However json.dumps has an allow_nan parameter, which defaults to True so we can use NaN.
I think that when the NaN value is passed to python it is converted to None. Let's see python2 behavior: