猫头鹰
信安舆情早知道

Redis Lua远程代码执行EXP

Author:boywhp(boywhp@126.com)

前篇:LUA虚拟机逃逸

一、概述

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基地址通常做了ASLR处理,但在知道进程基地址前提下,可通过GOT表项获取到,具体见Linux GLIBC源码:elf_machine_runtime_setup函数

/* The GOT entries for functions in the PLT have not yet been filled
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 *

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字节命令,10字节消息是此种利用方式最大的硬伤。

三、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,定义如下:

typedef struct
{

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_SYM(i) ((i) >> 32)
#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状态的临时堆栈中的

struct 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函数,如下

case OP_SETUPVAL: {
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字节不是原子操作。能执行一次命令不崩溃服务进程,其实就已经可以实用了,最后附上一张成功利用截图。

test1

 

原文:乌云知识库  作者:boywhp

感谢所有在过去,现在支持过乌云的人。

转载请注明来自MottoIN,未经允许不得转载!MottoIN » Redis Lua远程代码执行EXP

分享到:更多 ()

评论 抢沙发

评论前必须登录!

 

MottoIN 换一个角度看安全

寻求报道联系我们