lcrworld
文章

我的博客网站被克隆了!但是我只用了14行代码完成反击

76 阅读 6186 字 约 31 分钟 2026-07-25 12:15 · 更新于 2026-07-25 13:06

2026 年 7 月 25 日,周六,早上 9:30。

​‌‌​‌‌​​​‌‌​‌‌‌‌​‌‌​​​‌‌​‌‌​​​​‌​‌‌​‌‌​​​‌‌​‌​​​​‌‌​‌‌‌‌​‌‌‌​​‌‌​‌‌‌​‌​​​‌‌‌‌‌​​​‌‌​​​​‌​​‌‌‌​‌​​​‌‌​‌​‌​​‌‌​​​‌​‌‌‌‌‌​​​‌‌‌​‌​​​​‌‌‌​‌​​‌‌​‌‌​‌​‌‌‌​​‌‌​‌‌‌​‌‌​​‌‌​​‌‌​​‌‌‌​​‌​​‌‌​​‌​​​‌‌‌​‌‌​​‌‌‌​‌‌​

我像往常一样打开搜索引擎,输入 lcrworld.xyz 自搜一下——看看收录有没有变化、排名涨了没。这是站长的日常,一种 职业病 好习惯😊。


结果这一搜,搜出事了。


搜索结果里除了我的 lcrworld.xyz,还赫然躺着一个 arvcbv.com。标题一样、描述一样、点进去一看——页面也一模一样。我新发的文章,他那边同步更新了。我换了个头图,他那边也跟着换了。

当时我的表情大概是这样的:◐⩊◑

一、恐慌:9:30 — 10:00

第一反应:服务器被黑了。

冷静分析一下,如果对方只是扒站(wget 下载前端页面),那文章内容应该是静态的、有延迟的。但 arvcbv.com 上的文章是实时的,我刚发的他就有。这意味着两种可能:

  1. 他入侵了我的服务器,直接读了我的数据库。
  2. 他在做某种实时代理。

不管是哪种,都得先确认服务器有没有被入侵。于是开始了全面取证。

先看 SSH 登录日志。这一看,血压直接上来了:

$ grep 'Failed password' /var/log/auth.log | wc -l
2847

$ grep 'Failed password' /var/log/auth.log | awk '{for(i=1;i<=NF;i++) if($i=="from") print $(i+1)}' | sort | uniq -c | sort -rn | head -5
    596 91.92.40.50      # 保加利亚
    503 27.71.230.31     # 越南
    377 8.219.137.42     # 阿里云新加坡
    146 1.255.171.167    # 韩国
     96 39.97.229.27     # 阿里云杭州

来自 20 多个 IP 的 2800+ 次暴力破解,日夜不停地轰炸我的 22 端口。保加利亚的这位大哥尤其执着,596 次,平均一天 20 多次,比我打卡还规律。

然后检查了一下服务器的防护配置:

  • fail2ban?没装。
  • UFW 防火墙?没开。
  • root 密码登录?开着。
  • SSH 默认端口?22。

好家伙,这配置简直就是在门口挂了个牌子写着"欢迎光临,门没锁,钥匙在垫子下面"。2800 多次爆破没有一次被自动拦截,全靠密码本身扛着。(这段是AI骂我的时候写的wwww)

但密码能扛多久?迟早有一天被人撞开。想到这里后背一阵发凉。

但是——成功入侵了吗?

查完所有成功登录记录,松了一口气:

检查项 结果
成功登录 IP全部中国境内,和我的活动范围一致
后门 / 恶意进程没有
异常 crontab没有
SUID 提权文件全是系统默认的
LD_PRELOAD 注入没有
.bash_history干干净净,就几条正常命令
系统文件被篡改查了 apt 历史,是 Ubuntu 自动安全更新

服务器没有被入侵。

好,排除了一种可能。但困惑反而更深了——

没入侵我的服务器,他是如何通过别的方式克隆我整个网站的?

我的源代码没泄露过,Git 仓库是私有的,数据库密码也没给任何人。一个完整的网站克隆——前端页面 + 后端 API + 实时数据同步——这总不能是简单的扒站工具干的吧?

带着这个疑问,开始对比两个网站的技术细节。

二、真相:10:00 — 10:30

真相大白的过程,比我想象的快得多,也简单得多。简单到让人想笑。

第一步:对比前端文件

arvcbv.com:   assets/index-1K0S1bxy.js
lcrworld.xyz:  assets/index-1K0S1bxy.js

Bundle hash 一模一样

arvcbv.com 上这个文件的 last-modified 是 7 月 23 日——两天前。那天,对方只是跑了一行命令:

wget -r -k -p https://lcrworld.xyz/

把我的网站所有公开文件下载到了他的服务器上。HTML、JS、CSS、字体——这些东西本来就对全网公开,浏览器要渲染页面就必须能下载。任何人打开开发者工具的 Network 面板,都能把这些文件一个个保存下来。

他不需要我的源代码。编译后的产物就是公开的。SPA 网站编译后的前端代码本身就是完全公开的,和源代码等价(尤其是对有耐心的人来说)。

