1. 项目概述:为什么我们需要扩展httpx
在安全研究、渗透测试或者日常的资产巡检工作中,httpx已经成为了一个绕不开的工具。它就像一个高效的“侦察兵”,能快速告诉我们一个URL是死是活,背后跑着什么服务器,用了什么技术栈。但干过这行的朋友都知道,标准化的侦察报告虽然全面,却不一定总能命中我们当下的特定需求。比如,我想在探测时顺便检查目标是否使用了某个特定的、有漏洞版本的JavaScript库;或者我想在返回结果里,自动加上这个域名在Shodan或Fofa上的最新开放端口信息;再或者,我想把httpx的输出格式,定制成能直接导入我们内部CMDB系统的样子。
这些需求,就是标准版httpx的“能力边界”。官方提供了丰富的flag,但不可能预见所有场景。这时候,自己动手写一个httpx扩展插件,就成了把通用工具变成专属神器的关键一步。这不仅仅是“会不会写代码”的问题,更是关于如何将自动化流程深度融入自己工作流的核心技能。今天,我就结合自己多次为httpx编写插件的经验,带你从零开始,理解其插件机制,并亲手构建一个能解决实际问题的扩展。
2. httpx插件机制深度解析
在动手写代码之前,我们必须先搞清楚httpx的“五脏六腑”,明白插件是如何嵌入到这个工具的生命周期中的。这能避免我们写出不兼容甚至导致崩溃的代码。
2.1 核心架构与插件切入点
httpx基于Go语言开发,其核心是一个高度并发的HTTP客户端引擎。它的执行流程可以简化为:读取输入 -> 预处理目标 -> 创建HTTP请求 -> 发送请求 -> 接收响应 -> 后处理响应 -> 输出结果。
插件主要在两个核心阶段介入:
- 请求前阶段:在HTTP请求真正发出之前。我们可以在这里修改请求头、请求体、URL,甚至根据条件跳过某些目标的探测。
- 响应后阶段:在收到HTTP响应之后,输出结果之前。这是我们最常使用的阶段,可以分析响应内容、提取新信息、修改输出结构,或者触发额外的动作(如调用外部API)。
httpx的插件本质上是实现了特定接口的Go包。官方虽然没有一个像Nuclei那样显式的“插件仓库”,但其代码结构是高度模块化的,为我们通过导入(import)和实现接口的方式来扩展功能留下了标准路径。
2.2 理解retryablehttp与上下文传递
httpx底层使用了retryablehttp库来处理HTTP请求的重试和稳健性。这意味着我们的插件在处理请求和响应时,必须考虑到“重试”这个上下文。例如,一个在第一次请求失败时触发的插件逻辑,在重试成功后可能就不应该再执行。
更重要的是上下文(Context)传递。Go的context.Context用于传递请求的截止时间、取消信号以及跨API和进程的请求作用域值。在编写插件时,任何可能阻塞的操作(如网络IO、复杂计算)都应该监听ctx.Done()通道,以确保当用户按下Ctrl+C时,插件能优雅退出,而不是卡死整个进程。
注意:一个常见的错误是直接在插件中执行一个没有超时控制的网络请求。如果目标服务无响应,你的插件会挂起,进而拖慢整个httpx的扫描队列。务必为所有外部调用设置合理的超时。
2.3 插件类型与选择
根据你的目标,插件大致可以分为两类:
- 输出处理器:这类插件工作在响应后阶段,专注于“消化”结果。例如,将httpx发现的特定资产自动添加到Jira工单,或者将结果与之前的扫描结果进行diff对比,找出新增资产。它不改变探测过程,只改变结果的去向和处理方式。
- 请求/响应修改器:这类插件能更深入地介入探测流程。
- 请求修改器:可以给所有请求自动添加一个特定的认证头(如
X-API-Key),或者将HTTP请求批量转换为HTTPS尝试。 - 响应分析器:在标准检测之外,对响应体进行深度正则匹配、提取所有JavaScript文件链接、计算页面特定元素的哈希等。
- 请求修改器:可以给所有请求自动添加一个特定的认证头(如
我们的第一个插件,将从最常见的“响应分析器”开始,因为它风险相对较低,功能独立,易于调试。
3. 实战:构建你的第一个httpx插件——敏感路径探测器
我们以一个非常实用的场景开始:httpx可以告诉我们一个网站存活,并返回标题和状态码。但我们经常想知道,这个网站上是否存在一些常见的敏感路径或管理后台,比如/admin,/wp-admin,/config.json,/backup.zip等。手动一个个试太慢,整合到扫描流程里才高效。
3.1 环境准备与项目初始化
首先,确保你的Go环境版本在1.25或以上(与httpx要求对齐)。打开终端,创建我们的插件项目:
mkdir httpx-plugin-sensitivepaths && cd httpx-plugin-sensitivepaths go mod init github.com/你的用户名/httpx-plugin-sensitivepaths接下来,我们需要获取httpx的依赖库。由于我们是编写插件,通常不需要直接修改httpx源码,而是引入其相关的公共包。最关键的是github.com/projectdiscovery/httpx/common/runner和github.com/projectdiscovery/httpx/common/httpx中定义的一些接口和结构体。为了简化,我们可以先参考httpx项目本身的结构。
一个更务实的方法是,直接查看httpx的GitHub仓库,找到common目录下的类型定义。但为了快速启动,我们可以先创建一个最小化的结构。实际上,更常见的插件开发模式是“fork并修改”,但对于学习,我们采用“侧挂”方式。
创建主文件main.go:
package main import ( "context" "fmt" "net/http" "regexp" "strings" "time" "github.com/projectdiscovery/httpx/common/runner" "github.com/projectdiscovery/httpx/common/httpx" )这里会遇到第一个坑:common/runner和common/httpx可能并不直接对外暴露所有我们需要的方法。实际上,httpx的插件生态并不像Nuclei那样有官方SDK。更常见的做法是,我们编写一个独立的工具,通过标准输入(stdin)读取httpx的JSON输出,或者通过调用httpx作为库(library)来集成。
为了更贴近“插件”的本质(即增强httpx本身的功能),我们选择第二种方式:将我们的逻辑编写成一个实现了httpx库中某个回调接口的模块,然后编译进一个自定义版本的httpx二进制文件中。这是ProjectDiscovery许多工具的实际扩展方式。
3.2 定义插件逻辑与数据结构
让我们调整思路。我们创建一个名为plugins的目录,在里面实现我们的核心逻辑。
创建plugins/sensitive_paths.go:
package plugins import ( "fmt" "net/url" "strings" "sync" "github.com/projectdiscovery/httpx/common/httpx" ) // SensitivePathsChecker 是我们插件的主结构体 type SensitivePathsChecker struct { // 预定义的敏感路径列表 paths []string // 用于并发安全的锁(如果内部有状态更新,可能需要) mu sync.RWMutex // 存储匹配结果的映射,键为原始URL,值为发现的敏感路径列表 results map[string][]string } // NewSensitivePathsChecker 构造函数,初始化路径列表 func NewSensitivePathsChecker() *SensitivePathsChecker { return &SensitivePathsChecker{ paths: []string{ "/admin", "/wp-admin", "/wp-login.php", "/administrator", "/backup", "/config", "/.git/HEAD", "/.env", "/api", "/debug", "/test", "/phpinfo.php", "/server-status", // ... 你可以根据需要扩展这个列表 }, results: make(map[string][]string), } } // Check 方法是核心,它接收一个httpx的响应结果,并执行检查 func (s *SensitivePathsChecker) Check(resp *httpx.Response) { if resp == nil || resp.URL == "" { return } baseURL, err := url.Parse(resp.URL) if err != nil { return } var foundPaths []string for _, path := range s.paths { // 构造完整的测试URL testURL := *baseURL // 复制baseURL testURL.Path = singleJoiningSlash(testURL.Path, path) // 注意:这里为了简化,我们只是逻辑演示。 // 实际中,我们需要在此发起一个新的HTTP请求来测试testURL是否可访问。 // 但更优雅的方式是,让我们的插件在httpx的主扫描循环中,为每个基础URL自动添加这些路径进行探测。 // 这涉及到修改httpx的“目标生成”逻辑,更为复杂。 // 因此,第一个版本我们采用“后检测”模式:分析已存在的响应内容,查找路径线索。 } // 存储结果 if len(foundPaths) > 0 { s.mu.Lock() s.results[resp.URL] = foundPaths s.mu.Unlock() } } // GetResults 返回所有发现的结果 func (s *SensitivePathsChecker) GetResults() map[string][]string { s.mu.RLock() defer s.mu.RUnlock() return s.results } // 辅助函数,处理路径拼接 func singleJoiningSlash(a, b string) string { aslash := strings.HasSuffix(a, "/") bslash := strings.HasPrefix(b, "/") switch { case aslash && bslash: return a + b[1:] case !aslash && !bslash: return a + "/" + b } return a + b }上面的代码揭示了一个关键问题:我们的插件究竟应该在哪个环节运作?是像“ nuclei模板”一样主动发起新请求,还是被动分析已有响应?
对于“敏感路径探测”,主动发起请求是更准确的。但这要求我们深度集成到httpx的引擎中。作为入门插件,我们退一步,先实现一个响应内容分析器:检查返回的HTML页面里,是否包含了指向这些敏感路径的链接。这同样有价值,可以揭示页面暴露出的潜在接口。
3.3 实现响应内容链接提取
让我们修改Check方法,专注于分析resp.Body:
func (s *SensitivePathsChecker) Check(resp *httpx.Response) { if resp == nil || resp.URL == "" || len(resp.Body) == 0 { return } bodyStr := string(resp.Body) var foundPaths []string for _, path := range s.paths { // 简单检查:路径是否直接出现在HTML中(作为链接的一部分) // 这是一个简单的字符串包含检查,实际应用可能需要更精确的正则匹配 pattern := `["']` + regexp.QuoteMeta(path) + `[^"'\s]*["']` re, err := regexp.Compile(pattern) if err != nil { continue } if re.MatchString(bodyStr) { foundPaths = append(foundPaths, path) } // 也可以查找href或src属性 hrefPattern := `href\s*=\s*["'][^"']*` + regexp.QuoteMeta(path) + `[^"']*["']` if matched, _ := regexp.MatchString(hrefPattern, bodyStr); matched { foundPaths = append(foundPaths, "链接:"+path) } } // 去重 foundPaths = unique(foundPaths) if len(foundPaths) > 0 { s.mu.Lock() s.results[resp.URL] = foundPaths s.mu.Unlock() // 实时输出提示 fmt.Printf("[插件发现] URL: %s -> 疑似暴露路径: %v\n", resp.URL, foundPaths) } } func unique(strSlice []string) []string { keys := make(map[string]bool) list := []string{} for _, entry := range strSlice { if _, value := keys[entry]; !value { keys[entry] = true list = append(list, entry) } } return list }现在,我们的插件有了一个明确的功能:扫描HTTP响应体,寻找敏感路径的踪迹。
3.4 集成到httpx执行流程
这是最具挑战性的一步。我们需要修改httpx的源代码,或者采用“包装器”模式。为了保持简洁和可维护性,我推荐包装器模式:我们编写一个自己的main.go,它导入httpx的runner库,并在runner的配置阶段,注册我们的回调函数。
查阅httpx的源码,会发现runner包有一个Runner结构体,它接受一个Options配置。虽然选项中没有直接的“响应后插件”列表,但我们可以通过自定义输出器(Custom Writer)或拦截HTTP客户端的方式来实现。
更简单的一种实践方法是利用httpx的-json输出,然后编写一个独立的、流式处理这些JSON行的Go程序。这严格来说不算“插件”,而是一个“后处理器”,但它无需修改httpx,耦合度低,非常灵活。
让我们实现这个更实用的方案:
- 编译一个增强版httpx(可选):如果你希望功能内聚,可以fork httpx仓库,在输出循环处(比如在
runner/runner.go的process函数中)添加一个钩子,调用我们的SensitivePathsChecker.Check方法。这需要你熟悉httpx代码结构。 - 使用Unix管道和后处理器(推荐):保持httpx官方二进制不变,通过管道将它的JSON输出传递给我们的Go程序。
我们采用第2种方案,创建一个新的工具httpx-processor。
创建cmd/httpx-processor/main.go:
package main import ( "bufio" "encoding/json" "fmt" "io" "os" "path/filepath" "github.com/你的用户名/httpx-plugin-sensitivepaths/plugins" ) func main() { checker := plugins.NewSensitivePathsChecker() scanner := bufio.NewScanner(os.Stdin) fmt.Println("开始处理httpx的JSON流输出...") for scanner.Scan() { line := scanner.Bytes() if len(line) == 0 { continue } // 解析httpx的JSON输出行 var result map[string]interface{} if err := json.Unmarshal(line, &result); err != nil { fmt.Fprintf(os.Stderr, "解析JSON行失败: %v\n", err) continue } // 构造一个简单的响应对象。注意:真实场景需要映射更多字段。 // 这里假设httpx输出包含url, body, status_code等字段。 url, _ := result["url"].(string) body, _ := result["body"].(string) statusCode, _ := result["status_code"].(float64) if url == "" || body == "" { continue } // 这里我们模拟一个httpx.Response。在实际插件中,你需要定义或引用一个兼容的结构体。 // 为了演示,我们直接使用一个包含URL和Body的简单结构。 resp := &plugins.SimpleResponse{ URL: url, Body: []byte(body), // 可以添加更多字段... } checker.Check(resp) } if err := scanner.Err(); err != nil { fmt.Fprintf(os.Stderr, "读取输入出错: %v\n", err) os.Exit(1) } // 打印汇总结果 fmt.Println("\n=== 敏感路径扫描汇总 ===") allResults := checker.GetResults() for url, paths := range allResults { fmt.Printf("%s:\n", url) for _, p := range paths { fmt.Printf(" - %s\n", p) } } }同时,我们需要在plugins包中定义一个简单的响应结构,避免直接依赖httpx的内部结构。
在plugins/sensitive_paths.go中添加:
// SimpleResponse 用于后处理器模式的简化响应结构 type SimpleResponse struct { URL string Body []byte StatusCode int Headers map[string][]string }并修改Check函数签名,使其接受一个接口而非具体类型,提高灵活性。
type Response interface { GetURL() string GetBody() []byte } func (s *SensitivePathsChecker) Check(resp Response) { if resp == nil || resp.GetURL() == "" || len(resp.GetBody()) == 0 { return } bodyStr := string(resp.GetBody()) // ... 后续检查逻辑不变 }然后让SimpleResponse实现这个接口。
这样,我们就拥有了一个独立的处理器。使用方式如下:
# 首先,用httpx扫描并以JSONL格式输出 httpx -l targets.txt -json -o results.jsonl # 然后,用我们的处理器分析结果 cat results.jsonl | go run cmd/httpx-processor/main.go # 或者一步到位 httpx -l targets.txt -json | go run cmd/httpx-processor/main.go虽然这不是传统意义上的“内存内插件”,但通过Unix管道和流式处理,我们实现了同样的目标,并且更加解耦和灵活。你可以在此基础上,轻松添加更多分析模块,比如提取邮箱、识别框架版本等。
4. 进阶:开发主动探测型插件
被动分析有其局限。我们来实现一个真正的主动探测插件,它需要集成到httpx内部,为每个基础目标派生出一组新的探测目标(即附加敏感路径)。
这需要更深入地理解httpx的runner包。核心思路是:实现一个PreProcessor接口,在目标被放入队列前,对其进行扩展。
假设httpx有类似的扩展点(注:最新版httpx可能提供了更灵活的输入处理钩子,这里为教学概念),我们的插件结构可能如下:
创建plugins/active_path_prober.go:
package plugins import ( "fmt" "net/url" "strings" "github.com/projectdiscovery/httpx/common/runner" ) // ActivePathProber 实现了 runner.InputPreProcessor 接口(假设存在) type ActivePathProber struct { basePaths []string } func NewActivePathProber() *ActivePathProber { return &ActivePathProber{ basePaths: []string{"/admin", "/backup", "/.git/config", "/wp-login.php"}, // 示例路径 } } // Process 方法接收一个原始输入(如域名或URL),返回一个需要探测的URL列表 func (a *ActivePathProber) Process(input string) ([]string, error) { var targets []string // 确保输入有协议头 if !strings.HasPrefix(input, "http") { input = "http://" + input } baseURL, err := url.Parse(input) if err != nil { return nil, err } // 为每个基础路径生成新URL for _, path := range a.basePaths { newURL := *baseURL newURL.Path = singleJoiningSlash(newURL.Path, path) targets = append(targets, newURL.String()) } // 也包含原始目标 targets = append(targets, input) fmt.Printf("[ActivePathProber] 为 %s 生成了 %d 个探测目标\n", input, len(targets)) return targets, nil }然后,在自定义的httpx main函数中,我们需要在初始化runner时,注册这个预处理器。这通常意味着我们需要复制一份httpx的cmd/httpx/main.go,并添加几行代码。
// 在你的自定义main.go中 preProber := plugins.NewActivePathProber() // 假设runner.Options有一个InputPreProcessors字段 options.InputPreProcessors = append(options.InputPreProcessors, preProber)这种方式侵入性较强,需要你维护一个httpx的分支。但对于需要深度定制扫描逻辑的团队来说,这是值得的。
5. 插件开发中的常见陷阱与调试技巧
即使思路清晰,在开发过程中也难免踩坑。这里分享几个我亲身经历的教训和解决方法。
5.1 并发安全是重中之重
httpx是高并发的。如果你的插件有共享状态(比如一个用于汇总结果的map),必须使用sync.RWMutex或sync.Map进行保护。我早期写的一个插件,因为没有加锁,在高速扫描时频繁出现fatal error: concurrent map writes,导致程序崩溃。
// 错误示例 type UnsafePlugin struct { results map[string]int } func (u *UnsafePlugin) Process(resp Response) { u.results[resp.GetURL()]++ // 并发写,危险! } // 正确示例 type SafePlugin struct { results map[string]int mu sync.RWMutex } func (s *SafePlugin) Process(resp Response) { s.mu.Lock() defer s.mu.Unlock() s.results[resp.GetURL()]++ }5.2 合理控制资源消耗
插件如果在每个响应上都执行非常耗CPU或内存的操作(如复杂的正则匹配、大字符串解析),会严重拖慢整体扫描速度。务必进行性能优化:
- 预编译正则表达式:在插件初始化时
regexp.Compile,而不是在每次Check中。 - 限制响应体大小:对于非常大的响应体(如文件下载),只分析前几KB可能就够了。可以通过判断
len(resp.Body)来提前返回。 - 避免阻塞IO:不要在插件主逻辑中进行网络请求。如果必须,请使用带超时的上下文和协程,并确保有并发数控制。
5.3 优雅处理错误与超时
你的插件不应该因为单个目标的处理失败而影响整个扫描。一定要用defer和recover机制捕获可能发生的panic,或者至少进行细致的错误判断。
func (p *MyPlugin) SafeCheck(resp Response) { defer func() { if r := recover(); r != nil { fmt.Fprintf(os.Stderr, "[插件Panic恢复] 处理 %s 时发生错误: %v\n", resp.GetURL(), r) } }() // 你的核心逻辑 p.Check(resp) }5.4 调试与日志输出
在开发阶段,善用日志。但要注意,直接fmt.Print到标准输出可能会干扰httpx本身的格式化输出。建议为你的插件设计一个日志级别,并通过环境变量控制,例如:
var debug = os.Getenv("HTTPX_PLUGIN_DEBUG") == "true" func (p *MyPlugin) logDebug(format string, args ...interface{}) { if debug { fmt.Fprintf(os.Stderr, "[插件调试] "+format+"\n", args...) } }这样,平时运行不会产生多余输出,需要排查问题时,只需HTTPX_PLUGIN_DEBUG=true httpx ...即可。
5.5 与现有flag的兼容性
你的插件行为可能会受到httpx原有flag的影响。例如,如果用户使用了-no-fallback(不尝试HTTPS回退),那么你插件生成的HTTPS探测目标就可能失败。在设计插件时,要仔细阅读httpx的flag文档,考虑你的逻辑是否应该尊重这些全局设置。一种方法是将runner.Options传递给你的插件结构体,以便在内部进行判断。
6. 从插件到工具链:集成与自动化
一个成熟的插件,最终应该无缝融入你的安全工具链。这里有几个方向:
- 与Nuclei联动:你的插件发现了一个疑似
/admin的路径,并且状态码是200。你可以立即将这个完整的URL传递给Nuclei,运行针对管理后台的漏洞检测模板。这可以通过在插件中调用exec.Command运行nuclei命令行,或者更好的方式,通过消息队列(如Redis)将目标传递出去,由另一个常驻进程处理。 - 结果自动入库:将插件发现的资产、敏感信息直接写入Elasticsearch、SQLite或PostgreSQL数据库,方便后续聚合分析和仪表板展示。
- 生成报告:插件结束后,自动生成一份HTML或Markdown格式的报告,高亮显示发现的高风险路径,并附上截图(如果启用了httpx的
-ss截图功能)。 - 配置化:将敏感路径列表、正则表达式模式等写成外部YAML或JSON配置文件,让插件行为无需重新编译即可调整。
例如,一个简单的与Nuclei联动的后处理脚本可能是这样的:
#!/bin/bash # scan_and_poc.sh TARGET_FILE=$1 # 1. 使用httpx进行基础探测和插件分析 httpx -l $TARGET_FILE -json -o httpx_results.jsonl # 2. 使用我们的插件提取出状态码为200的/admin路径 cat httpx_results.jsonl | jq -r 'select(.status_code == 200) | select(.url | contains("/admin")) | .url' > admin_targets.txt # 3. 如果发现目标,则用nuclei进行深度检测 if [[ -s admin_targets.txt ]]; then echo "发现管理后台,开始漏洞扫描..." nuclei -l admin_targets.txt -t ~/nuclei-templates/exposures/configs/ -o admin_scan_results.txt fi通过编写这样的Shell脚本或Python脚本,你可以将httpx插件、httpx本身以及其他工具(如jq, nuclei, subfinder)粘合起来,形成一个完全自动化的侦察流水线。
开发httpx扩展插件,是一个从“工具使用者”迈向“工具塑造者”的过程。它迫使你去理解工具内部的运作机制,思考如何将重复劳动转化为自动化逻辑。无论是简单的响应分析器,还是复杂的主动探测插件,其价值都在于它精准地解决了你独有的问题。