当前位置: 首页 > news >正文

反连平台设计详解:DNSLog + HTTP 回调 + 异步关联,从零实现漏洞扫描 OOB 检测基础设施

📋 目录

  • 背景介绍
  • 核心挑战
  • 解决方案
  • 完整实现
  • 测试策略
  • 最容易踩的5个坑
  • 面试高频考点
  • 总结与扩展

背景介绍

漏洞扫描中有一类问题无法只靠同步响应判断。

比如:

  • 目标服务器收到测试请求后,会在后台访问一个外部地址
  • 目标应用触发了异步任务,几秒后才产生网络访问
  • 目标环境没有把执行结果回显到 HTTP 响应里

这种场景需要反连平台(Reverse Connection Platform),也常被称为 OOB(Out-of-Band)检测基础设施。

它的核心思想是:

扫描器生成唯一回调地址 ↓ 检测插件把回调地址放入测试请求 ↓ 目标如果触发外联,会访问该地址 ↓ 反连平台记录回调 ↓ 扫描器根据唯一ID关联检测结果

xray 的 README 中也明确提到,XXE、SSRF 等检测模块会使用反连平台能力。对扫描器来说,反连平台不是单个插件,而是所有“无回显检测”的公共基础设施。


核心挑战

挑战1:如何生成唯一且可追踪的回调地址?

每一次检测都要有独立标识,否则多个插件、多个目标、多个任务之间会串结果。

扫描任务 A ├── 插件 ssrf │ └── token: a1b2c3 └── 插件 xxe └── token: d4e5f6 扫描任务 B └── 插件 ssrf └── token: g7h8i9

反连平台必须知道:

  • token 属于哪个扫描任务
  • token 属于哪个插件
  • token 绑定哪个目标
  • token 的有效期是什么

挑战2:HTTP 和 DNS 回调要同时支持

不同漏洞触发的外联形式不同:

HTTP回调 ├── 能拿到完整路径 ├── 能拿到Header ├── 能记录User-Agent └── 适合验证Web访问类外联 DNS回调 ├── 更轻量 ├── 出网限制下更容易触发 ├── 只能拿到域名查询 └── 适合验证解析行为

如果只支持 HTTP,会漏掉只做 DNS 解析但不发 HTTP 请求的场景;如果只支持 DNS,又拿不到更丰富的访问证据。

挑战3:检测是异步的

扫描器发出请求后,回调可能在几十毫秒后出现,也可能几秒后出现,还可能永远不出现。

所以不能写成:

sendPayload()ifcallbackExists(){reportVuln()}

更合理的方式是:

发送测试请求 ↓ 注册等待器 ↓ 在超时时间内监听回调事件 ↓ 有事件则确认,无事件则正常结束

挑战4:反连平台自身要安全

反连平台暴露在公网,天然会收到大量不可控流量。

需要重点考虑:

  • token 猜测与伪造
  • 日志污染
  • 大请求体占用内存
  • 高并发打满连接数
  • 敏感信息落盘

反连平台不是越“开放”越好,而是要在可观测和可控之间取平衡。


解决方案

架构设计

┌──────────────────────┐ │ Scanner │ │ plugin / task / url │ └──────────┬───────────┘ │ create token ▼ ┌──────────────────────┐ │ Reverse Client │ │ URL builder / wait │ └──────────┬───────────┘ │ ▼ ┌────────────────────────────────────┐ │ Reverse Platform │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ HTTP Server │ │ DNS Server │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ └──────┬──────────┘ │ │ ▼ │ │ Event Store + Matcher │ └────────────────┬───────────────────┘ │ callback event ▼ ┌──────────────────────┐ │ Result Joiner │ │ token -> scan result │ └──────────────────────┘

核心数据流

1. 插件请求一个反连地址 2. 平台生成 token 与检测上下文 3. 插件把地址用于测试请求 4. HTTP/DNS 服务接收外部回调 5. 平台把回调写入事件存储 6. 扫描器等待 token 对应事件 7. 命中后生成漏洞结果和证据

完整实现

步骤1:Token 与上下文设计

pkg/reverse/token.go

packagereverseimport("crypto/rand""encoding/base32""strings""time")typeTokenContextstruct{TokenstringTaskIDstringPluginstringTargetstringCreatedAt time.Time ExpiresAt time.Time}funcNewTokenContext(taskID,plugin,targetstring,ttl time.Duration)(TokenContext,error){token,err:=randomToken(16)iferr!=nil{returnTokenContext{},err}now:=time.Now()returnTokenContext{Token:token,TaskID:taskID,Plugin:plugin,Target:target,CreatedAt:now,ExpiresAt:now.Add(ttl),},nil}funcrandomToken(sizeint)(string,error){buf:=make([]byte,size)if_,err:=rand.Read(buf);err!=nil{return"",err}encoded:=base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(buf)returnstrings.ToLower(encoded),nil}