第二步:对比 API 响应头

前端文件能下载,但文章数据呢?arvcbv.com 上显示的是我数据库里的实时文章,这是怎么做到的?

对比两个网站的 API 响应头:

# arvcbv.com 的 API 响应头
Server: cloudflare
ETag: W/"3660-..."   ← Cloudflare 生成的

# lcrworld.xyz 的 API 响应头
Server: nginx/1.30.3

arvcbv.com 的 Server 是 cloudflare,不是 nginx。说明它走的是 Cloudflare 的服务。而我直接请求 arvcbv.com/api/rss/articles,返回的是我数据库里的真实文章——格式、内容、时间戳完全一致。

真相只有一个:

他用 Cloudflare Worker 写了一个反向代理,把所有 /api/* 请求转发到了 https://lcrworld.xyz/api/*

// Cloudflare Worker 伪代码,大概就这么几行
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname.startsWith('/api')) {
      // 所有 API 请求转发到原站
      return fetch('https://lcrworld.xyz' + url.pathname + url.search, request);
    }
    // 其他请求走本地静态文件
    return fetch(request);
  }
}

访客访问 arvcbv.com → Cloudflare Worker 把请求转发到我服务器 → 我的服务器忠实地返回文章数据。从服务器的角度看,这就是一个正常的 API 请求,它没有理由拒绝。

整个过程,对方只需要四步:

1. wget -r https://lcrworld.xyz/          ← 下载公开文件
2. 部署到自己服务器 / Cloudflare Pages
3. 写一个 Cloudflare Worker 做 API 代理
4. 绑定域名 arvcbv.com

没有任何一步需要入侵我的服务器。不需要 SSH 登录,不需要源代码,不需要数据库密码,不需要任何黑客技术。连脚本小子都算不上,这只是一个会写 Cloudflare Worker 的普通人就能做到的事。


这就是 SPA(单页应用)网站的天然弱点:前端代码完全公开 + API 可以被反向代理。任何 Vue/React/Angular 写的网站都可以用这个方法克隆。相当于在自己家门口装一面镜子,把对面屋里的画面实时反射过来。

三、反击:10:30 — 11:30

搞清楚原理之后,事情就简单了。既然他的 API 是代理我的服务器,那我在服务器端做校验,他的代理就会直接失效。

分两步走,先断图片,再断 API。

第一刀:Nginx 图片防盗链

arvcbv.com 的图片是热链我的服务器——HTML 里是相对路径 /uploads/xxx.jpg,通过 Cloudflare Worker 代理后实际从我服务器加载。等于他白嫖我的存储和带宽,给我刷 PV。


(我说怎么ICP备案期间,明明DNS解析都改到别的服务器了,网站访问就会REFUSED,每天的PV还是有几个呢)


在 Nginx 配置里加一段 valid_referers 校验:

location ~* \.(png|jpg|jpeg|gif|webp|avif|svg)$ {
    valid_referers none blocked server_names
        *.lcrworld.xyz lcrworld.xyz
        ~\.google\. ~\.bing\. ~\.baidu\. ~\.yandex\.;
    if ($invalid_referer) {
        return 403;
    }
    expires 30d;
}

效果:arvcbv.com 上所有图片瞬间全部裂图。访客看到的就是一片白板加裂图图标。原本漂漂亮亮的文章页,瞬间变成了 1998 年的纯文本网页。

但这只是开胃菜。图片断了只是皮外伤,文章内容还在,API 还在正常代理。真正的杀手锏在下面。

第二刀:Express Host 头校验 —— 致命一击

这一步是核心。

Cloudflare Worker 把请求转发到我服务器时,携带的 Host 头是 arvcbv.com——这是 HTTP 协议的基本行为,Worker 没法也不需要改这个头。那我只要在 Express 后端检查 Host 头,非本站请求一律拒绝就行了。


我在门口装了个门禁,刷我家的卡才让进。你拿着别人的卡来,门禁直接报警。😡


app.jsconst app = express() 后面加一个中间件:

const ALLOWED_HOSTS = ['lcrworld.xyz', 'www.lcrworld.xyz', 'en.lcrworld.xyz', 'localhost', '127.0.0.1'];
const ALLOWED_ORIGINS = ['https://lcrworld.xyz', 'https://www.lcrworld.xyz', 'https://en.lcrworld.xyz'];

app.use((req, res, next) => {
    if (process.env.NODE_ENV !== 'production') return next();
    
    const host = (req.headers['x-forwarded-host'] || req.headers.host || '').toLowerCase().split(':')[0];
    const origin = (req.headers['origin'] || '').toLowerCase();
    
    const hostOk = ALLOWED_HOSTS.some(allowed => host === allowed || host.endsWith('.' + allowed));
    const originOk = !origin || ALLOWED_ORIGINS.some(allowed => origin === allowed);
    
    if (!hostOk || !originOk) {
        console.warn(`[BLOCKED] Host=${host} Origin=${origin} Path=${req.path}`);
        return res.status(403).json({ error: 'Unauthorized domain' });
    }
    
    next();
});

就这么十几行代码。pm2 restart blog-server,搞定。


原理很简单:Cloudflare Worker 转发请求时,Host 头是 arvcbv.com,不在白名单里 → 直接 403。他无法伪造 lcrworld.xyz 的 Host 头,因为域名不归他控制——除非他把我的 DNS 也劫持了,但那就是另一个故事了。

验证:一击毙命

中间件上线后,立即测试:

# 原站正常
$ curl -s -o /dev/null -w '%{http_code}' lcrworld.xyz/api/rss/articles
200 ✓

# 克隆站 - API
$ curl -s arvcbv.com/api/rss/articles
{"error":"Unauthorized domain"}
403 ✓

# 克隆站 - 首页
$ curl -s -o /dev/null -w '%{http_code}' arvcbv.com/
403 ✓

arvcbv.com 全站 403。

文章列表,403。文章内容,403。评论,403。图片,403。搜索,403。所有 API,全部 403。

访客打开 arvcbv.com,看到的是一个光秃秃的白屏,上面写着:


 {"error":"Unauthorized domain"}

他之前 wget -r 下载的所有静态文件、写的 Cloudflare Worker、配置的域名 DNS、申请的 SSL 证书——全部白干。一夜回到解放前。🤣


而且他没有任何办法绕过。Host 头是 HTTP 协议层面的东西,他改不了。除非他把我整个数据库也复制一份到他自己的服务器上——但那是"搬家"不是"克隆",而且他没有我的数据库。


PM2 日志里可以看到拦截记录:

[BLOCKED] Host=arvcbv.com Origin=https://arvcbv.com Path=/api/articles UA=Mozilla/5.0...
[BLOCKED] Host=arvcbv.com Origin= Path=/api/rss/articles UA=Mozilla/5.0...

每一行 BLOCKED 都是一次被拒绝的请求。想象一下他刷新自己网站看到白屏时的表情——大概和我早上 9:30 发现被克隆时的表情一样精彩。

从发现到封堵,两个小时。9:30 发现,11:30 收工。

四、关于报警

很多人遇到这种事第一反应是报警。说实话,我也认真考虑过,还做了完整的证据采集——服务器全量日志 534MB、20 多个攻击 IP 列表、完整的取证报告。但冷静分析下来:

SSH 暴力破解——没成功入侵,攻击源主要是境外 IP(保加利亚、越南、韩国),跨境执法几乎不可能。全网每台开放 22 端口的服务器每天都在被扫,这在安全圈叫"互联网背景噪音"。你报警说"有人敲了我家门 2800 次没敲开",警察大概会让你装个更好的锁。

网站克隆——属于侵犯著作权和不正当竞争,但刑事立案门槛高,需要证明实际经济损失。更实际的维权路径是向域名注册商投诉、向 Cloudflare 发 abuse report 举报其 Worker 侵权、发律师函要求下架。技术封堵比走法律程序直接得多,效果立竿见影。

当然,所有证据已经打包保存了。万一后面要走法律途径,证据链是完整的。先技术封堵止血,法律维权慢慢来。

五、教训

这次事件暴露了两个层面的问题。

安全防护层面——服务器裸奔太久了。fail2ban 没装、防火墙没开、root 密码登录开着、SSH 用默认 22 端口。2800 次暴力破解没有任何自动拦截机制。虽然这次没被入侵,但说实话,纯属运气好。密码是扛住了,但谁能保证下一次?已列入加固清单:

  1. 装 fail2ban:5 次失败封禁 24 小时
  2. 开 UFW 防火墙:只开 22/80/443
  3. 禁 root 密码登录,换 SSH 密钥认证
  4. 改 SSH 端口:别用 22

架构层面——SPA 网站的前端代码是完全公开的,API 可以被任意反向代理。这是所有 SPA 的通病,不是 bug,是 feature——浏览器需要能下载这些文件才能渲染页面,你没法不公开。但只要在 API 层做 Host 校验,克隆站就是一戳就破的纸老虎。

这就像是:你的商店橱窗是透明的(前端公开),谁都能看、谁都能拍照。但收银台(API)你可以验身份,不是你店里的人不卖。他站在自己店里拿对讲机帮你点单(反向代理),你只要在收银台说一句"只认本店会员",他的对讲机就废了。

六、最后

整个事件从发现到解决,两个小时。最终结果:

站点 API 首页 图片
lcrworld.xyz(原站)200 ✓200 ✓200 ✓
arvcbv.com(克隆站)403 ✗403 ✗403 ✗

克隆者花时间下载文件、写 Worker、配域名、申请证书,我花十几行代码就让这一切归零。

如果你也是站长,建议现在就去你的 API 加一个 Host 头校验。五分钟的事,但能让 99% 的克隆者白干一场。

毕竟,让坏人白干一场,是这个世界上为数不多的、纯粹的快乐。


全文完

本作品采用 CC BY-NC-SA 4.0 许可协议

评论 (0)

支持 Markdown · Ctrl+Enter 发送
加载评论中...