CVE-2018-0296 Cisco ASA 拒绝服务漏洞分析

漏洞简介

CVE-2018-0296是思科ASA设备Web服务中存在的一个拒绝服务漏洞,远程未认证的攻击者利用该漏洞可造成设备崩溃重启。该漏洞最初由来自Securitum的安全研究人员Michal Bentkowski发现,其在博客中提到该漏洞最初是一个认证绕过漏洞,上报给思科后,最终被归类为拒绝服务漏洞。据思科发布的安全公告显示:针对部分型号的设备,该漏洞可造成设备崩溃重启;而针对其他型号的设备,利用该漏洞可获取设备的敏感信息,造成信息泄露。

针对该漏洞,目前已有公开的PoC脚本,可用于获取设备的敏感信息如用户名,或造成设备崩溃重启。经过实际测试,在公开PoC中造成该漏洞的关键url如下。

1
https://<ip>:<port>/+CSCOU+/../+CSCOE+/files/file_list.json?path=/

下面利用思科ASA设备和已有的PoC脚本,对该漏洞的形成原因进行分析。

背景知识

在实际对漏洞进行分析的过程中,发现思科ASA设备的lina程序中,存在大量的Lua脚本以及对Lua api的调用。为了便于理解,下面对Lua脚本的相关知识进行简单介绍。

Lua脚本和C/C++交互

Lua是一个小巧的脚本语言,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。Lua脚本可以很容易被C/C++代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛使用。不仅可作为扩展脚本,也可以作为普通的配置文件,代替XMLini等文件格式,并且更容易理解和维护。

LuaC/C++通信的主要方式是一个虚拟栈,其特点是后进先出。在Lua中,Lua栈就是一个struct,栈的索引可以是正数也可以是负数,其中正数索引1永远表示栈底,负数索引-1永远表示栈顶,如下图所示。

Lua中的栈在stack_init()函数中创建,其类似于下面的定义。

1
TObject stack[BASIC_STACK_SIZE + EXTRA_STACK]

Lua中,可以往栈上压入字符串、数值、表和闭包等类型,最后统一用Tobject这种数据结构进行保存,如下。TObject结构对应于Lua中所有的数据类型,是一个{值,类型}结构,它将值和类型绑在一起。其中用tt表示value的类型,value是一个联合体,共有4个域,说明如下。

  • p:可以保存一个指针,实际上指向Lua中的light userdata结构
  • n:保存数值,包括intfloat等类型
  • b:保存布尔值
  • gc:保存需要内存管理垃圾回收的类型如stringtableclosure
1
2
3
4
5
6
7
8
9
10
11
12
// lua 数据类型
#define LUA_TNONE (-1)

#define LUA_TNIL 0 // 空值
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8

Lua 栈操作常用api

Lua中提供了一系列与栈操作相关的api,常用的api如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 压入元素
void lua_pushnil (lua_State *L);
void lua_pushboolean (lua_State *L, int bool);
void lua_pushnumber (lua_State *L, double n);
void lua_pushlstring (lua_State *L, const char *s, size_t length);
void lua_pushstring (lua_State *L, const char *s);

// 检查一个元素是否是一个指定的类型
int lua_is* (lua_State *L, int index); // *可以是任何类型

// 获取元素
int lua_toboolean (lua_State *L, int index);
double lua_tonumber (lua_State *L, int index);
const char * lua_tostring (lua_State *L, int index);
size_t lua_strlen (lua_State *L, int index);

环境准备

调试环境搭建

由于该漏洞在不同型号设备上表现的行为不一致,这里分别选取了32位的设备和64位的设备,相关信息如下。其中,前面2个设备用于漏洞分析,设备asav9101用于补丁分析。

  • 真实设备ASA 5505,镜像为asa924-k8.bin32bit
  • GNS3仿真设备,镜像为asav962.qcow264bit
  • GNS3仿真设备,镜像为asav9101.qcow264bit

