剑客
关注科技互联网

Redis Lua远程代码执行EXP

Author:boywhp(

前篇:LUA虚拟机逃逸 http://drops.wooyun.org/tips/12677

一、概述

Redis服务器支持LUA脚本,通过如下命令行在服务器执行一段lua脚本:

redis-cli –eval exp.lua -h host_ip

Redis服务端实现了LUA沙盒机制,屏蔽了LUA的OS、文件操作等部分危险函数调用,但是未过滤 loadstring 函数,参考Lua虚拟机逃逸文档,即可实现Redis服务器内存读写操作,进而达成代码执行目的,本文操作测试环境Ubuntu 14.04 x64 + Redis2.8.20

二、OP_CALL代码执行

这是最容易想到的一种代码执行方案,因为在可以读写任意内存地址前提下,我们可以精确控制 CClosure 对象的f指针,使其执行linux系统的 system 函数。然后将其L对象内存填充为命令字符串。

EXP流程如下:

序号 描述
1 coroutine.create 调用luaB_cocreate创建一个lua_State(lua线程)
2 在新创建的lua_State对象,写入待执行的命令字符串
3 coroutine.resume继续执行
4 在新线程中coroutine.wrap(function() end)创建一个 CClosure 对象
5 CClosure 对象偏移32字节的f对象,指向 system 函数调用
6 执行 CClosure 函数调用
7 最终 n = (*curr_func(L)->c.f)(L); 完成代码执行

(一)、Linux ELF内存解析

利用的关键是成功获取system函数地址,通过动态解析Linux Elf内存地址可以获取任意LIBC函数地址。

1 、获取 Redis 进程基地址

通过扫描内存镜像的ELF文件Basic的MAGIC标识 7f 45 4c 46即可获取Redis的内存基地址,或者直接使用基地址0x400000,默认编译器生成的Redis似乎就是这个。

内存搜索的起点可以通过读CClosure对象偏移32字节的f指针,然后按照内存页对齐,依次向下搜索。

2 、获取 LIBC 基地址

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

in.  Their initial contents will arrange when called to push an

offset into the .rel.plt section, push _GLOBAL_OFFSET_TABLE_[1],

and then jump to _GLOBAL_OFFSET_TABLE[2].  */

got[1] = (Elf64_Addr) l; /* Identify this shared object.  */

/* The got[2] entry contains the address of a function which gets

called to get the address of a so far unresolved function and

jump to it.  The profiling extension of the dynamic linker allows

to intercept the calls to collect information.  In this case we

don’t store the address in the GOT so that all future calls also

end in this function.  */

got[2] = (Elf64_Addr) &_dl_runtime_resolve;

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

ElfW(Addr) l_addr ;                /* Base address shared object is loaded at.  */

char * l_name ;         /* Absolute file name object was found in.  */

ElfW(Dyn) * l_ld ;           /* Dynamic section of the shared object.  */

struct link_map *l_next, *l_prev ; /* Chain of loaded objects.  */

};

通过遍历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模块基地址

3 、获取 system 函数地址

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

(二)、注意事项

使用OP_CALL方式执行利用时需注意,利用前要保存好新线程lua_State的内存信息,并且写入命令长度不宜过长,否则会破坏lua_State对象,导致利用时直接内存异常,实测Ubuntu Redis x64 2.8.20一次最长执行10字节命令,0字节限制是此种利用方式最大的硬伤。

三、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寄存器传递,并不会影响堆栈平衡,因此理论上是可行的。

(一)、Linux IAT HOOK

Linux ELF动态节信息DT_JMPREL入口处,保存了ELF重定位入口的地址,通常指向 .rela.plt 重定位节,类似Windows下的导入地址表。

重定位节表项数据结构为Elf64_Rel/Elf64_Rela,定义如下:

{

Elf64_Addr r_offset;          /* Address */

Elf64_Xword      r_info;                     /* Relocation type and symbol index */

Elf64_Sxword    r_addend;         /* Addend */

} Elf64_Rela;

Rela比Rel多了个r_addend字段,其中r_info对应重定位类型和符号表索引位置(高32位为索引,低32位为类型),通常GCC编译重定位类型为 R_X86_64_JUMP_SLOT

对应的GLIBC处理函数为 elf_machine_rela/elf_machine_lazy_rel ,大致如下:

#define ELF64_R_TYPE(i)                   ((i) & 0xffffffff)

elf_machine_rela (struct link_map *map, const Elf64_Rela *reloc,

const Elf64_Sym *sym, const struct r_found_version *version,

void *const reloc_addr_arg)

Elf64_Addr *const reloc_addr = reloc_addr_arg;

