ARTEX 无扫描器资产收集方法
资产是”副产品”,不是”前置阶段”
传统渗透的资产收集是一个独立的前置阶段:
subfinder -d target.com → httpx → naabu → nmap → 得到资产清单 → 开始打ARTEX 完全反过来:
planner 派一条意图("摸清某子系统") → worker 打的过程中顺手发现资产 → insert_assets 记进图 → enrich 异步补全(DNS/HTTP) → 图变大 → planner 看到新节点 → 派新意图资产收集不是一个阶段,是探索过程的沉淀物。 这解释了为什么它不需要 subfinder —— subfinder 是”一次性把子域全捞出来”的工具,而 ARTEX 是”打到哪收到哪,边收边扩”。
这个循环靠的是图的反馈:insert_assets 写资产 → SetOwnerNode(intent.ID) 记下哪条意图发现的 → enrich 补全 → planner 的 graph_overview 里资产数变多 → 下一轮规划自然看到新空间。
1. 五条资产管道
管道一:外部平台同步(ScopeSentry)
server/sync_scopesentry.go
ScopeSentry 是一个 ASM(攻击面管理)平台。ARTEX 通过它的 MCP 接口按「项目」或「任务」两个维度拉数据:
// 数据源本身是一个名为 "ScopeSentry" 的 http 传输 MCP 行// (url + X-API-Key 头保存在 mcp_servers)拉回来的东西映射成 6 种资产类型:域 / 子域 / IP / 端口 / 站点 / 端点。
关键设计:这里用 mcphttp.Client.Call 直接调用,不触发 AskUser 权限弹窗 —— 因为这是后台批量同步,不该一条条弹窗确认。
还有个保护:syncMaxPerType = 5000,单目标单类型最多入 5000 条,防止误配把库撑爆。
这是”不装 subfinder”的第一个答案:让别人替你扫,你只消费结果。 ScopeSentry 本身就是干资产测绘的,重复造轮子没意义。
管道二:在线公开数据源(agent 自己 curl API)
这是最反直觉的一环 —— 没有 subfinder,但 agent 会直接 curl 打各种公开数据源的 API:
| 数据源 | 次数 | 能拿到什么 |
|---|---|---|
| ICP 备案查询 | 13 | 按备案号反查同主体所有域名 |
| crt.sh(证书透明度日志) | 11 | 历史上所有签发过证书的子域 |
| subcenter / subdomain.center | 3 | 在线子域查询 |
| web.archive.org | 3 | 历史快照里的旧域名/旧接口 |
| hackertarget | 2 | 在线子域/反查 |
| urlscan.io | 2 | 别人扫过的页面里的资源域名 |
| bing / google | 2 | site: 语法搜子域 |
| rapiddns | 1 | 被动 DNS 数据集 |
| alienvault OTX | 1 | 威胁情报里的关联域名 |
| github 搜索 | 1 | 代码里泄露的内部域名 |
crt.sh 的真实用法:
curl -s --max-time 30 "https://crt.sh/?q=%25.<domain>&output=json" -o crtsh.jsonwc -c crtsh.json%25 是 % 的 URL 编码,%.domain 就是通配符查询 —— 一次拿到该域所有历史证书里的子域名。
从文件命名就能看出手法:
subs_crtsh.txt ← crt.sh 拿的subs_ht.txt ← hackertarget 拿的subs_subcenter.txt ← subcenter 拿的subs_cs.txt ← 另一个源ct_subs.txt ← 证书透明度linked_subs.txt ← 关联域名推导all_subs.txt ← 合并去重后的总表这是 subfinder 的替代方案,而且更灵活:subfinder 内置几十个源的 SDK,规则固定;agent 直接 curl 每个源的 API,返回什么格式就现场解析什么格式。用 11 次 crt.sh、13 次 ICP 查询,覆盖了 subfinder 最核心的两个源。
管道三:主动 DNS 枚举
258 条命令带 for 循环 + dig/host/getent,典型形态:
for h in <sub1>.<target> <sub2>.<target> portal-service.<sub>.<target> uniapp.<sub>.<target>; do a=$(dig +short A "$h" | grep -E '^[0-9]' | tr '\n' ',') c=$(dig +short CNAME "$h" | grep -E '\.' | tr '\n' ',') echo "$h -> A=[$a] CNAME=[$c]"done注意四个细节:
- A 和 CNAME 分开取。CNAME 指向 CDN 时,A 记录是 CDN 的 IP(无价值),但 CNAME 能暴露真实源站(比如指向
xxx.oss-cn-hangzhou.aliyuncs.com就说明用的是阿里云 OSS)。 grep -E '^[0-9]'过滤。dig +short有时返回空行或注释,不过滤会污染结果。tr '\n' ','压成一行。多 IP 合成1.2.3.4,5.6.7.8,方便后续处理。- 通配符检测先行(关键):
for n in randomxyzabc123.<domain> thisdoesnotexist98765.<domain>; do dig +short A "$n"done如果随机字符串也解析出 IP,说明配了泛解析 —— 此时所有”子域存在”的结论都不可信,必须靠响应内容差异(title、大小)而不是 DNS 是否解析来判断。
还有指定 resolver 的进阶用法:
dig +short A "$d" @<ip>用目标自己的 DNS 服务器查,能看到外部视角看不到的内网记录。
管道四:被动提取(从已有材料里抠)
这是产出最多的一条,全部次数:
| 手法 | 次数 | 说明 |
|---|---|---|
| 从 HTML/JS 提取域名 | 261 | 页面里的外链、API 地址、跳转 |
| favicon 抓取 | 108 | 抓 /favicon.ico 做系统指纹 |
| 从 JS bundle 找接口 | 96 | SPA 的真实 API 藏在 bundle 里 |
| 提取 API 路径 | 100 | /api/... 正则批量抠 |
| 证书 SAN | 52 | 一张证书常绑多个域名 |
favicon 指纹是最巧的一招。agent 会自己写 Python 算 mmh3:
Tips — 为什么要用favicon 这个文件虽然小,但往往具有“出厂默认、长期不变、可被哈希”的特性。很多软件(如 CMS、网络设备管理界面、Web 服务器)会自带一个固定的 favicon.ico 文件,只要计算它的哈希值并与已知数据库比对,就能推测目标运行的是什么系统。
import hashlib, struct, base64, mmh3data = open('well.ico','rb').read()print("mmh3:", mmh3.hash(data))b = base64.encodebytes(data) # 很多平台的 favicon hash 是对 base64 算的为什么有用:同一套系统(同一套 OA、同一个厂商的网关)在不同域名下 favicon 完全相同,mmh3 hash 也相同。所以拿到一个 hash 就能反向关联「哪些域名跑的是同一套系统」,还能去 FOFA/Shodan 用这个 hash 找同类目标。
DB 里 assets.favicon_mmh3 字段就是存这个 —— 但ARTEX 自己不计算它,是 agent 算好填进去的,或者从 ScopeSentry 同步时拿过来(FaviconMMH3: it.Icon)。
JS bundle 提取是另一条主力。SPA 用 curl 只能拿到空壳 <div id="app">,真实接口全在 bundle 里:
grep -rhoaE '.{0,120}("/api[^"]+").{0,200}' OUTDIR/js/*.js | head -20技能 api-recon 为此专门写了 harvest_static.py,并且明令禁止直接 Read 大 bundle:
100KB 的
index-*.js禁止 Read/grep 进上下文;用 OUTDIR 脚本批处理
管道五:写入 + 异步补全(enrich)
前四条管道产出的原始线索,通过 insert_assets 工具写进资产表:
// 一次可混合多种类型"各类型必填字段:root_domain→domain;ip→ip;subdomain→domain; app→app_name;service(HTTP)→url;service(非HTTP)→service_name+port; endpoint→url+method"写入时 task_id 不暴露给模型 —— 由程序经 SetTaskID 权威赋值。防止模型伪造归属。
写入后触发 enrich(SetEnrich(enr))异步补全:
// DNS 无门禁;HTTP 探测受 RoE 约束(§5.2)func (e *Engine) doDNS(id int64, host string) { data, _ := e.resolv.QueryMultiple(host) // dnsx 库 for _, ip := range data.A { UpsertIP(...) } // 建 IP 节点 for _, a := range data.A { UpsertSubdomain(...) } // 建 A 记录 for _, aaaa := range data.AAAA { ... } // AAAA for _, cname := range data.CNAME { ... } // CNAME}
func (e *Engine) doHTTP(id int64, rawURL string) { resp, _ := e.client.Do(req) // UA: artex-enrich/1.0 body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 硬上限 1 MiB UpsertHTTPService(URL, StatusCode, ContentLength, PageTitle)}三个设计细节:
LimitReader 1 MiB:探测只读前 1MB,防止撞上大文件把内存打爆- 冷却窗口:同一
(kind, id)在cooldown内不重复跑,防止 planner 反复唤醒导致重复探测 - HTTP 探测受 RoE 门禁:DNS 解析无害所以无门禁,但 HTTP 请求会打到目标,必须受授权范围约束
enrich 只做补全,不做发现。它不会枚举子域,只对已经写进库的资产做解析和探测。发现靠前四条管道。
2. 数据模型:去重靠 DB,不靠代码
6 种资产类型,每类都有唯一索引(db/schema.sql):
| 类型 | 唯一索引 | 含义 |
|---|---|---|
root_domain | uq_av2_root_domain (domain) | 根域 |
ip | uq_av2_ip (ip) | IP |
subdomain | uq_av2_subdomain (domain, record_type) | 子域(按记录类型分) |
app | uq_av2_app_bundle (bundle_id) / uq_av2_app_name | 应用 |
service | uq_av2_service_http (url) / uq_av2_service_other (domain,ip,port,service_name) | 服务 |
endpoint | uq_av2_endpoint (url, method) | 接口 |
靠唯一索引 + ON CONFLICT DO UPDATE 做幂等,不在 Go 代码里写”查一下存不存在”的逻辑。这样并发写也不会重复。
ASSETS 表有 21 个业务字段,几个值得注意的:
c_segment CIDR+ GIST 索引 → 支持网段归属查询open_ports JSONB[]→ 一个 IP 的多个端口bound_domains TEXT[]→ IP 反查绑定的域名technologies TEXT[]+ GIN 索引 → 按技术栈搜资产icp→ 按备案号关联同主体资产
运行时实际数据:
| type | 数量 |
|---|---|
| endpoint | 160 |
| subdomain | 116 |
| ip | 54 |
| service | 41 |
| app | 15 |
| root_domain | 5 |
3. 归属与范围:三套机制
机制一:company_scope(企业资产范围)
支持 5 种规则(db/company_scope.go):
| kind | 匹配方式 |
|---|---|
domain | 注册域(用 publicsuffix 精确取 registrable domain) |
ip | 单 IP(归一成 /32 或 /128) |
cidr | 网段(inet_ops GIST 匹配) |
icp | 备案号(去所有 Unicode 空白 + 折叠大小写,不做模糊匹配) |
keyword | 关键词 |
ICP 这条最聪明:一个单位名下可能几十个域名,全都共用一个备案号。按备案号匹配,等于一次锁定该主体的所有资产,比一个个加域名省事得多。
company_source 字段区分两种归属来源:
explicit—— 人手动指定的企业scope—— 由范围规则自动推导
这个区分很重要:重建 scope 时不能把历史手工关联抹掉,所以旧数据保守地全部标成 explicit。
机制二:task_scope(任务测试范围)
task_scope 表 129 条记录,表示「这个任务允许测哪些资产」。它是覆盖度的分母:
add_task_scope工具:planner 主动把整根域/子公司/子域/IP 纳入范围list_untested_assets工具:查「范围内还没测的资产」worker处理意图时,自动把意图锚定的资产加入 scope(AddAutoScope,ON CONFLICT DO NOTHING保证幂等)
这形成了覆盖度的闭环:范围是分母,已测是分子,planner 看得到缺口,据此派新意图。
机制三:资产 DSL 查询
db/asset_dsl.go 实现了一个类 Google 语法的查询语言:
domain=example.com AND status_code=200technologies~=nginx OR page_title=后台支持字段:domain / root_domain / ip / url / page_title / icp / service_name / app_name / bundle_id 等,运算符有 = == != > >= < <=,逻辑组合 AND / OR。
planner 用 list_assets 工具按条件检索整库,比如「所有 status_code=200 且 technologies 含 nginx 的站点」。
4. 实测缺口
缺口一:IP 一个都没归属
ip | 无归属 | 54 ← 全部理由是 company_scope 只有 3 条规则,全是 domain,没有 cidr/ip 规则。于是:
- 子域靠域名后缀匹配上了(116/116 归属)
- IP 没有任何规则能匹配,全部悬空
影响:list_untested_assets 按企业维度统计覆盖度时,54 个 IP 不在任何企业的分母里 —— 端口扫描结果实际上没有进入覆盖度考核。
修法:给两家公司各加一条 cidr 规则(把已知 IP 段纳入),或者让 enrich 在建 IP 节点时,从 bound_domains 反推归属企业。
缺口二:root_domain 3/5 无归属
5 个根域里 3 个没归属,同样是 scope 规则覆盖不全。
缺口三:资产”来源”信息不足
assets 表没有”发现来源”字段(是 crt.sh 来的?JS 里抠的?还是 ScopeSentry 同步的?)。只有 task_ids 和 company_source。
影响:无法回答「哪条管道产出的资产质量最高」,也就无法优化 recon 策略。
5. 为什么这套方法能work
5.1 用在线数据源替代本地工具
| subfinder 做的事 | ARTEX 的替代 |
|---|---|
| 聚合 30+ 被动源 | curl 打 crt.sh / ICP / hackertarget / OTX / urlscan |
| 统一输出 | 每源单独解析,落 subs_<源>.txt |
| 内置去重 | sort -u + DB 唯一索引 |
优势:源可以随时换、随时加,不用等工具更新。某个源挂了,换一个就是。而且 agent 能根据返回内容做语义判断(“这个子域叫 test- 开头,可能是测试环境,优先打”),工具做不到。
5.2 用图驱动替代清单驱动
subfinder 的输出是一份静态清单,扫完就完了。
ARTEX 的资产在图里,和 intent/fact/finding 连在一起。所以:
- 发现一个新子域 → 可能触发 planner 派新意图
- 某个意图打到一组接口 →
insert_assets记下来 → 覆盖度分母变大 - 覆盖度有缺口 →
list_untested_assets提示 → planner 补测
资产不是终点,是探索空间的度量。
5.3 用 DB 约束替代去重代码
7 个唯一索引 + ON CONFLICT DO UPDATE,让多条管道并发写同一个资产也不会重复。这比在每个管道里写”先查再插”可靠得多。
5.4 用异步补全替代批量探测
enrich 是异步 worker 池,一个资产写完立即返回,解析和探测在后台做。
好处:worker 不用等 DNS 解析完才能继续;而且同一资产被多次写入时,冷却窗口能挡住重复探测。
总结
- 资产来源:外部平台同步(ScopeSentry)+ 在线数据源 curl(crt.sh/ICP/hackertarget)+ 主动 dig 枚举 + 从页面/JS/证书/favicon 里抠
- 资产落点:Postgres 的
assets表,6 类 + 21 字段 + 7 个唯一索引 - 资产补全:
enrich异步跑 DNS(dnsx)和 HTTP(限 1MB) - 资产用途:作覆盖度分母,驱动 planner 下一步派什么意图
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!