ASA设备中内置了gdbsever,但默认不启动。为了对设备进行调试,需要修改镜像文件以启动gdbserver。同时,由于ASA设备会对镜像文件进行完整性校验,所以修改后的镜像文件无法直接通过tftpASDM工具传入设备。ASA使用CF卡作为存储设备,可以通过用CF卡读卡器直接将镜像写入CF卡中的方式绕过校验,因为ASA没有对CF中的镜像进行校验。

详细的调试环境搭建和镜像修改等内容可以参考nccgroup的系列博客.

设备配置

思科ASA设备会在443端口提供Web服务。笔者在进行测试时,对设备的WebVPN功能(Clientless SSL VPN)进行了配置,使得可以访问Web服务,进而触发该漏洞。详细的配置操作可参考思科相关文档

漏洞分析

环境搭建好后,运行已有的PoC脚本,针对asa924设备,会造成敏感信息泄露,而针对asav962设备,会造成设备崩溃重启。下面基于asav962设备,重点对拒绝服务漏洞进行分析。

崩溃分析

运行PoC脚本,在gdb中捕获到如下错误。可以看到,崩溃点在libc.so.6库中的strlen()函数里,由于在0x7ffff497699a处尝试访问一个非法的内存地址0x13,故产生Segmentation fault错误,而rax的值来源于strlen()函数的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Thread 2 received signal SIGSEGV, Segmentation fault.
[Switching to Thread 1677]
0x00007ffff497699a in strlen () from ***/_asav962.qcow2.extracted/rootfs/lib64/libc.so.6
(gdb) x/10i $rip
=> 0x7ffff497699a <strlen+42>: movdqu xmm12,XMMWORD PTR [rax]
0x7ffff497699f <strlen+47>: pcmpeqb xmm12,xmm8
0x7ffff49769a4 <strlen+52>: pmovmskb edx,xmm12
0x7ffff49769a9 <strlen+57>: test edx,edx
0x7ffff49769ab <strlen+59>: je 0x7ffff49769b1 <strlen+65>
0x7ffff49769ad <strlen+61>: bsf eax,edx
0x7ffff49769b0 <strlen+64>: ret
0x7ffff49769b1 <strlen+65>: and rax,0xfffffffffffffff0
0x7ffff49769b5 <strlen+69>: pcmpeqb xmm9,XMMWORD PTR [rax+0x10]
0x7ffff49769bb <strlen+75>: pcmpeqb xmm10,XMMWORD PTR [rax+0x20]
(gdb) i r $rax
rax 0x13 19
(gdb) bt
#0 0x00007ffff497699a in strlen () from ***/_asav962.qcow2.extracted/rootfs/lib64/libc.so.6
#1 0x0000555557ee51ce in lua_pushstring ()
#2 0x00005555583c87d2 in webvpn_file_name ()
#3 0x0000555557eec43b in luaD_precall ()
#4 0x0000555557efc258 in luaV_execute ()
#5 0x0000555557eeced0 in luaD_call ()
#6 0x0000555557eebeda in luaD_rawrunprotected ()
#7 0x0000555557eed323 in luaD_pcall ()
#8 0x0000555557ee5de6 in lua_pcall ()
#9 0x0000555557f00821 in lua_dofile ()
#10 0x000055555822053b in aware_run_lua_script_ns ()
#11 0x0000555557dc6e3d in ak47_new_stack_call ()
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