Token 需要足够随机,避免被猜测;同时要适合作为子域名前缀,所以使用小写字母和数字更方便。

步骤2:回调地址生成器

pkg/reverse/client.go

packagereverseimport("fmt""time")typeClientstruct{BaseDomainstringHTTPBasestringStore*Store}typeCallbackAddressstruct{TokenstringDNSNamestringHTTPURLstring}func(c*Client)NewCallback(taskID,plugin,targetstring)(CallbackAddress,error){ctx,err:=NewTokenContext(taskID,plugin,target,5*time.Minute)iferr!=nil{returnCallbackAddress{},err}c.Store.SaveContext(ctx)returnCallbackAddress{Token:ctx.Token,DNSName:fmt.Sprintf("%s.%s",ctx.Token,c.BaseDomain),HTTPURL:fmt.Sprintf("%s/%s",c.HTTPBase,ctx.Token),},nil}

插件只需要拿到回调地址,不应该关心反连平台内部如何记录事件。

步骤3:事件存储

pkg/reverse/store.go

packagereverseimport("sync""time")typeCallbackEventstruct{TokenstringProtocolstringRemoteIPstringHoststringPathstringUserAgentstringRawstringCreatedAt time.Time}typeStorestruct{mu sync.RWMutex contextsmap[string]TokenContext eventsmap[string][]CallbackEvent waitersmap[string][]chanCallbackEvent}funcNewStore()*Store{return&Store{contexts:map[string]TokenContext{},events:map[string][]CallbackEvent{},waiters:map[string][]chanCallbackEvent{},}}func(s*Store)SaveContext(ctx TokenContext){s.mu.Lock()defers.mu.Unlock()s.contexts[ctx.Token]=ctx}func(s*Store)AddEvent(event CallbackEvent)bool{s.mu.Lock()defers.mu.Unlock()ctx,ok:=s.contexts[event.Token]if!ok||time.Now().After(ctx.ExpiresAt){returnfalse}s.events[event.Token]=append(s.events[event.Token],event)for_,waiter:=ranges.waiters[event.Token]{select{casewaiter<-event:default:}}returntrue}func(s*Store)Wait(tokenstring,timeout time.Duration)(CallbackEvent,bool){ch:=make(chanCallbackEvent,1)s.mu.Lock()iflist:=s.events[token];len(list)>0{event:=list[0]s.mu.Unlock()returnevent,true}s.waiters[token]=append(s.waiters[token],ch)s.mu.Unlock()timer:=time.NewTimer(timeout)defertimer.Stop()select{caseevent:=<-ch:returnevent,truecase<-timer.C:returnCallbackEvent{},false}}

这里的Wait支持两种情况:

  • 回调已经到达,直接返回
  • 回调尚未到达,挂起等待直到超时

步骤4:HTTP回调服务

pkg/reverse/http_server.go

packagereverseimport("io""net""net/http""strings""time")typeHTTPServerstruct{Store*Store}func(s*HTTPServer)ServeHTTP(w http.ResponseWriter,r*http.Request){token:=strings.Trim(strings.Split(r.URL.Path,"/")[1]," ")iftoken==""{http.NotFound(w,r)return}body,_:=io.ReadAll(io.LimitReader(r.Body,4096))event:=CallbackEvent{Token:token,Protocol:"http",RemoteIP:clientIP(r),Host:r.Host,Path:r.URL.String(),UserAgent:r.UserAgent(),Raw:string(body),CreatedAt:time.Now(),}if!s.Store.AddEvent(event){http.NotFound(w,r)return}w.WriteHeader(http.StatusOK)_,_=w.Write([]byte("ok"))}funcclientIP(r*http.Request)string{host,_,err:=net.SplitHostPort(r.RemoteAddr)iferr!=nil{returnr.RemoteAddr}returnhost}

注意点:

  • 请求体必须限制大小
  • 未知 token 不返回详细错误
  • 只记录必要证据
  • 不把任意请求内容直接渲染到页面

步骤5:DNS回调服务

pkg/reverse/dns_server.go

