mirror of
https://github.com/shadow1ng/fscan.git
synced 2026-09-22 03:10:42 +08:00
- eval_random: randomInt参数max<=min时不再panic,返回CEL错误 - scanner: 长驻插件nil/panic时兜底发送ready通道,消除死锁 - poc_executor: Ceye API密钥改为环境变量CEYE_API/CEYE_DOMAIN - Eval: ParseResponse加入oResp.Request nil检查 - Eval: reverseCheck中http.NewRequest错误不再忽略 - poc_executor: clusterpoc中CEL表达式求值错误记录日志 - winwmi: PowerShell执行失败完整记录错误信息 - sshkey: authorized_keys读取失败处理错误 - minidump: Scan结束后释放系统DLL句柄 - Windows插件: PE文件错误消息改用i18n
907 lines
24 KiB
Go
907 lines
24 KiB
Go
package lib
|
||
|
||
import (
|
||
"crypto/md5" //nolint:gosec // G501: MD5用于POC规则去重,非加密用途
|
||
"fmt"
|
||
"math/rand" //nolint:gosec // G404: math/rand用于生成测试数据,非加密用途
|
||
"net/http"
|
||
"net/url"
|
||
"os"
|
||
"regexp"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/google/cel-go/cel"
|
||
"github.com/shadow1ng/fscan/common"
|
||
"github.com/shadow1ng/fscan/common/i18n"
|
||
"github.com/shadow1ng/fscan/common/output"
|
||
"github.com/shadow1ng/fscan/webscan/fingerprint"
|
||
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||
)
|
||
|
||
// Ceye平台凭据(通过环境变量配置,避免将密钥硬编码在源码中)
|
||
// CEYE_API: Ceye API令牌
|
||
// CEYE_DOMAIN: Ceye平台域名(可选,默认使用api.ceye.io)
|
||
var ceyeAPI, ceyeDomain string
|
||
|
||
func init() {
|
||
ceyeAPI = os.Getenv("CEYE_API")
|
||
ceyeDomain = os.Getenv("CEYE_DOMAIN")
|
||
if ceyeDomain == "" {
|
||
ceyeDomain = "api.ceye.io"
|
||
}
|
||
}
|
||
|
||
// Task 定义单个POC检测任务的结构体
|
||
type Task struct {
|
||
Req *http.Request // HTTP请求对象
|
||
Poc *Poc // POC检测脚本
|
||
}
|
||
|
||
// VulnResult 漏洞结果结构体
|
||
type VulnResult struct {
|
||
Poc *Poc // POC脚本
|
||
VulName string // 漏洞名称
|
||
Target string // 目标URL
|
||
Details map[string]interface{} // 详细信息
|
||
}
|
||
|
||
// POCContext POC执行上下文配置
|
||
// 显式传递配置
|
||
type POCContext struct {
|
||
DNSLog bool // 是否启用DNSLog检测
|
||
POCFull bool // 是否完整POC扫描
|
||
}
|
||
|
||
// CheckMultiPoc 并发执行多个POC检测
|
||
// 参数说明:
|
||
// - req: HTTP请求对象
|
||
// - pocs: POC检测脚本列表
|
||
// - workers: 并发工作协程数量
|
||
// - pocCtx: POC执行上下文配置
|
||
func CheckMultiPoc(req *http.Request, pocs []*Poc, workers int, pocCtx *POCContext) {
|
||
// 确保至少有一个工作协程
|
||
if workers <= 0 {
|
||
workers = 1
|
||
}
|
||
|
||
// 创建任务通道,缓冲区大小为POC列表长度
|
||
tasks := make(chan Task, len(pocs))
|
||
var wg sync.WaitGroup
|
||
|
||
// 启动指定数量的工作协程池
|
||
for i := 0; i < workers; i++ {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
// 从任务通道循环获取任务
|
||
for task := range tasks {
|
||
// 执行POC检测,返回是否存在漏洞、错误信息和漏洞名称
|
||
isVulnerable, vulName, err := executePoc(task.Req, task.Poc, pocCtx)
|
||
|
||
// 处理执行过程中的错误
|
||
if err != nil {
|
||
common.LogError(i18n.Tr("webscan_poc_exec_error", task.Poc.Name, err))
|
||
continue
|
||
}
|
||
|
||
// 仅当通过普通POC规则(非clusterpoc)检测到漏洞时,才创建结果
|
||
// 因为clusterpoc已在内部处理了漏洞输出
|
||
if isVulnerable && vulName != "" {
|
||
// 构造漏洞详细信息
|
||
details := make(map[string]interface{})
|
||
details["vulnerability_type"] = task.Poc.Name
|
||
details["vulnerability_name"] = vulName
|
||
|
||
// 添加作者信息(如果有)
|
||
if task.Poc.Detail.Author != "" {
|
||
details["author"] = task.Poc.Detail.Author
|
||
}
|
||
|
||
// 添加参考链接(如果有)
|
||
if len(task.Poc.Detail.Links) != 0 {
|
||
details["references"] = task.Poc.Detail.Links
|
||
}
|
||
|
||
// 添加漏洞描述(如果有)
|
||
if task.Poc.Detail.Description != "" {
|
||
details["description"] = task.Poc.Detail.Description
|
||
}
|
||
|
||
// 创建并保存扫描结果
|
||
result := &output.ScanResult{
|
||
Time: time.Now(),
|
||
Type: output.TypeVuln,
|
||
Target: task.Req.URL.String(),
|
||
Status: "vulnerable",
|
||
Details: details,
|
||
}
|
||
_ = common.SaveResult(result)
|
||
|
||
// 构造控制台输出的日志信息
|
||
logMsg := fmt.Sprintf("目标: %s\n 漏洞类型: %s\n 漏洞名称: %s\n 详细信息:",
|
||
task.Req.URL,
|
||
task.Poc.Name,
|
||
vulName)
|
||
|
||
// 添加作者信息到日志
|
||
if task.Poc.Detail.Author != "" {
|
||
logMsg += "\n\t作者:" + task.Poc.Detail.Author
|
||
}
|
||
|
||
// 添加参考链接到日志
|
||
if len(task.Poc.Detail.Links) != 0 {
|
||
logMsg += "\n\t参考链接:" + strings.Join(task.Poc.Detail.Links, "\n")
|
||
}
|
||
|
||
// 添加描述信息到日志
|
||
if task.Poc.Detail.Description != "" {
|
||
logMsg += "\n\t描述:" + task.Poc.Detail.Description
|
||
}
|
||
|
||
// 输出成功日志
|
||
common.LogVuln(logMsg)
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
|
||
// 分发所有POC任务到通道
|
||
for _, poc := range pocs {
|
||
tasks <- Task{
|
||
Req: req,
|
||
Poc: poc,
|
||
}
|
||
}
|
||
|
||
// 关闭任务通道
|
||
close(tasks)
|
||
|
||
// 等待所有POC检测任务完成
|
||
wg.Wait()
|
||
}
|
||
|
||
// collectVarDeclarations 收集POC的变量声明
|
||
func collectVarDeclarations(p *Poc) []*exprpb.Decl {
|
||
var decls []*exprpb.Decl
|
||
|
||
// 收集 Set 中的变量声明
|
||
for _, item := range p.Set {
|
||
decls = append(decls, MakeVarDecl(item.Key, item.Value))
|
||
}
|
||
|
||
// 收集 Sets 中的变量声明
|
||
for _, item := range p.Sets {
|
||
value := ""
|
||
if len(item.Value) > 0 {
|
||
value = item.Value[0]
|
||
}
|
||
decls = append(decls, MakeVarDecl(item.Key, value))
|
||
}
|
||
|
||
return decls
|
||
}
|
||
|
||
// executePoc 执行单个POC检测
|
||
func executePoc(oReq *http.Request, p *Poc, pocCtx *POCContext) (bool, string, error) {
|
||
// 收集POC变量声明
|
||
varDecls := collectVarDeclarations(p)
|
||
|
||
// 从基础环境扩展(复用缓存的基础环境,仅添加变量声明)
|
||
env, err := ExtendEnvWithVars(varDecls)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("执行环境错误 %s: %w", p.Name, err)
|
||
}
|
||
|
||
// 解析请求
|
||
req, err := ParseRequest(oReq)
|
||
if err != nil {
|
||
return false, "", fmt.Errorf("请求解析错误 %s: %w", p.Name, err)
|
||
}
|
||
|
||
// 初始化变量映射
|
||
variableMap := make(map[string]interface{})
|
||
defer func() { variableMap = nil }()
|
||
variableMap["request"] = req
|
||
|
||
// 处理设置项
|
||
for _, item := range p.Set {
|
||
key, expression := item.Key, item.Value
|
||
if expression == "newReverse()" {
|
||
if !pocCtx.DNSLog {
|
||
return false, "", nil
|
||
}
|
||
variableMap[key] = newReverse(pocCtx.DNSLog)
|
||
continue
|
||
}
|
||
if _, err = evalset(env, variableMap, key, expression); err != nil {
|
||
common.LogError(i18n.Tr("webscan_set_exec_error", p.Name, err))
|
||
}
|
||
}
|
||
|
||
// 处理爆破模式
|
||
if len(p.Sets) > 0 {
|
||
success, err := clusterpoc(oReq, p, variableMap, req, env, pocCtx)
|
||
return success, "", err
|
||
}
|
||
|
||
return executeRules(oReq, p, variableMap, req, env)
|
||
}
|
||
|
||
// executeRules 执行POC规则并返回结果
|
||
func executeRules(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env) (bool, string, error) {
|
||
// 处理单个规则的函数
|
||
executeRule := func(rule Rules) (bool, error) {
|
||
Headers := cloneMap(rule.Headers)
|
||
|
||
// 替换变量
|
||
for varName, varValue := range variableMap {
|
||
if _, isMap := varValue.(map[string]string); isMap {
|
||
continue
|
||
}
|
||
strValue := fmt.Sprintf("%v", varValue)
|
||
|
||
// 替换Header中的变量
|
||
for headerKey, headerValue := range Headers {
|
||
Headers[headerKey] = strings.ReplaceAll(headerValue, "{{"+varName+"}}", strValue)
|
||
}
|
||
|
||
// 替换Path和Body中的变量
|
||
rule.Path = strings.ReplaceAll(rule.Path, "{{"+varName+"}}", strValue)
|
||
rule.Body = strings.ReplaceAll(rule.Body, "{{"+varName+"}}", strValue)
|
||
}
|
||
|
||
// 构建请求路径
|
||
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
|
||
req.URL.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
|
||
} else {
|
||
req.URL.Path = rule.Path
|
||
}
|
||
req.URL.Path = strings.ReplaceAll(req.URL.Path, " ", "%20")
|
||
|
||
// 创建新请求(传递原始请求的Context以支持超时控制)
|
||
newRequest, err := http.NewRequestWithContext(
|
||
oReq.Context(),
|
||
rule.Method,
|
||
fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.URL.Host, req.URL.Path),
|
||
strings.NewReader(rule.Body),
|
||
)
|
||
if err != nil {
|
||
return false, fmt.Errorf("请求创建错误: %w", err)
|
||
}
|
||
|
||
// 设置请求头
|
||
newRequest.Header = oReq.Header.Clone()
|
||
for k, v := range Headers {
|
||
newRequest.Header.Set(k, v)
|
||
}
|
||
_ = Headers // 清空Headers
|
||
|
||
// 发送请求
|
||
resp, err := DoRequest(newRequest, rule.FollowRedirects)
|
||
newRequest = nil
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
variableMap["response"] = resp
|
||
|
||
// 执行搜索规则
|
||
if rule.Search != "" {
|
||
result := doSearch(rule.Search, GetHeader(resp.Headers)+string(resp.Body))
|
||
if len(result) == 0 {
|
||
return false, nil
|
||
}
|
||
for k, v := range result {
|
||
variableMap[k] = v
|
||
}
|
||
}
|
||
|
||
// 执行表达式
|
||
out, err := Evaluate(env, rule.Expression, variableMap)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
if flag, ok := out.Value().(bool); ok {
|
||
return flag, nil
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
// 处理规则组的函数
|
||
executeRuleSet := func(rules []Rules) bool {
|
||
for _, rule := range rules {
|
||
flag, err := executeRule(rule)
|
||
if err != nil || !flag {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// 执行检测规则
|
||
success := false
|
||
if len(p.Rules) > 0 {
|
||
success = executeRuleSet(p.Rules)
|
||
return success, "", nil
|
||
}
|
||
for _, item := range p.Groups {
|
||
name, rules := item.Key, item.Value
|
||
if success = executeRuleSet(rules); success {
|
||
return true, name, nil
|
||
}
|
||
}
|
||
|
||
return false, "", nil
|
||
}
|
||
|
||
// doSearch 在响应体中执行正则匹配并提取命名捕获组
|
||
func doSearch(re string, body string) map[string]string {
|
||
// 编译正则表达式
|
||
r, err := regexp.Compile(re)
|
||
// 正则表达式编译
|
||
if err != nil {
|
||
common.LogError(i18n.Tr("webscan_regex_compile_error", err))
|
||
return nil
|
||
}
|
||
|
||
// 执行正则匹配
|
||
result := r.FindStringSubmatch(body)
|
||
names := r.SubexpNames()
|
||
|
||
// 处理匹配结果
|
||
if len(result) > 1 && len(names) > 1 {
|
||
paramsMap := make(map[string]string)
|
||
for i, name := range names {
|
||
if i > 0 && i <= len(result) {
|
||
// 特殊处理Set-Cookie头:剥离Path/Expires等属性,仅保留key=value
|
||
if strings.HasPrefix(re, "Set-Cookie:") {
|
||
paramsMap[name] = optimizeCookies(result[i])
|
||
} else {
|
||
paramsMap[name] = result[i]
|
||
}
|
||
}
|
||
}
|
||
return paramsMap
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// optimizeCookies 优化Cookie字符串,移除不必要的属性
|
||
func optimizeCookies(rawCookie string) string {
|
||
var output strings.Builder
|
||
|
||
// 解析Cookie键值对
|
||
pairs := strings.Split(rawCookie, "; ")
|
||
for _, pair := range pairs {
|
||
nameVal := strings.SplitN(pair, "=", 2)
|
||
if len(nameVal) < 2 {
|
||
continue
|
||
}
|
||
|
||
// 跳过Cookie属性
|
||
switch strings.ToLower(nameVal[0]) {
|
||
case "expires", "max-age", "path", "domain",
|
||
"version", "comment", "secure", "samesite", "httponly":
|
||
continue
|
||
}
|
||
|
||
// 构建Cookie键值对
|
||
if output.Len() > 0 {
|
||
output.WriteString("; ")
|
||
}
|
||
output.WriteString(nameVal[0])
|
||
output.WriteString("=")
|
||
output.WriteString(strings.Join(nameVal[1:], "="))
|
||
}
|
||
|
||
return output.String()
|
||
}
|
||
|
||
// newReverse 创建新的反连检测对象
|
||
// dnsLog参数:显式传入配置
|
||
func newReverse(dnsLog bool) *Reverse {
|
||
// 检查DNS日志功能是否启用
|
||
if !dnsLog {
|
||
return &Reverse{}
|
||
}
|
||
|
||
// 生成随机子域名
|
||
const (
|
||
letters = "1234567890abcdefghijklmnopqrstuvwxyz"
|
||
subdomainLength = 8
|
||
)
|
||
//nolint:gosec,govet // G404: 用于生成测试子域名,非加密用途; shadow: 局部randSource不影响全局
|
||
randSource := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
subdomain := RandomStr(randSource, letters, subdomainLength)
|
||
|
||
// 构建URL
|
||
urlStr := fmt.Sprintf("http://%s.%s", subdomain, ceyeDomain)
|
||
u, err := url.Parse(urlStr)
|
||
// 解析反连URL
|
||
if err != nil {
|
||
common.LogError(i18n.Tr("webscan_reverse_url_error", err))
|
||
return &Reverse{}
|
||
}
|
||
|
||
// 返回反连检测配置
|
||
return &Reverse{
|
||
URL: urlStr,
|
||
Domain: u.Hostname(),
|
||
Ip: u.Host,
|
||
IsDomainNameServer: false,
|
||
}
|
||
}
|
||
|
||
// clusterpoc 执行集群POC检测,支持批量参数组合测试
|
||
func clusterpoc(oReq *http.Request, p *Poc, variableMap map[string]interface{}, req *Request, env *cel.Env, pocCtx *POCContext) (success bool, err error) {
|
||
var strMap StrMap // 存储成功的参数组合
|
||
var shiroKeyCount int // shiro key测试计数
|
||
|
||
// 遍历POC规则
|
||
for ruleIndex, rule := range p.Rules {
|
||
// 检查是否需要进行参数Fuzz测试
|
||
if !isFuzz(rule, p.Sets) {
|
||
// 不需要Fuzz,直接发送请求
|
||
success, err = clustersend(oReq, variableMap, req, env, rule)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if !success {
|
||
return false, err
|
||
}
|
||
continue
|
||
}
|
||
|
||
// 生成参数组合
|
||
setsMap := Combo(p.Sets)
|
||
ruleHash := make(map[string]struct{}) // 用于去重的规则哈希表
|
||
|
||
// 遍历参数组合
|
||
paramLoop:
|
||
for comboIndex, paramCombo := range setsMap {
|
||
// Shiro Key测试特殊处理:默认只测试10个key
|
||
if p.Name == "poc-yaml-shiro-key" && !pocCtx.POCFull && comboIndex >= 10 {
|
||
if paramCombo[1] == "cbc" {
|
||
continue
|
||
}
|
||
if shiroKeyCount == 0 {
|
||
shiroKeyCount = comboIndex
|
||
}
|
||
if comboIndex-shiroKeyCount >= 10 {
|
||
break
|
||
}
|
||
}
|
||
|
||
// 克隆规则以避免相互影响
|
||
currentRule := cloneRules(rule)
|
||
var hasReplacement bool
|
||
var currentParams StrMap
|
||
payloads := make(map[string]interface{})
|
||
var payloadExpr string
|
||
|
||
// 计算所有参数的实际值
|
||
for i, set := range p.Sets {
|
||
key, expr := set.Key, paramCombo[i]
|
||
if key == "payload" {
|
||
payloadExpr = expr
|
||
}
|
||
output, err := evalset1(env, variableMap, key, expr)
|
||
if err != nil {
|
||
common.LogError(i18n.Tr("webscan_set_exec_error", key, err))
|
||
}
|
||
payloads[key] = output
|
||
}
|
||
|
||
// 替换规则中的参数(使用提取的函数)
|
||
hasReplacement, currentParams = applyParametersToRule(¤tRule, p.Sets, payloads, variableMap, payloadExpr)
|
||
|
||
// 如果没有参数被替换,跳过当前组合
|
||
if !hasReplacement {
|
||
continue
|
||
}
|
||
|
||
// 规则去重
|
||
ruleMD5 := getRuleHash(¤tRule)
|
||
if _, exists := ruleHash[ruleMD5]; exists {
|
||
continue
|
||
}
|
||
ruleHash[ruleMD5] = struct{}{}
|
||
|
||
// 发送请求并处理结果
|
||
success, err = clustersend(oReq, variableMap, req, env, currentRule)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
if success {
|
||
targetURL := fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.URL.Host, req.URL.Path)
|
||
|
||
// 处理成功情况
|
||
if currentRule.Continue {
|
||
// 使用Continue标志时,记录但继续测试其他参数
|
||
recordVulnerabilityResult(targetURL, p, currentParams, false)
|
||
continue
|
||
}
|
||
|
||
// 记录成功的参数组合
|
||
strMap = append(strMap, currentParams...)
|
||
if ruleIndex == len(p.Rules)-1 {
|
||
// 最终规则成功,记录完整的结果并返回
|
||
recordVulnerabilityResult(targetURL, p, strMap, false)
|
||
return false, nil
|
||
}
|
||
break paramLoop
|
||
}
|
||
}
|
||
|
||
if !success {
|
||
break
|
||
}
|
||
if rule.Continue {
|
||
return false, nil
|
||
}
|
||
}
|
||
|
||
return success, nil
|
||
}
|
||
|
||
// applyParametersToRule 将参数应用到规则中,返回是否有替换发生和替换的参数列表
|
||
// 这是一个纯函数,不修改原始规则,而是修改传入的currentRule指针
|
||
func applyParametersToRule(
|
||
currentRule *Rules,
|
||
sets ListMap,
|
||
payloads map[string]interface{},
|
||
variableMap map[string]interface{},
|
||
payloadExpr string,
|
||
) (hasReplacement bool, replacedParams StrMap) {
|
||
// 遍历所有参数集
|
||
for _, set := range sets {
|
||
paramReplaced := false
|
||
key := set.Key
|
||
value := fmt.Sprintf("%v", payloads[key])
|
||
paramPattern := "{{" + key + "}}"
|
||
|
||
// 替换Header中的参数
|
||
for headerKey, headerVal := range currentRule.Headers {
|
||
if strings.Contains(headerVal, paramPattern) {
|
||
currentRule.Headers[headerKey] = strings.ReplaceAll(headerVal, paramPattern, value)
|
||
paramReplaced = true
|
||
}
|
||
}
|
||
|
||
// 替换Path中的参数
|
||
if strings.Contains(currentRule.Path, paramPattern) {
|
||
currentRule.Path = strings.ReplaceAll(currentRule.Path, paramPattern, value)
|
||
paramReplaced = true
|
||
}
|
||
|
||
// 替换Body中的参数
|
||
if strings.Contains(currentRule.Body, paramPattern) {
|
||
currentRule.Body = strings.ReplaceAll(currentRule.Body, paramPattern, value)
|
||
paramReplaced = true
|
||
}
|
||
|
||
// 记录替换的参数
|
||
if paramReplaced {
|
||
hasReplacement = true
|
||
if key == "payload" {
|
||
// 处理payload的特殊情况:检查payload表达式中是否包含变量
|
||
hasVarInPayload := false
|
||
for varKey, varVal := range variableMap {
|
||
if strings.Contains(payloadExpr, varKey) {
|
||
hasVarInPayload = true
|
||
replacedParams = append(replacedParams, StrItem{varKey, fmt.Sprintf("%v", varVal)})
|
||
}
|
||
}
|
||
if hasVarInPayload {
|
||
continue
|
||
}
|
||
}
|
||
replacedParams = append(replacedParams, StrItem{key, value})
|
||
}
|
||
}
|
||
|
||
return hasReplacement, replacedParams
|
||
}
|
||
|
||
// getRuleHash 计算规则的MD5哈希值用于去重
|
||
func getRuleHash(rule *Rules) string {
|
||
//nolint:gosec // G401: MD5用于规则去重,非加密用途
|
||
ruleDigest := md5.Sum([]byte(fmt.Sprintf("%v", rule)))
|
||
return fmt.Sprintf("%x", ruleDigest)
|
||
}
|
||
|
||
// recordVulnerabilityResult 记录漏洞检测结果
|
||
func recordVulnerabilityResult(targetURL string, pocDef *Poc, params StrMap, skipSave bool) {
|
||
// 构造详细信息
|
||
details := make(map[string]interface{})
|
||
details["vulnerability_type"] = pocDef.Name
|
||
details["vulnerability_name"] = pocDef.Name // 使用POC名称作为漏洞名称
|
||
|
||
// 添加作者信息(如果有)
|
||
if pocDef.Detail.Author != "" {
|
||
details["author"] = pocDef.Detail.Author
|
||
}
|
||
|
||
// 添加参考链接(如果有)
|
||
if len(pocDef.Detail.Links) != 0 {
|
||
details["references"] = pocDef.Detail.Links
|
||
}
|
||
|
||
// 添加漏洞描述(如果有)
|
||
if pocDef.Detail.Description != "" {
|
||
details["description"] = pocDef.Detail.Description
|
||
}
|
||
|
||
// 添加参数信息(如果有)
|
||
if len(params) > 0 {
|
||
paramMap := make(map[string]string)
|
||
for _, item := range params {
|
||
paramMap[item.Key] = item.Value
|
||
}
|
||
details["parameters"] = paramMap
|
||
}
|
||
|
||
// 保存漏洞结果(除非明确指示跳过)
|
||
if !skipSave {
|
||
result := &output.ScanResult{
|
||
Time: time.Now(),
|
||
Type: output.TypeVuln,
|
||
Target: targetURL,
|
||
Status: "vulnerable",
|
||
Details: details,
|
||
}
|
||
_ = common.SaveResult(result)
|
||
}
|
||
|
||
// 生成日志消息
|
||
var logMsg string
|
||
if pocDef.Name == "poc-yaml-backup-file" || pocDef.Name == "poc-yaml-sql-file" {
|
||
logMsg = fmt.Sprintf("检测到漏洞 %s %s", targetURL, pocDef.Name)
|
||
} else {
|
||
logMsg = fmt.Sprintf("检测到漏洞 %s %s 参数:%v", targetURL, pocDef.Name, params)
|
||
}
|
||
|
||
// 输出成功日志
|
||
common.LogVuln(logMsg)
|
||
}
|
||
|
||
// isFuzz 检查规则是否包含需要Fuzz测试的参数
|
||
func isFuzz(rule Rules, Sets ListMap) bool {
|
||
// 遍历所有参数
|
||
for _, param := range Sets {
|
||
key := param.Key
|
||
paramPattern := "{{" + key + "}}"
|
||
|
||
// 检查Headers中是否包含参数
|
||
for _, headerValue := range rule.Headers {
|
||
if strings.Contains(headerValue, paramPattern) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// 检查Path中是否包含参数
|
||
if strings.Contains(rule.Path, paramPattern) {
|
||
return true
|
||
}
|
||
|
||
// 检查Body中是否包含参数
|
||
if strings.Contains(rule.Body, paramPattern) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// Combo 生成参数组合
|
||
func Combo(input ListMap) [][]string {
|
||
if len(input) == 0 {
|
||
return nil
|
||
}
|
||
|
||
// 处理只有一个参数的情况
|
||
if len(input) == 1 {
|
||
output := make([][]string, 0, len(input[0].Value))
|
||
for _, value := range input[0].Value {
|
||
output = append(output, []string{value})
|
||
}
|
||
return output
|
||
}
|
||
|
||
// 递归处理多个参数的情况
|
||
subCombos := Combo(input[1:])
|
||
return MakeData(subCombos, input[0].Value)
|
||
}
|
||
|
||
// MakeData 将新的参数值与已有的组合进行组合
|
||
func MakeData(base [][]string, nextData []string) [][]string {
|
||
// 预分配足够的空间
|
||
output := make([][]string, 0, len(base)*len(nextData))
|
||
|
||
// 遍历已有组合和新参数值
|
||
for _, existingCombo := range base {
|
||
for _, newValue := range nextData {
|
||
// 创建新组合
|
||
newCombo := make([]string, 0, len(existingCombo)+1)
|
||
newCombo = append(newCombo, newValue)
|
||
newCombo = append(newCombo, existingCombo...)
|
||
output = append(output, newCombo)
|
||
}
|
||
}
|
||
|
||
return output
|
||
}
|
||
|
||
// clustersend 执行单个规则的HTTP请求和响应检测
|
||
func clustersend(oReq *http.Request, variableMap map[string]interface{}, req *Request, env *cel.Env, rule Rules) (bool, error) {
|
||
// 替换请求中的变量
|
||
for varName, varValue := range variableMap {
|
||
// 跳过map类型的变量
|
||
if _, isMap := varValue.(map[string]string); isMap {
|
||
continue
|
||
}
|
||
|
||
strValue := fmt.Sprintf("%v", varValue)
|
||
varPattern := "{{" + varName + "}}"
|
||
|
||
// 替换Headers中的变量
|
||
for headerKey, headerValue := range rule.Headers {
|
||
if strings.Contains(headerValue, varPattern) {
|
||
rule.Headers[headerKey] = strings.ReplaceAll(headerValue, varPattern, strValue)
|
||
}
|
||
}
|
||
|
||
// 替换Path和Body中的变量
|
||
rule.Path = strings.ReplaceAll(strings.TrimSpace(rule.Path), varPattern, strValue)
|
||
rule.Body = strings.ReplaceAll(strings.TrimSpace(rule.Body), varPattern, strValue)
|
||
}
|
||
|
||
// 构建完整请求路径
|
||
if oReq.URL.Path != "" && oReq.URL.Path != "/" {
|
||
req.URL.Path = fmt.Sprint(oReq.URL.Path, rule.Path)
|
||
} else {
|
||
req.URL.Path = rule.Path
|
||
}
|
||
|
||
// URL编码处理
|
||
req.URL.Path = strings.ReplaceAll(req.URL.Path, " ", "%20")
|
||
|
||
// 创建新的HTTP请求(传递原始请求的Context以支持超时控制)
|
||
reqURL := fmt.Sprintf("%s://%s%s", req.URL.Scheme, req.URL.Host, req.URL.Path)
|
||
newRequest, err := http.NewRequestWithContext(oReq.Context(), rule.Method, reqURL, strings.NewReader(rule.Body))
|
||
if err != nil {
|
||
return false, fmt.Errorf("HTTP请求错误: %w", err)
|
||
}
|
||
defer func() { newRequest = nil }()
|
||
|
||
// 设置请求头
|
||
newRequest.Header = oReq.Header.Clone()
|
||
for key, value := range rule.Headers {
|
||
newRequest.Header.Set(key, value)
|
||
}
|
||
|
||
// 发送请求
|
||
resp, err := DoRequest(newRequest, rule.FollowRedirects)
|
||
if err != nil {
|
||
return false, fmt.Errorf("请求发送错误: %w", err)
|
||
}
|
||
|
||
// 更新响应到变量映射
|
||
variableMap["response"] = resp
|
||
|
||
// 执行搜索规则
|
||
if rule.Search != "" {
|
||
searchContent := GetHeader(resp.Headers) + string(resp.Body)
|
||
result := doSearch(rule.Search, searchContent)
|
||
|
||
if len(result) > 0 {
|
||
// 将搜索结果添加到变量映射
|
||
for key, value := range result {
|
||
variableMap[key] = value
|
||
}
|
||
} else {
|
||
return false, nil
|
||
}
|
||
}
|
||
|
||
// 执行CEL表达式
|
||
out, err := Evaluate(env, rule.Expression, variableMap)
|
||
if err != nil {
|
||
if strings.Contains(err.Error(), "Syntax error") {
|
||
common.LogError(i18n.Tr("webscan_cel_syntax_error", rule.Expression, err))
|
||
}
|
||
return false, err
|
||
}
|
||
|
||
// 检查表达式执行结果
|
||
if fmt.Sprintf("%v", out) == "false" {
|
||
return false, nil
|
||
}
|
||
|
||
return true, nil
|
||
}
|
||
|
||
// cloneRules 深度复制Rules结构体
|
||
// 参数:
|
||
// - tags: 原始Rules结构体
|
||
// 返回: 复制后的新Rules结构体
|
||
func cloneRules(tags Rules) Rules {
|
||
return Rules{
|
||
Method: tags.Method,
|
||
Path: tags.Path,
|
||
Body: tags.Body,
|
||
Search: tags.Search,
|
||
FollowRedirects: tags.FollowRedirects,
|
||
Expression: tags.Expression,
|
||
Headers: cloneMap(tags.Headers),
|
||
Continue: tags.Continue,
|
||
}
|
||
}
|
||
|
||
// cloneMap 深度复制字符串映射
|
||
func cloneMap(tags map[string]string) map[string]string {
|
||
if tags == nil {
|
||
return nil
|
||
}
|
||
cloneTags := make(map[string]string, len(tags))
|
||
for key, value := range tags {
|
||
cloneTags[key] = value
|
||
}
|
||
return cloneTags
|
||
}
|
||
|
||
// evalset 执行CEL表达式并处理特殊类型结果
|
||
func evalset(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (string, error) {
|
||
out, err := Evaluate(env, expression, variableMap)
|
||
if err != nil {
|
||
variableMap[k] = expression
|
||
return expression, err
|
||
}
|
||
|
||
// 根据不同类型处理输出
|
||
switch value := out.Value().(type) {
|
||
case *UrlType:
|
||
variableMap[k] = URLTypeToString(value)
|
||
case int64:
|
||
variableMap[k] = int(value)
|
||
default:
|
||
variableMap[k] = fmt.Sprintf("%v", out)
|
||
}
|
||
|
||
return fmt.Sprintf("%v", variableMap[k]), nil
|
||
}
|
||
|
||
// evalset1 执行CEL表达式的简化版本
|
||
func evalset1(env *cel.Env, variableMap map[string]interface{}, k string, expression string) (string, error) {
|
||
out, err := Evaluate(env, expression, variableMap)
|
||
if err != nil {
|
||
variableMap[k] = expression
|
||
} else {
|
||
variableMap[k] = fmt.Sprintf("%v", out)
|
||
}
|
||
return fmt.Sprintf("%v", variableMap[k]), err
|
||
}
|
||
|
||
// CheckInfoPoc 检查POC信息并返回别名
|
||
func CheckInfoPoc(infostr string) string {
|
||
for _, poc := range fingerprint.PocDatas {
|
||
if strings.Contains(infostr, poc.Name) {
|
||
return poc.Alias
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// GetHeader 将HTTP头转换为字符串格式
|
||
func GetHeader(header map[string]string) string {
|
||
var builder strings.Builder
|
||
for name, values := range header {
|
||
builder.WriteString(fmt.Sprintf("%s: %s\n", name, values))
|
||
}
|
||
builder.WriteString("\r\n")
|
||
return builder.String()
|
||
}
|