struct link_map *sym_map = RESOLVE_MAP (&sym, version, r_type);

Elf64_Addr value = (sym == NULL ? 0 : (Elf64_Addr) sym_map->l_addr + sym->st_value);

case R_X86_64_JUMP_SLOT:

*reloc_addr = value + reloc->r_addend;

elf_machine_lazy_rel (struct link_map *map,

Elf64_Addr l_addr, const Elf64_Rela *reloc)

Elf64_Addr *const reloc_addr = (void *) (l_addr + reloc->r_offset) ;

const unsigned long int r_type = ELF64_R_TYPE (reloc->r_info);

/* Check for unexpected PLT reloc type.  */

if (__builtin_expect (r_type == R_X86_64_JUMP_SLOT , 1))

{

if (__builtin_expect ( map->l_mach.plt , 0) == 0)

*reloc_addr += l_addr;

else

*reloc_addr =map->l_mach.plt + (((Elf64_Addr) reloc_addr) – map->l_mach.gotplt) * 2;

}

其中 reloc_addr 就是重定位地址, l_addr+ reloc->r_offset ,通过修改该地址对应的内存数据,即可实现Linux系统的导入函数HOOK,fputs是redis进程直接导入的GLIBC函数,此时l_addr 等于link_map->l_addr为0。

因此IAT HOOK流程如下

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

然而在Redis实际Exploite时,在执行完命令后,基本上都会出现Redis服务进程Crash现象,主要原因是LUA写内存并不完美,会多写4个字节03 00 00 00到随后的地址中,导致重定位地址表后一项函数地址被修改为无效地址,Redis测试bin中,紧随fputs符号的就是memset,该函数调用异常频繁,基本上100%崩溃。

因此必须解决这个问题才能达成完美利用,此时比较容易想到的解决方案就是,先保存所有的原始重定位地址列表,然后依次向后写,通常重定位表由于内存对齐,应该会有部分空隙(未实际验证),这样我们可以将多写的4字节推移到最后一项。然而实测时发现,写重定位地址表有一定概率崩溃,毕竟LUA脚本执行时也在频繁调用导入函数,基本上到不了最后一项,Redis服务进程就已经崩溃了。

(二)、突破LUA写内存限制

LUA写内存不完美的最大问题在于OP_SETUPVAL时,setobj写内存时,会将4字节的tt字段写入到目标内存后4字节。如果tt数据能够被我们控制,又刚好是待写内存后的4字节值!那就能间接实现完美内存写入8字节了。

tt字段怎么控制呢,只要能知道地址就好办了,毕竟可以任意地址读写了(虽然不完美)。LUA函数变量是保存在lua_State状态的临时堆栈中的

CommonHeader;

lu_byte status;

StkId top;  /* first free slot in the stack */

StkId base;  /* base of current function */

其中base堆栈情况,在函数调用时大致如下(参考 luaD_precall 代码):

func parms1 parms_max stack1 stack_max
  base          

因此我们可以精确控制LUA函数,通过base+offset偏移地址获取到任意LUA变量的地址,并通过写内容方式控制tt内容。

这里需要注意的是lua_State的堆栈是动态变化的, luaD_checkstack 时会检查堆栈是否需要增长,如果堆栈预存不够, luaD_growstack 调用 luaD_reallocstack 重分配堆栈,因此最好在需要写tt的时候再计算,防止提前计算的地址无效。

offset的值可以直接通过调试器得到,只要Exp脚本不变化,偏移地址都是固定的,这是由LUA编译器决定的。

一个小坑

OP_SETUPVAL在 setobj 写12字节内存后,会立即调用 luaC_barrier 函数,如下

int b=GETARG_B(i);

UpVal *uv = cl->upvals[b];

setobj (L, uv->v, ra);

luaC_barrier (L, uv, ra );

如果ra写入tt的是 iscollectable(o) (ttype(o) >= LUA_TSTRING)的话,会检查垃圾收集等信息,如果ra指向一个无效内存的话,就会内存读异常崩溃。在写测试代码时,尝试写入”AAAABBBBCCCC”,一直崩溃到笔者都快崩溃(就差一步就成功了),最后才意识到必须写入一个合法内存地址才能通过,而EXP恰巧就是要写入一个函数地址,真是山穷水尽疑无路,柳暗花明又一村。

(三)完美EXP

完美的EXP应该是没有的,只能做到尽量完美,实测在Ubuntu14.04 x64 默认编译环境下能够凑合用,测试发现有时还是会出现崩溃,毕竟写IAT 12字节不是原子操作。能执行一次命令不崩溃服务进程,其实就已经可以实用了,最后附上一张成功利用截图。

Redis Lua远程代码执行EXP

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址