游客发表
2019年秋天,渗透代码一个叫Alex Birsan的渗透代码安全研究员在GitHub上闲逛时发现了一件事:他随手搜了几个大公司的内部包名,比如“paypal”,渗透代码 “shopify”,结果发现这些名字在公共npm仓库里根本没被注册。渗透代码他花了几分钟,渗透代码注册了其中一个,渗透代码然后写了个极简的渗透代码package.json,里面就一行代码——向他的渗透代码服务器发一个HTTP请求。几个小时后,渗透代码他的渗透代码服务器日志里出现了来自PayPal内部网络的首个请求。
这就是渗透代码“依赖混淆”攻击的雏形。它不靠漏洞,渗透代码不靠0day,渗透代码靠的渗透代码是现代软件开发的一个基本事实:你写的代码里,很大一部分不是渗透代码你写的。
一个Node.js项目,哪怕只有几行业务逻辑,node_modules文件夹里也可能躺着几百个包。这些包又各自依赖别的包,层层嵌套。npm的解析逻辑是:先查你项目里有没有,没有就去公共registry拉。问题出在“私有包”和“公共包”同名的时候——很多企业的私有registry配置并不严格,或者开发者本地环境压根没连上公司内网。npm会默认去公共源找,而公共源上那个包,可能是Alex Birsan注册的。
微软、苹果、特斯拉、Yelp都中过这招。Birsan后来在DevOps平台上一口气注册了三十多家公司的内部包名,全部命中。没有一家公司提前发现。
不是技术难,是流程难。大公司的内部包名往往就是项目代号或者服务名,这些名字在Jira、Confluence、内部文档里到处都是。攻击者不需要渗透,只需要读你的招聘JD——上面写着“熟悉paypal-checkout-service者优先”。
更麻烦的是,很多团队用.npmrc文件配置私有源,但这个文件经常被.gitignore掉,或者在不同开发机上配置不一致。CI/CD流水线里如果没锁死registry地址,一次构建就可能把外部代码拉进来。Birsan的payload极其简单,就是一个postinstall脚本。npm install跑完,脚本执行,数据外发。整个过程不到两秒。
2021年12月,Log4j爆雷。一个被无数Java应用间接依赖的日志库,因为一个JNDI lookup漏洞,让全球安全团队过了个难忘的圣诞。这件事和依赖混淆是同一个底层逻辑:你信任的代码里,藏着你没看过的代码。
我翻过几个中型项目的package-lock.json,平均每个项目直接依赖40个包,间接依赖超过800个。这800个包里,有多少是个人维护者?有多少最后一次更新在三年前?有多少的npm账号连2FA都没开?没人说得清。
有个细节挺有意思。Birsan做实验时,为了不造成实际危害,payload只发了一个带主机名和用户名的HTTP请求。但就这个请求,让他拿到了微软内部一堆机器的hostname。如果换成反弹shell,或者挖矿脚本,或者窃取环境变量里的AWS密钥——后果不用多想。
企业侧,最直接的是在.npmrc里写死registry,并且用npm scopes把内部包全部圈起来,比如@company/开头。CI流水线里加一步npm config get registry的校验。开发侧,别随便npm install一个来路不明的包,尤其别在连了生产VPN的机器上跑。
但说实话,这些措施在“开发体验”面前经常让步。装个包还要配registry?太麻烦。锁版本?那怎么用新特性。安全团队推不动,因为业务团队觉得你在拖进度。
Birsan那篇博客的结尾写得很克制。他说,这不是npm的问题,也不是某家公司的问题,是整个生态默认信任模型的问题。你信任一个包,是因为别人也在用。但“别人也在用”从来就不是安全证明。
代码渗透不需要多高明的手段。有时候,只是一个没人注册的包名,和一个没关掉的postinstall脚本。
友情链接