packagereverseimport("net""strings""time""github.com/miekg/dns")typeDNSServerstruct{BaseDomainstringStore*Store}func(s*DNSServer)HandleDNS(w dns.ResponseWriter,r*dns.Msg){msg:=new(dns.Msg)msg.SetReply(r)for_,q:=ranger.Question{token:=s.extractToken(q.Name)iftoken==""{continue}s.Store.AddEvent(CallbackEvent{Token:token,Protocol:"dns",RemoteIP:w.RemoteAddr().String(),Host:q.Name,Path:dns.TypeToString[q.Qtype],CreatedAt:time.Now(),})msg.Answer=append(msg.Answer,&dns.A{Hdr:dns.RR_Header{Name:q.Name,Rrtype:dns.TypeA,Class:dns.ClassINET,Ttl:1,},A:net.ParseIP("127.0.0.1"),})}_=w.WriteMsg(msg)}func(s*DNSServer)extractToken(namestring)string{name=strings.TrimSuffix(strings.ToLower(name),".")base:=strings.TrimSuffix(strings.ToLower(s.BaseDomain),".")if!strings.HasSuffix(name,base){return""}left:=strings.TrimSuffix(name[:len(name)-len(base)],".")parts:=strings.Split(left,".")iflen(parts)==0{return""}returnparts[0]}

真实实现中要补充:

  • DNS 服务器监听端口
  • 域名 NS 解析配置
  • IPv6、CNAME 等记录处理
  • 上游解析兜底策略

步骤6:检测插件中的等待逻辑

pkg/plugin/oob_check.go

packagepluginimport("context""time")funcRunOOBCheck(ctx context.Context,reverse*reverse.Client,targetstring)(*VulnResult,error){callback,err:=reverse.NewCallback("task-001","ssrf",target)iferr!=nil{returnnil,err}iferr:=sendSafeProbe(ctx,target,callback.HTTPURL);err!=nil{returnnil,err}event,ok:=reverse.Store.Wait(callback.Token,8*time.Second)if!ok{returnnil,nil}return&VulnResult{Target:target,Plugin:"ssrf",Evidence:event.Protocol+" callback from "+event.RemoteIP,},nil}

这里的sendSafeProbe代表插件发送的安全探测请求。文章重点是反连平台设计,不展开具体漏洞利用细节。


测试策略

单元测试:Token唯一性

funcTestTokenUnique(t*testing.T){seen:=map[string]struct{}{}fori:=0;i<10000;i++{token,err:=randomToken(16)require.NoError(t,err)_,exists:=seen[token]assert.False(t,exists)seen[token]=struct{}{}}}

单元测试:等待回调

funcTestWaitCallback(t*testing.T){store:=NewStore()ctx,_:=NewTokenContext("task","plugin","target",time.Minute)store.SaveContext(ctx)gofunc(){time.Sleep(50*time.Millisecond)store.AddEvent(CallbackEvent{Token:ctx.Token,Protocol:"http"})}()event,ok:=store.Wait(ctx.Token,time.Second)assert.True(t,ok)assert.Equal(t,"http",event.Protocol)}

安全测试:未知Token不记录

funcTestUnknownTokenIgnored(t*testing.T){store:=NewStore()ok:=store.AddEvent(CallbackEvent{Token:"unknown"})assert.False(t,ok)}

最容易踩的5个坑

坑1:Token太短导致串结果

错误示例

token:=strconv.Itoa(rand.Intn(10000))

正确做法

token,err:=randomToken(16)

Token 必须使用密码学安全随机数,不能使用时间戳、自增 ID 或普通随机数。

坑2:等待逻辑阻塞扫描主流程

错误示例

time.Sleep(30*time.Second)checkCallback()

正确做法

event,ok:=store.Wait(token,8*time.Second)

等待必须有超时,并且应以事件通知为主,不要靠固定 sleep。

坑3:没有设置Token过期

错误示例

contexts[token]=ctx

正确做法

ctx.ExpiresAt=time.Now().Add(5*time.Minute)

过期策略可以减少内存占用,也可以降低旧回调污染新扫描的风险。

坑4:把回调内容直接展示在报告中

错误示例

<pre>{{ .RawCallback }}</pre>

正确做法

safeEvidence:=html.EscapeString(limit(event.Raw,1024))

反连平台收到的数据不可信,进入报告前必须转义和限长。

坑5:DNS服务没有部署验证

错误示例

// 本地启动DNS服务后直接认为可用

正确做法

digtoken.example-oob.com @authoritative-dns

DNS 反连依赖域名 NS 配置,代码正确不代表公网解析链路正确。


面试高频考点

考点1:为什么反连平台能检测无回显漏洞?

回答要点

  1. 同步响应看不到结果时,观察目标是否产生外部网络行为
  2. 每次检测生成唯一 token
  3. 回调事件和扫描任务通过 token 关联
  4. HTTP 与 DNS 证据可以作为检测依据

