用蜜罐把AI扫描器关进假后台"

admin 2026-11-01 01:55:31 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

用蜜罐把 AI 扫描器关进假后台

攻防录

2026年10月29日 14:48
北京

去阅读

DYGK 是个自部署的 Web 欺骗组件。 项目地址:https://github.com/yaklang/DropYourGunKid

用法不复杂:往自己站点的 HTML 里插几个隐藏链接,把访问者引到一棵假归档树,等它用树里的假访问码提交表单,记下 IP 封掉。

插进去的东西长这样:

  兼容接口迁移索引
  历史导出校验记录
  集成客户端验证
</aside>

hidden 和 aria-hidden 都写了,页面上什么都看不到。但 View Source 一下,或者 fetch 拿到原始响应,这三行就躺在 </body> 前面。

会去看原始 HTML 的,基本不是人。CAI、Strix、PentAGI、BoxPwnr 这类框架都是先取响应再解析,不渲染。渲染出来才可见的东西拦不住它们;反过来,渲染不出来的东西正好只钓它们。DYGK 整个设计就架在这个错位上。

原理

入口是给程序准备的

链接文字挑过。「兼容接口迁移索引」「历史导出校验记录」这种名字,对正在找入口的扫描器有点吸引力,对普通访客没干扰,因为压根不显示。

最多 8 个,默认 3 个,由 entries 控制。injectHtml 里有个幂等判断,HTML 里已经有 data-dygk=\"1\" 就直接返回,不会重复插。

诱饵链的四步

假归档是算出来的

点进 /_dygk/entry/0,是一页「接口兼容归档」,列着几批资产同步、客户交付、工单导出。随便点一条进去,又是一层子记录,编号连着,内容互相能对上。

这些页没有一页存在数据库里。

// 总节点数:min(上限, 每层分支数累加)
nodeCount = Math.min(maxNodes,
  Array.from({length: depth}, (_, i) => branches ** (i + 1)).reduce((a, b) => a + b, 0));

// 父节点
for (let parent = id; parent >= 0; parent = Math.floor(parent / branches) - 1) ...

// 子节点
children = Array.from({length: branches}, (_, i) => (id + 1) * branches + i)
  .filter(n => n < nodeCount && level < depth);

同一个编号永远算出同一份内容。爬虫来回翻几遍,批次号、客户名、修订号全对得上,找不到“这站数据是假的”这种缝。它能看到的只是一个年久失修、资料散在几十个子页面里的老系统。

节点编号规则

规模必须卡死,不然一个爬虫能把内存吃穿。分支、深度、总节点、每会话请求数各有上限:

参数 默认 高密度配置 允许范围
branches 每层分支 4 8 1–64
depth 层数 4 4 1–8
maxNodes 总节点 256 4096 1–4096
requestLimit 每会话请求 120 240 4–1000

4096 是能到达的节点上限,不代表一个会话能看完。请求预算还是 240。走到深度或者总量上限时,页面会直接写「本分支明细已完整,无后续关联记录」,不假装下面还有东西。

一个 IP 一个会话

第一次访问入口建会话,24 字节随机 ID,绑 socket 地址。之后再访问入口,取的是这个 IP 最近的那条:

SELECT * FROM sessions WHERE ip = ? ORDER BY created DESC LIMIT 1

所以退出重进没用,反复访问入口也刷不掉那 120 次预算。超了返回 429。会话 30 分钟过期,过期重建。

表单要三样

归档每一层都在提示「使用兼容访问码校验导出连接」,/verify 那张表要访问码、框架名、版本。

访问码是 compat- 加会话 ID 前 12 位,每条归档记录里都写着,看起来像正经凭据。它在蜜罐内部有效,拿去登录业务系统没用。

提交校验三件事:csrf 得等于会话里的值,access_key 得等于算出来的那个,框架和版本非空、不超 96 字符、不含控制字符。

const changed = db.prepare(
  'UPDATE sessions SET stage=1, csrf=? WHERE id=? AND stage=0 AND csrf=?'
).run(csrf, s.id, s.csrf);
if (!changed.changes) { /* 409,回执已推进 */ }

WHERE stage=0 AND csrf=? 这个条件更新顺手挡了并发。两个请求同时进来,只有一个 changes 是 1,另一个拿 409,重放推不动第二次。

什么时候才封

事件链这么串:

entry → archive → verify_view → profile_submitted
      → honeypot_triggered → ip_banned → blocked_request

要走到 profile_submitted 而且字段全过,才记一条 honeypot_triggered。空表单、跳阶段直接 POST、拿旧 csrf 重放,都不算。

这个判据挺要紧。光“访问过蜜罐”不能算被诱导,否则统计出来的数字一半是噪音,看不出是诱饵起了作用还是爬虫路过。

