导语:在Alpine Linux 漏洞利用的第一部分《Alpine Linux:从漏洞发现到代码执行》,我对Alpine Linux的软件包管理器中的两个关键漏洞, CVE-2017-9669和CVE-2017-9671做了深入的分析。

1500366177576938.jpg

在Alpine Linux 漏洞利用的第一部分《Alpine Linux:从漏洞发现到代码执行》,我对Alpine Linux的软件包管理器中的两个关键漏洞, CVE-2017-9669和CVE-2017-9671做了深入的分析。

今天这部分,我会对这两个漏洞的发现过程进行详细描述,并演示如何实现远程执行代码。

第一步是把部署环境中导致 漏洞的所有内容都进行复制,由于这是一个可用性测试(sanity check),所以必须首先做,只有完成这一步才能尝试进行复杂的有效载荷。我的fuzzer的 漏洞文件直接从apk_tar_parse中的文件处理程序中读取,但是在实际情况下,文件将从tar.gz服务器读取。

为了模仿一个中间人攻击的场景,我使用了Docker的主机功能,以便对要添加到映像的主机文件的映射进行自定义。另外,我还启动了一个nginx映像,以从本地目录(使用-v)来服务我的文件。

我从fuzzer的 漏洞目录中压缩了 漏洞的tar,并将其放在服务目录中:

cat crash | gzip -9 > ~/docker/files/alpine/v3.6/main/x86_64/APKINDEX.tar.gz

运行的映像:

docker run -ti --add-host dl-cdn.alpinelinux.org:172.17.0.2 alpine:3.6

执行“apk更新”后,我收到一个细分化的漏洞。这意味着漏洞是可重复的,现在是时候调试执行,并讨论如何利用漏洞。

我写了一个小Dockerfile,它下载命令间的依赖关系,并从源代码编译apk-tools包。我添加了CFLAGS -g和-O0标志以包括所有调试符号,让调试变得更简单。我从这个Docker文件构建了一个映像,并使用-privileged运行,以允许gdb禁用ASLR。

我运行了“gdb –args apk update”,并得到了预期的segfault。 gdb在 free()上的漏洞,这意味着这可能是一个堆溢出,并且在一个简短的调试会话后,我发现了第一次调用blob_realloc的漏洞,在此,我建议你查看源文件archive.c

我制作了一个简单的文件,通过拥有一个negative size(32位有符号整型范围内的-1 或0o77777777777)的块文件( block file)来触发漏洞:

1.png

我压缩了文件,并在服务器中运行了apk文件。此时,当复制entry.name缓冲区的空终止字符串时会产生漏洞,这是因为entry.name没有分配,所以它指向null。同时entry.size是0xffffffffffffffff,即未映射的地址,意味着任何复制的尝试都会导致segfault:

2.png

译者注:代码中的一个问题是entry.size被隐式转换为64位, get_octal函数返回一个int,但entry.size的类型为off_t

我制作了另一个文件,这个文件有两个tar块,一个是正确地分配一个大小为I的缓冲区,另一个文件则利用分配的缓冲区来使用漏洞。

我调试了漏洞执行(而不是对blob_realloc的调用),并按预期缓冲区被首先分配,然后在第二个块的解析过程中被用作复制目标。不过要注意的是,要使发现变得更容易,应该调用gzip_read(来自is-> read),它会将块从源流复制到目标流,一旦源流用完就停止,所以它只会复制一样数据。

了解和利用堆

现在,我就有了一个缓冲区溢出,并控制大小和内容。虽然,理论上,可能有许多方法来实现代码执行,但事实上,在经过大量验证后,其中大多数集中在glibc。

然而,Alpine使用的是musl libc而不是glibc。这无疑有不同的方式来利用它,在此,我不再赘述。由于我的目标只是为了提供POC来实现远程执行,所以本文,我只会尝试一些更简单的办法。

如果我可以在堆上找到任何有用的东西,就会进行覆盖来操纵执行流程。例如,我可以覆盖一个检查签名的标志,然后安装一个我想要的包,或者更改一些要安装的包的数据等。