根据栈回溯信息,查看函数lua_pushstring()webvpn_file_name(),其部分伪代码片段如下。在函数webvpn_file_name()中,将v1 + 0x13这个指针作为参数传递给lua_pushstring(),最终传递给strlen()函数。崩溃点处访问的非法内存地址为0x13,说明v1=0,即在webvpn_file_name()lua_touserdata()返回值为NULL(也就是0)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_DWORD *__fastcall lua_pushstring(__int64 a1, const char *a2)
{
size_t v2; // r14
__int64 v3; // r13
_DWORD *result; // rax

if ( a2 )
{
v2 = _wrap_strlen(a2);
// ...
}

signed __int64 __fastcall webvpn_file_name(_QWORD *a1)
{
signed __int64 v1; // rax

v1 = lua_touserdata(a1, 1);
lua_pushstring((__int64)a1, (const char *)(v1 + 0x13));
return 1LL;
}

由前面lua的相关知识可知,函数lua_touserdata()用于获取栈底数据。因此,很自然的想法就是分析这个NULL值是从哪里来的,即在什么地方通过调用lua_pushnil()往栈上压入了NULL值。

静态分析

通过查找字符串/+CSCOE+/files/file_list.json的交叉引用定位到aware_webvpn_content()函数。在该函数中可以看到有很多请求url的字符串,同时还包含很多lua脚本的名称,猜测该函数应该是负责对这些请求进行处理,根据不同的请求url执行对应的lua脚本。示例如下。

查看files_list_json_lua脚本的内容,其主要功能是列出当前路径下的目录或文件,依次调用了Lua中的OPEN_DIR()READ_DIR()FILE_NAME()FILE_IS_DIR()等函数。而在aware_addlib_netfs()函数中,建立了Lua函数和C函数之间的对应关系,示例如下。

1
2
3
4
5
// Lua函数与C函数对应关系
OPEN_DIR() <---> webvpn_open_dir()
READ_DIR() <---> webvpn_read_dir()
FILE_NAME() <---> webvpn_file_name()
FILE_IS_DIR() <---> webvpn_file_is_dir()

在查看对应的C函数时,在webvpn_read_dir()函数中,有一个对lua_pushnil()函数的调用。根据函数的调用顺序,猜测webvpn_file_name()函数中获取到的NULL值来自于这里。

动态分析

根据之前的猜测,尝试在调用lua_pushnil()处下断点,然后查看Lua栈上的数据,如下。

其中,rdi指向的数据结构的定义大致如下,这里主要关注其中的lua_stack_top_ptrlua_stack_base_ptr两个指针,分别指向Lua栈的栈顶和栈底,栈中的元素就是前面提到的{类型,值}结构。

1
2
3
4
5
6
7
8
9
10
11
struct {
uint64 xxx;
uint64 xxx;
uint64 lua_stack_top_ptr; // 指向栈顶 (空栈,即始终指向刚入栈元素的下一个位置)
uint64 lua_stack_base_ptr; // 指向栈底 (栈地址由低向高增长)
uint64 xxx;
uint64 xxx;
uint64 xxx;
uint64 xxx;
...
}

之后在webvpn_file_name()中调用lua_touserdata()函数前下断点,查看此时Lua栈上的内容,如下。此时,lua_touserdata()函数的第2个参数为1,即获取Lua栈底的数据,而此时栈底的数据为NULL

继续单步执行程序,查看函数lua_touserdata()的返回值。可以看到,其返回值确实为NULL,之后将一个非法内存地址0x13作为参数传入了lua_pushstring(),最终导致Segmentation fault错误。

但是,这里的NULL值并不是来自之前lua_pushnil()压入的nil值,而是位于其下面的栈元素。在下断点调试的过程中,发现设置的2个断点均只命中一次就触发了问题,极大地缩小了调试的范围。同时,在2个断点处Lua栈的地址是一样的,因此可以在第1个断点命中后,对相应的Lua栈地址设置硬件断点,看在哪个地方对其值进行了修改。

gdb中设置硬件断点后,继续执行时提示如下错误。网上查找相应的解决方案,建议使用set can-use-hw-watchpoints 0,但实际测试时貌似也存在问题。最后采用hook-stop的方式来观察指定地址处的内容。

1
2
3
define hook-stop
x/2gx <addr>
end

通过设置断点并查看相应地址处的内容,最终定位到修改内容的地方位于luaV_execute()中。对照lua-5.0源码,luaV_execute()函数是Lua VM执行字节码的入口,修改内容的地方位于OP_GETGLOBAL操作码的处理流程中。

asav962与asa924执行流程对比

前面的分析定位到了luaV_execute()函数中,而该函数属于Lua VM的一部分,难道是因为files_list_json_lua脚本存在问题,而导致Lua VM执行字节码时出现错误?由于该拒绝服务漏洞对型号为asa924的设备没有影响,下面对asa924设备上对应的执行流程进行分析。

根据前面的分析思路,在webvpn_file_name()中设置断点,发现其流程与asav962类似,lua_touserdata()函数的返回值同样会为NULL,而asa924设备却不会发生崩溃。2个webvpn_file_name()的对比如下。

通过调试可知,针对32位程序(asa924),lua_touserdata()函数的返回值为指向字符串的指针。当该指针为空时,其直接作为参数传入lua_pushstring(),而在lua_pushstring()中会对参数是否为空进行判断。而针对64位程序(asav962),lua_touserdata()函数的返回值为指向某个结构体的指针。当该指针为空时,传入lua_pushstring()的参数为0x13,从而”绕过“了lua_pushstring()中的校验,最终造成非法内存地址访问。

至此,分析清楚了该拒绝服务漏洞产生的原因,主要是由于32位程序和64位程序中lua_touserdata()函数的返回值代表的结构不一致造成的。

补丁分析

在镜像asav9101.qcow2中该漏洞被修复了。基于前面对漏洞形成原因的分析,下面以asav9101.qcow2镜像为例,对漏洞的修复情况进行简单分析。

目录遍历漏洞补丁分析

通过动态调试分析,对请求url的解析在UrlSniff_cb()函数中完成,其中增加了对./../的处理逻辑,部分代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
v16 = *v5;		// v5 指向请求url
v17 = v5;
v18 = v5;
LABEL_45:
while ( v16 )
{
if ( v16 == '.' )
{
v20 = v18[1];
switch ( v20 )
{
case '.':
v9 = (unsigned __int8)v18[2];
if ( !(_BYTE)v9 )
goto LABEL_75;
if ( (_BYTE)v9 == '/' )
{
v20 = v18[3]; // 匹配到"../"
v18 += 2;
LABEL_75:
++v18;
v16 = v20;
goto LABEL_45;
}
break;
case '/':
v16 = v18[2]; // 匹配到"./"
v18 += 2;
goto LABEL_45;
case '\0':
++v18;
goto LABEL_60;
}
do
{
LABEL_48:

拒绝服务漏洞补丁分析

根据前面的分析可知,拒绝服务漏洞的触发位置在函数webvpn_file_name()中。在镜像asav9101.qcow2中,该函数内容如下,可以看到并没有对该函数进行更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
webvpn_file_name proc near
; __unwind {
push rbp
mov esi, 1
mov rbp, rsp
push rbx
mov rbx, rdi
sub rsp, 8
call lua_touserdata
mov rdi, rbx
lea rsi, [rax+13h]
call lua_pushstring
add rsp, 8
mov eax, 1
pop rbx
pop rbp
retn
; }

在字符串列表中查找/+CSCOE+/files/file_list.json显示没有结果,表明在该镜像中将这个接口去掉了。同时根据之前files_list_json_lua脚本的内容进行查找,在该镜像中仍然可以找到对应的lua脚本内容,但是找不到对该脚本的交叉引用,进一步证实该接口/+CSCOE+/files/file_list.json被去掉了。

小结

  • 利用CVE-2018-0296漏洞,远程未认证的攻击者可以对目标设备实施拒绝服务攻击,或从设备获取敏感信息。
  • 拒绝服务漏洞的形成原因是由于32位程序和64位程序中lua_touserdata()函数的返回值代表的结构不一致造成。
  • 在镜像asav9101.qcow2中已经修复了该漏洞,其中拒绝服务漏洞的修复方式是去掉了触发了该漏洞的请求url接口。

相关链接

  • Cisco Adaptive Security Appliance Web Services Denial of Service Vulnerability
  • Error description CVE-2018-0296 - bypassing authentication in the Cisco ASA web interface
  • Cisco Adaptive Security Appliance - Path Traversal
  • Test CVE-2018-0296 and extract usernames
  • Lua和C++交互详细总结
  • 网络设备漏洞分析技术研究
  • Cisco ASA series part one: Intro to the Cisco ASA

本文首发于安全客,文章链接:https://www.anquanke.com/post/id/171916