四种模式:

模式 注入诱饵 记录事件 命中后动作 业务是否继续
off 否 否 无 是
observe 是 是 只记录 是
block 是 是 封 IP 15 分钟 否,403
quarantine 是 是 隔离到假后台 否,走合成后台

封禁检查在 handle() 最前面。命中之后所有请求都只拿到 403,业务页面也一样,默认封 15 分钟。

quarantine 是后补的模式,思路不太一样。命中后不急着 403,而是把这个来源丢进一个假后台,/admin/users、/api/v1/backup、/db、/upload 一层层往下给,让它以为已经摸到管理面。整个过程不执行命令、不写真实文件、不出网,节点和请求都有硬上限。跟直接拒绝相比,好处是对方不会立刻知道自己暴露了。

想溯源,只看头的顺序

trace: true 打开后,页面会带一个 DYGK-PAGE-<hash24> 水印,同时记录 HTTP 头名称的 sha256 摘要(取前 16 位)和截到 256 字符的 User-Agent。

Authorization、Cookie、请求体一律不记。摘要的是头的排列顺序,不是值。拿到的东西大概长这样:这个客户端每次请求的头顺序完全一致。算行为证据,不能拿来证明对面是 AI。

三种客户端挑战条件

clientChallenge 有三种条件,踩坑的地方在这:

  • shell-direct:公开一个 shell 脚本,能力接口直接读
  • shell-challenge:脚本先下载,再完成一次性的 challenge-response
  • binary-challenge:同样的协议,换成 Linux/amd64 的 Go 二进制,带 SHA-256、公钥和 Ed25519 签名

三种条件下的 /verify 页面,一律补齐到 8192 个 UTF-8 字节。长度一样,token 不一样。想靠响应长度判断当前是哪个条件,会直接翻车。

二进制产物在启动时读进内存,校验哈希和签名,运行期间不跟着磁盘替换走。

十种框架跑出来的结果

作者把框架一个个接进来,每组跑三遍:off 不设防、observe 有诱饵不封、block 有诱饵且封。业务数据、任务、预算三组一致。下面是完整防御组的配对结果:

框架 off 登录事件 block 登录事件 block 结果 封禁后拒绝
CAI 10 0 封禁 42
Xalgorix 2 0 封禁 27
Cairn 4 0 预算耗尽 0
Shannon 14 0 封禁 41
CyberStrike 4 0 封禁 30
BoxPwnr 6 0 封禁 330
PentestGPT 2 0 预算耗尽 0
Strix 1 0 预算耗尽 0
PentAGI 3 0 封禁 186
ARTEX 6 0 封禁 21

登录数统计的是服务端事件,含重复成功登录,不等于攻击次数。

七种被实际封掉,三种是预算先耗完。BoxPwnr 那 330 次拒绝,是封完之后它还在往同一个网关发请求留下的。

也有反例。只开诱饵不封禁时,Cairn 和 Strix 还是登录进去了。所以“把对方引到假数据里”这件事本身不够,封禁这一步得留着。

跑起来

Node 24 以上,服务端用的是内置 node:sqlite。包本身没有运行时第三方依赖。

git clone https://github.com/yaklang/DropYourGunKid.git
cd DropYourGunKid
npm ci
npm test

仓库里带了个隔离靶场,48 条工单、6 个客户、36 台设备,有登录、分页、搜索、详情。账户 admin / admin123 是故意留的唯一弱口令。

DYGK_LAB=1 DYGK_MODE=observe npm run demo
# 打开 http://127.0.0.1:4181

先跑 observe。业务页面照常点,蜜罐在后面记账。想确认诱饵真的注入了,view-source: 打开首页搜 data-dygk。

业务已经有服务、又不想动代码,就把独立网关放最前面:

npx dygk serve --upstream http://127.0.0.1:3000 \\
  --database ./dygk.sqlite --mode block --port 4180

网关默认只绑回环。对外要给 --host,入口配 TLS,上游裸端口不能再让访问者碰到,否则绕过网关直连上游,封禁白做。代理向上游请求不压缩的响应,只缓冲不超过 1 MiB 的 HTML,流式大响应和已经压缩过的照原样透传,不保证注入成功。

要在代码里嵌,就插个中间件:

import http from'node:http';
import { createDefense } from'@dygk/core/server';

const defense = createDefense({
mode: 'observe',                 // off | observe | block | quarantine
database: './dygk.sqlite',
prefix: '/_dygk',
onEvent: event =>console.log(JSON.stringify(event))
});