考点2:HTTP回调和DNS回调有什么区别?

回答要点

  1. HTTP 证据更丰富,可以记录路径、Header、User-Agent
  2. DNS 更轻量,有些环境只会解析域名不发 HTTP 请求
  3. HTTP 依赖应用层访问,DNS 依赖解析链路
  4. 实际平台通常两者都支持

考点3:如何防止反连平台误报?

回答要点

  1. Token 必须唯一且不可猜测
  2. Token 绑定任务、插件和目标
  3. 设置合理过期时间
  4. 记录协议、来源 IP、时间等证据
  5. 必要时结合二次确认逻辑

总结与扩展

核心经验总结

  1. 反连平台是公共基础设施

    • 服务于 SSRF、XXE、无回显命令检测等场景
    • 插件只依赖回调地址和等待接口
    • 平台负责事件接收和关联
  2. 唯一Token是核心

    • 绑定扫描上下文
    • 防止串任务
    • 便于证据追踪
  3. HTTP和DNS要同时覆盖

    • HTTP 证据更完整
    • DNS 触达率更高
    • 两者互补
  4. 异步等待要可控

    • 事件通知优于固定 sleep
    • 每次等待必须有超时
    • 过期 token 要及时清理
  5. 平台自身要做安全防护

    • 限制请求体大小
    • 未知 token 静默处理
    • 报告展示前转义
    • 清理过期数据
http://www.jsqmd.com/news/1398942/

相关文章:

  • 如何避免zxpy脚本中的常见陷阱:安全与性能优化指南
  • Search-Engines-Scraper核心功能解密:结果去重、多格式导出与高级过滤技巧
  • 网盘下载慢怎么解决?这款免费开源的网盘直链解析工具一次搞定
  • 珠海斗门区房屋漏水维修靠谱商家推荐(2026新):精准测漏维修 - 超人防水
  • RAGMeUp架构详解:Python后端与React前端的无缝协作
  • 终极显存管理实战:RTX 5090 上 Qwen3-VL-32B INT8 ConvRot 模型部署与性能优化全攻略
  • 2026年8月无锡宜兴屋顶漏水维修哪家好?屋面防水科普指南 - 聪居到家
  • 实测Audio8-ASR-0.1B语音识别效果:WER低至2.7%的技术突破
  • useMergeRefs钩子:优化React组件性能的高级技巧
  • 三步搭建免费工业监控平台:Scada-LTS开源SCADA系统部署实战与避坑指南
  • 2026 年 8 月济南名包出手实测:古驰 Jackie1961 系列回收报价,剖析本地回收常见交易纠纷 - 品牌观测员
  • 终极指南:在iPhone上部署Audio8-ASR-0.1B,200MB内存实现本地语音转写
  • 清新房屋漏水维修靠谱商家推荐(2026新):精准测漏维修 - 超人防水
  • 如何在5分钟内上手u-dma-buf?从编译到设备创建的快速实践
  • DDRM核心功能解析:超分辨率、去模糊与降噪的终极解决方案
  • PyFlow开发者教程:从源码编译到自定义参数调优
  • 超宽温 PT1000 分度表 (-200℃~850℃)
  • GerberTools完整指南:如何快速搞定Gerber文件处理与拼板生产
  • 广州香奈儿二手包包变现科普,年份磨损瑕疵对报价的影响 - 资讯早知道
  • Portrait-Segmentation核心架构解密:Slim-net如何实现1.5MB模型20FPS实时推理
  • 如何利用DeepSeek Harness提升AI开发效率?5个实用技巧与最佳实践
  • Snipe-IT 快速上手:从 Docker 部署到第一台设备入库(约半小时)
  • 漏洞扫描报告生成系统设计:HTML 模板 + JSON 结构化 + 多格式导出,附 5 个安全坑点
  • 如何用 UModel 轻松提取虚幻引擎资源?一份实战上手指南
  • 用工风险管控系统哪家好?科学选购指南 - 汇聚至此
  • 2026年8月哈尔滨南岗区月嫂推荐 哪家照顾新生儿专业指南 - 起跑123
  • 告别各玩各的:Nucleus Co-op 让单机游戏轻松变身本地分屏,4人同屏一步到位
  • 告别逐首找歌词:ZonyLrcToolsX 命令行一键批量下载 LRC 歌词文件全攻略
  • 告别“资源黑盒“:这款 UE4 Pak 文件分析工具,让包体优化不再靠猜
  • 联想小新无声故障排查:从驱动更新到虚拟声卡冲突的完整解决方案