- 网络安全
- 应用安全
- 开发工具
【免费下载链接】zaproxy
The ZAP by Checkmarx Core project
ZAP(Zed Attack Proxy)核心项目在主动扫描器中内置了一个名为 Kb(Knowledgebase,知识库)的共享存储组件,用于在扫描过程中跨插件记录、共享与传递扫描结果。本文基于 ZAP 核心仓库(zaproxy)中的 docs/Kb.md 文档,结合 Kb.java 等源码实现,系统讲解 Kb 的两种存储作用域、全部公开 API、读写语义、在扫描器中的接入方式,以及当前的 URI 级使用实例,帮助读者理解 ZAP 插件间依赖协作的底层机制,并掌握在自己的扫描插件中使用 Kb 的完整方法。
一、Kb 是什么:扫描过程中的“插件间共享结果池”
ZAP 的主动扫描由多个扫描插件(Plugin)协作完成,单个插件往往只负责探测某类单一漏洞。实际场景中经常出现“依赖关系”:例如先由某个插件探测出后端数据库类型,后续插件才能针对该数据库类型实施精准的注入探测。为此,ZAP 在org.parosproxy.paros.core.scanner包中提供了 Kb 知识库组件。
Kb 的源码 Javadoc 明确了它的设计定位:
Knowledge base records the properties or result found during a scan. It is mainly used to share result among plugin when dependency arise.
也就是说,Kb 的作用是记录扫描过程中发现的性质与结果,并在存在依赖关系的插件之间共享这些结果。它不是一个持久化数据库,也不属于会话数据,而是与一次主机扫描(HostProcess)生命周期绑定的内存缓存。
两种存储作用域(关键设计)
Kb 内部用两个TreeMap分别维护两类数据(见 Kb.java):
| 作用域 | 存储结构 | 适用场景 |
|---|---|---|
| Host 级(全局) | TreeMap<String, Object> mapKb | key = 名称,value = 结果,对整个主机(Host)全局有效 |
| URI 级(按路径) | TreeMap<String, TreeMap<String, Object>> mapURI | key = URL(去掉 query 的路径)+ 名称,value = 结果,仅对特定路径有效 |
这一设计的意图在于:某些扫描结论是针对整个站点/主机的(如“后端是 MySQL”),而另一些结论则只对某个具体路径成立(如“/admin/login存在 SQL 注入”),两种信息需要区分存储,避免全局结论被局部数据污染。
二、Kb 的完整 API 与读写语义
Kb 类(Kb.java)是线程安全的(所有公开读写方法均为synchronized),所有 API 都以“Host 级 / URI 级”成对出现。
Host 级 API(key = 名称)
| 方法 | 语义 |
|---|---|
add(String key, Object value) | 向知识库写入一个键值对;只有唯一值才会被加入(重复值自动去重) |
get(String key) | 获取该 key 对应列表中的第一个值;不存在或列表为空返回null |
getList(String key) | 获取该 key 对应的全部值列表(Vector<Object>);无历史值返回null |
getString(String key) | 以String形式获取第一个值;若值不是String或不存在返回null |
getBoolean(String key) | 以boolean形式获取第一个值;若值不是Boolean返回false(注意:与getString的返回语义不同) |
URI 级 API(key = URL + 名称)
| 方法 | 语义 |
|---|---|
add(URI uri, String key, Object value) | 为指定 URI 写入键值对,同样只保留唯一值 |
get(URI uri, String key) | 获取该 URI 下该 key 的第一个值 |
getList(URI uri, String key) | 获取该 URI 下该 key 的全部值列表 |
getString(URI uri, String key) | 获取该 URI 下该 key 的第一个值并转String |
getBoolean(URI uri, String key) | 获取该 URI 下该 key 的第一个值并转boolean |
三个必须注意的语义细节
- “add 一个值”实际上是“追加到一个列表”:底层
add(map, key, value)实现中,每个 key 对应一个Vector<Object>,新值追加到向量末尾;get系列方法只返回向量的第一个元素(Kb.java)。因此getList才是取回全部历史值的正确手段。 - 唯一性去重:
add在追加前会检查if (!v.contains(value)),保证同一 key 下不会出现重复值(Kb.java)。 - URI 级存储会剥离查询串:写入 URI 级数据时,源码会先
uri.clone(),再uri.setQuery(null)去除 query 部分,以“去掉 query 的路径”作为 key(Kb.java)。这意味着/path?a=1与/path?a=2会被视为同一个 URI 键;若 URI 不可克隆或去除 query 失败,写入会被静默丢弃并记录日志。
三、扫描器如何接入 Kb:AbstractPlugin 与 HostProcess
Kb 目前通过两个入口暴露给扫描插件使用(这一点在原文档中也有明确说明):
- AbstractPlugin.getKb():每个扫描插件都可直接调用
getKb()获取当前扫描的知识库引用,其实现为return getParent().getKb(),即委托给所属的 HostProcess。 - HostProcess.getKb():按需懒加载——首次访问时创建
new Kb(),之后复用同一个实例,并保证“永不返回 null”。
从源码结构可以推断出 Kb 的完整生命周期:每个 HostProcess 持有一个 Kb 实例(HostProcess.java 声明private Kb kb = null),而一次扫描中运行的所有插件共享该 HostProcess 的 Kb。因此:
- Kb 的共享范围是同一主机扫描内的全部插件,而不是全局的、跨扫描的;
- 不同主机(Host)的扫描拥有各自独立的 Kb,互不干扰;
- 扫描结束、HostProcess 释放后,Kb 中记录的数据随之失效。
在插件中使用 Kb 的典型写法
由于 Kb 同时支持“Host 级”与“URI 级”两种写入,一个插件内典型的数据交换流程如下:
// 写入:将扫描中发现的结论存入知识库(供后续依赖插件读取) Kb kb = getKb(); kb.add("some/host/level/key", discoveredValue); // Host 级,对整个主机生效 kb.add(msg.getRequestHeader().getURI(), "path/key", value); // URI 级,仅对该路径生效 // 读取:从知识库获取前置插件写入的结论 String rdbms = kb.getString("sql/rdbms_name"); if (rdbms != null) { // 已确认数据库类型,执行针对性的探测 } // 批量读取 java.util.Vector<Object> values = kb.getList("some/key");注意:Kb 的 key 当前采用“
命名空间/具体键名”的分层字符串约定(如sql/<rdbms_name>),写入与读取方必须约定一致的 key,才能完成跨插件协作。
四、当前 URI 级使用实例:SqlInjectionScanRule 与 sql/ 键
原文档记录的、当前代码库中唯一明确的 URI 级 Kb 使用场景来自org.zaproxy.zap.extension.ascanrules.SqlInjectionScanRule(SQL 注入扫描规则插件),它使用 key 跟踪:
sql/<rdbms_name>
即“sql/+ 具体数据库名(rdbms name)”形式的键。可以推断其协作模式为:某个探测插件在扫描到某一路径时,若识别出后端数据库类型(例如 MySQL、PostgreSQL、Oracle 等),就调用kb.add(uri, "sql/" + rdbmsName, ...)将结论按 URI 级写入;后续针对该路径做 SQL 注入检测的插件再通过kb.getString(uri, "sql/<rdbms_name>")读取,从而避免重复探测、并针对已知数据库类型选择更精准的注入语法。
需要说明的是:
SqlInjectionScanRule属于独立的 zap-extensions 仓库(ascanrules 扩展),在当前 zaproxy 核心仓库中并无其源码;本文所述仅为 docs/Kb.md 中记录的事实与其在核心 Kb 实现下的必然交互方式,属合理推断。
五、单元测试对 Kb 语义的验证
核心仓库的 KbUnitTest.java 直接印证了前文描述的 API 语义:
| 测试用例 | 验证的行为 |
|---|---|
shouldStoreValueForGivenKey | add(key, value)后get(key)能取回原值 |
shouldRetrieveStoredObjectsForGivenKey | 同一 key 写入两个不同对象,getList返回大小为 2 的列表且保持写入顺序(contains断言) |
shouldRetrieveStoredBooleanForGivenKey | 写入Boolean.TRUE后getBoolean返回true |
shouldRetrieveStoredStringForGivenKey | 写入字符串后getString返回原字符串 |
shouldReturnNullWhenGivenKeyHasNoStoredValue | 未写入的 key,get返回null |
shouldReturnFalseWhenRetrievingNonBooleanValueAsBoolean | 存入非 Boolean 对象后用getBoolean读取,返回false而非抛异常 |
shouldReturnNullWhenRetrievingNonStringValueAsString | 存入非 String 对象后用getString读取,返回null而非抛异常 |
值得注意的是,测试文件中 URI 级相关的 4 个用例(shouldStoreValueForGivenUriAndKey、shouldRetrieveStoredObjectsForGivenUriAndKey、shouldRetrieveStoredBooleanForGivenUriAndKey、shouldRetrieveStoredStringForGivenUriAndKey)目前均标注@Disabled(“Not yet implemented”),即 URI 级 API 在核心仓库中尚无自动化测试覆盖。这提示开发者:若要在 zap-extensions 中依赖 URI 级 Kb 行为,需要自行保证测试覆盖。
六、在扩展插件中正确使用 Kb 的实践建议
综合原文档、核心实现与测试,为在 ZAP 扫描扩展中使用 Kb 的开发实践给出以下要点:
- 明确作用域:整站结论用 Host 级(
add(String key, Object value)),局部结论用 URI 级(add(URI uri, String key, Object value)),不要混用,避免数据污染。 - 约定 key 命名空间:沿用以斜杠分层的命名空间(如
sql/<rdbms_name>),并在文档/代码注释中登记已使用的 key,防止不同插件间的 key 冲突。 - 写入时只写结论,读取时容忍缺失:读取方务必处理
null返回值——依赖的前置插件可能未运行或未写入对应 key。 - 注意类型语义:
getBoolean对非 Boolean 值返回false、getString对非 String 值返回null,读取前应确认写入值的类型。 - 记住 URI 级去 query 的键规约:按路径维度共享数据时,query 参数不同不影响键匹配;需要区分 query 的场景不应使用 Kb 的 URI 级存储。
- 共享范围即单次主机扫描:Kb 数据随 HostProcess 生命周期存续,跨扫描或跨主机需要的数据应改由 ZAP 会话或其他持久化机制承载。
七、小结
Kb 是 ZAP 主动扫描引擎中承担“插件间结果共享与依赖协作”职责的轻量级内存组件,其核心价值在于让不同扫描插件围绕同一主机/路径共享探测结论,从而减少重复探测、实现针对性的漏洞检测。其两级作用域(Host 级 / URI 级)、唯一值去重、getList/get的列表语义,以及“去 query 的 URI 键”等设计,均可在 Kb.java 与 KbUnitTest.java 中得到直接验证。开发者若需在扫描扩展中实现插件间数据依赖,可直接通过getKb()接入,并遵循本文的键规约与读写语义进行开发。
- 网络安全
- 应用安全
- 开发工具
【免费下载链接】zaproxy
The ZAP by Checkmarx Core project
相关推荐
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考