通过调用像execv或者系统这样的方法来实现代码执行是最简单的方法和最快的方式。但是由于溢出会破坏堆栈,所以我需要重新构建它,或者在内存管理器使用覆盖内存之前调用回调函数(大概就是在调用free() 发生的那个时候)。

我继续检查代码,尝试在堆中找到任何可能在内存中的前一个entry.name的变量。调试一段时间后,我想我实际上可以覆盖的是struct,它用于调用写入流(writes to the stream)的函数。它的类型是apk_istream:

struct apk_istream {
void (*get_meta)(void *stream, struct apk_file_meta *meta);
ssize_t (*read)(void *stream, void *ptr, size_t size);
void (*close)(void *stream);
};

理论上我将覆盖那些到我的目标函数中进行读取的函数,然后找到要覆盖的内容来控制调用的参数。

通过把一个断点放在调用读取,可以计算出我的缓冲区之间的delta是is架构:

3.png

我填写了我精心设计的tar文件0x153a0,它对应16个零字节,以覆盖get_meta,并使用零读取函数指针(每个函数指针是8个字节),这些程序漏洞会在0x0000000000000000处发生。

现在是尝试调用我的目标函数的时候了,为了简单起见,我决定使用一个字符串作为参数,并将其作为shell命令执行。它的地址是0x7ffff7db0956。对于参数,似乎is->read的调用本身就是第一个参数。只要get_meta函数从不被调用,我就可以用我的shell字符串覆盖前8个字节。

我的tar文件的结尾看起来像这样:

4.png

前8个字节是我的shell字符串,后面是系统地址的8个字节,执行情况正如预期一样:

5.png

不过这个有效载荷长度被限制在8个字节,在8字节内,这个解决方案似乎是有效的。但是有时会出现意外的问题,由于is-> read调用以块形式写入,所以可能会有一种情况,即它只写入指针的一部分(只有4个字节),然后继续调用该指针来读取更多的数据,这是我在第一次构建这个POC时遇到的一个问题。

所以为了继续寻找另一个可以覆盖的结构,我恢复了文件最初指针的is架构,并添加了8字节的数据,现在看看我是否得到另一个漏洞(我添加的数据是在0xAA字节之后):

6.png

看来我能用我的数据覆盖gis-> bs指针,你可以检查gunzip.c以准群理解这个bs指针是什么(请参阅struct apk_gzip_istream)。 bs是apk_bstream:

struct apk_bstream {
unsigned int flags;
void (*get_meta)(void *stream, struct apk_file_meta *meta);
apk_blob_t (*read)(void *stream, apk_blob_t token);
void (*close)(void *stream, size_t *size);
};

它虽然以相同的方式使用(也将其自身作为第一个参数),但它给出了另外8个字节(标志)用于shell字符串。现在可以通过将指针覆盖到堆上的某个位置来运行代码。

为方便起见,我把地址放在32字节之前。该地址将为0x5555559682a0(is-0x20)。如上所述,它的前16个字节将由我的shell字符串。之后,我会把系统的地址覆盖到读取地址。

有效载荷看起来像这样:

7.png

红色代表gis-> bs-> flags和gis-> bs-> get_meta,用shell字符串覆盖。

黄色代表gis->bs->read,用系统地址覆盖。

其余的颜色代表  gis->bs->close ,然后是 – > get_meta,这是无关紧要的,因为is函数就是最初的地址。

8.png

总结

通过覆盖堆上的结构体的函数指针,我能够在目标设备上执行代码。我本文的例子中运行echo,但有效载荷可以是任何东西,例如,打开一个带有netcat的远程shell。

值得注意的是,为了找到这个漏洞,我假设攻击者知道执行程序的内存布局。所以作为复制我的漏洞利用的先决条件,ASLR必须被禁止执行(除非你在apk中发现了内存漏洞)。 Gdb会默认情况下执行此操作,因此在运行时不需要手动禁用ASLR。

源链接

Hacking more

...