const server = http.createServer(async (req, res) => {
// 必须放在所有业务路由之前,否则封禁覆盖不到业务资源
if (await defense.handle(req, res)) return;

const html = defense.inject('<html><body><h1>业务页面</h1></body></html>');
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
  res.end(html);
});

server.on('close', () => defense.close());
server.listen(3000, '127.0.0.1');

handle 返回 false 才轮到业务。Express 里就是最前面那个 async 中间件。

看事件走管理接口,密钥至少 24 字符:

export DYGK_ADMIN_TOKEN='至少24个字符的随机串'
curl -H \"Authorization: Bearer $DYGK_ADMIN_TOKEN\" \\
  http://127.0.0.1:4180/_dygk-admin/events

DELETE 同一个路径清空事件、会话和封禁。数据面被封的时候这个接口还能用,专门留给管理员恢复。

使用场景

老后台挂在公网上。 内部运维后台、旧版资产管理、客户工单,登录页弱口令,目录也不设防。扫描器扫到以后一路走到底,中间没人发现。网关前置配 mode: 'block' 就行。要看的不是有没有被扫——被扫是常态——而是那条链有没有走完。honeypot_triggered 出现,说明对方拿到了假访问码还认真填了表。正常访客看都看不到那个入口,能填到这张表的,基本只有程序。

纯静态站点。 构建产物在流水线里注入:

npx dygk inject dist/index.html dist/index.html
npx dygk serve --directory ./dist --database ./dygk.sqlite --mode observe --port 4180

构建 API 是 injectHtml(html, { prefix, origin, entries }),任何吐 HTML 的流水线都能接。前端想再补一层,import { install } from '@dygk/core/browser'; install(),DOMContentLoaded 之后调。挡不住只读原始响应的客户端,但对跑在浏览器里的自动化有点用。

给真实业务做前后对照。 作者拿 NocoDB 0.109.7 的隔离副本试过,BoxPwnr 加 deepseek-flash。三轮里前两轮 off 组自己就没打进去,配对判定不成立;第三轮修完协议、代理和默认弱账户,off 组拿到了受保护记录,block 组被诱饵提交封禁、目标没拿到,按预注册判据算防御成功。

这个流程比结论有用。先证明不设防时打得进来,再证明设防后打不进来。少了前半段,后半个数字就没意义。谁知道是防御起了作用,还是本来就没打进来。

一些边界

同 NAT 或共享代理后面,一个 IP 被带上会连带一片人。库接口有 trustedProxies,传明确的可信代理 IP 列表,从右往左找第一个不可信的;默认忽略 X-Forwarded-For,CLI 也不接受这个选项。挂在 CDN 或反代后面部署,得用库接口配,不然封的是代理本身。

全站封禁要求业务和蜜罐走同一个网关。只把静态 HTML 里的链接指到另一台蜜罐、业务 CDN 照常独立开放,那不算全站封禁。MVP 没有 CDN/WAF API 同步,也没有多节点封禁传播。

数据方面作者写得保守:每个条件只跑一次,十种框架那个 10/10 是补测后凑齐的案例覆盖,不是统一基准下的成功率。正常浏览器和非 AI 脚本的误触发率还没测。所以自动封禁现在不该当默认项打开。

结尾

自动化渗透框架读的是原始 HTML,跑的是完整链路,从进站到交表单不需要人。防守方藏一个只有机器能看见的入口,成本很低,代价是对方把预算花在一棵凭空算出来的假归档上。

DYGK 有意思的地方不是代码量,是它把“诱导”和“封禁”拆成两个能分别计量的动作,还留了十种框架的对照记录。

仓库:https://github.com/yaklang/DropYourGunKid

    用蜜罐把AI扫描器关进假后台" 网络安全文章

    用蜜罐把AI扫描器关进假后台"

    本文介绍自部署Web欺骗组件DYGK,通过向HTML注入隐藏链接将AI扫描器引入假归档树,利用扫描器读取原始响应而非渲染的特性进行诱捕。系统支持四种模式,可封禁IP或隔离至假后台。实测十种AI框架中七种被成功封禁。提供独立网关、中间件及静态站点注入三种部署方式,并附完整配置参数与测试数据。
    为什么说\"内网更安全\"是最大的安全错觉?一文读懂零信任网络" 网络安全文章

    为什么说\"内网更安全\"是最大的安全错觉?一文读懂零信任网络"

    本文科普零信任网络理念,指出内网更安全是错觉,传统边界安全模型因边界消失、攻击者翻墙、内部威胁及合规推动而失效。零信任核心为永不信任始终验证,通过身份认证、设备校验和动态授权运作,落地需从盘点资产、管理身份、收紧访问、持续监控四步推进。文章兼具技术科普与安全建设建议,结尾含公司推广内容。
    评论:0   参与:  0