侦查守卫开发记录

发布时间: 2023-03-27

设计想法

  • 一个工具,负责将输入信息转换为附带指纹信息输出。
  • 既可以提供接口给平台调用,又可以作为工具单独使用。

目标输入

  • 单个目标
  • 文件导入
  • 标准输入
  • API接口

  • 去重处理

功能队列

  • 指纹识别
  • 服务识别
  • nuclei验证漏洞
  • Webhook结果推送
  • 保存到文件或者打印

  • API响应返回

指纹规则

管理更新

  • 指纹库

https://github.com/0x727/FingerprintHub

  • 以组件名命名单个yaml文件,多个指纹规则。
name: 0example
priority: 3
nuclei_tags:
  - []
fingerprint:
  - path: /
    request_method: get
    request_headers: {}
    request_data: ''
    status_code: 0
    headers: {}
    keyword:
      - <title>Example Domain</title>
    favicon_hash: []
  • FingerprintHub会将指纹扁平化处理,将namepriority放进单个指纹中。
{
    "path": "/",
    "request_method": "get",
    "request_headers": {},
    "request_data": "",
    "status_code": 0,
    "headers": {},
    "keyword": [
      "<title>Example Domain</title>"
    ],
    "favicon_hash": [],
    "priority": 3,
    "name": "0example"
  },

指纹分类

  • 指纹序列化后会在Rust数据结构中分为:请求部分和匹配部分,这样可以把相同请求的指纹归整在一起,在函数缓存中方便Hash。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct V3WebFingerPrint {
    #[serde(default)]
    pub name: String,
    #[serde(default)]
    pub priority: u32,
    pub request: WebFingerPrintRequest,
    pub match_rules: WebFingerPrintMatch,
}
  • 根据请求的路径和请求方法是否是自定义请求,是否有favicon_hash来分类到:首页匹配图标匹配自定义匹配

请求逻辑

  • 流程图
flowchart TD
subgraph gg[结束]
结束
end
subgraph request [请求逻辑]
目标([目标])-->判断协议{是否有协议判断}
判断协议-->|有| 首页请求[首页请求]
判断协议-->|没有| 添加协议[[添加HTTPS协议]]
添加协议-->首页请求
首页请求-->是否有响应{是否有响应}
是否有响应-->|HTTPS没有响应| 添加HTTP协议[[换HTTP协议]]
添加HTTP协议-->首页请求
是否有响应-->|有响应| 生成匹配数据[[生成匹配数据]]
是否有响应-->|没有响应| 结束([结束])
生成匹配数据-->是否有跳转{是否有跳转}
是否有跳转-->|有跳转|是否超过最大跳转次数{是否超过最大跳转次数}
是否超过最大跳转次数-->|没有|继续请求[[继续请求]]
继续请求[[继续请求]]-->是否有响应{是否有响应}
是否有跳转-->|没有跳转|匹配数据列表[(添加匹配数据到列表)]
是否超过最大跳转次数 -->|超过最大跳转次数| 匹配数据列表[(添加匹配数据到列表)]
end
subgraph matcher [Web匹配逻辑]
匹配数据列表-->是否有图标Hash{是否有图标Hash}
是否有图标Hash-->|有| 匹配图标Hash指纹[[匹配图标Hash指纹]]
是否有图标Hash-->|没有| 匹配图标关键词指纹[[匹配图标关键词指纹]]
匹配图标Hash指纹-->是否有交集{是否有交集}
是否有交集-->|真|匹配到Web指纹
是否有交集-->|假|跳过
匹配图标关键词指纹-->短路求值{短路求值}
短路求值-->|真|匹配到Web指纹
短路求值-->|假|跳过
跳过-->结束
end
subgraph info [补充指纹信息]
匹配到Web指纹-->是否开启服务识别{是否开启服务识别}
是否开启服务识别-->|开启|服务识别[[补充服务信息]]
是否开启服务识别-->|没有开启|是否调用nuclei{是否调用nuclei}
服务识别-->是否调用nuclei{是否调用nuclei}
是否调用nuclei-->|调用|调用nuclei[[调用nuclei]]
是否调用nuclei-->|没有调用|结果队列[(结果队列)]
调用nuclei-->结果队列[(结果队列)]
end
subgraph result [结果处理]
结果队列-->是否开启Webhook{是否开启Webhook}
是否开启Webhook-->|开启|调用结果到Webhook服务器[[调用结果到Webhook服务器]]
是否开启Webhook-->|没有开启|是否需要保持到文件{是否需要保持到文件}
调用结果到Webhook服务器-->是否需要保持到文件{是否需要保持到文件}
是否需要保持到文件-->|是|保存到文件[[保存到文件]]
保存到文件-->打印结果到屏幕[[打印结果到屏幕]]
是否需要保持到文件-->|否|打印结果到屏幕[[打印结果到屏幕]]
打印结果到屏幕[[打印结果到屏幕]]-->结束([结束])
end

生成匹配数据

  • 匹配数据是什么?就是指纹规则中的可以匹配的字段,比如:响应体和响应头中的关键词响应状态码图标Hash组成的数据结构。
  • 每一个请求响应都会生成匹配数据
  • 匹配结构
pub struct RawData {
    pub url: Url,
    pub path: String,
    pub headers: reqwest::header::HeaderMap,
    pub status_code: reqwest::StatusCode,
    pub text: String,
    pub favicon: HashMap<String, String>,
    pub next_url: Option<Url>,
}
  • 提取跳转URL图标URL网页标题的正则表达式
  • 网页标题的来源:title标签和meta标签的content内容。
pub fn get_title(text: &str) -> String {
    for titles in Document::from(text).find(Name("title")) {
        if !titles.text().is_empty() {
            return titles.text();
        }
    }
    for titles in Document::from(text).find(Name("meta")) {
        if titles.attr("property") == Some("title") {
            return titles.attr("content").unwrap_or_default().to_string();
        }
    }
    String::new()
}
  • 获取网页图标的代码
fn get_favicon_link(text: &str, base_url: &Url) -> HashSet<Url> {
    let mut icon_links = HashSet::new();
    for links in Document::from(text).find(Name("link")) {
        if let (Some(rel), Some(href)) = (links.attr("rel"), links.attr("href")) {
            if ["icon", "shortcut icon"].contains(&rel) {
                if href.starts_with("http://") || href.starts_with("https://") {
                    let favicon_url = Url::parse(href).unwrap_or_else(|_| base_url.clone());
                    icon_links.insert(favicon_url);
                } else {
                    let favicon_url = base_url.join(href).unwrap_or_else(|_| base_url.clone());
                    icon_links.insert(favicon_url);
                }
            }
        }
    }
    if let Ok(favicon_url) = base_url.join("/favicon.ico") {
        icon_links.insert(favicon_url);
    }
    icon_links
}
  • 获取下一跳地址:首先是30x状态码的请求头LOCATION跳转链接,然后判断meta标签是否有http-equiv属性为的refresh的再回去url属性得到跳转地址,剩下的就只有正则匹配获取了。
 fn get_next_jump(headers: &HeaderMap, url: &Url, text: &str) -> Option<Url> {
    let mut next_url_list = Vec::new();
    if let Some(location) = headers
        .get(LOCATION)
        .and_then(|location| location.to_str().ok())
    {
        next_url_list.push(location.to_string());
    }
    if next_url_list.is_empty() {
        for metas in Document::from(text).find(Name("meta")) {
            if let (Some(url),Some(http_equiv)) = (metas.attr("url"),metas.attr("http-equiv")) {
                if http_equiv == "refresh"{
                    next_url_list.push(url.to_string());
                }
            }
        }
    }
    if next_url_list.is_empty() && text.len() <= 1024 {
        for reg in RE_COMPILE_BY_JUMP.iter() {
            if let Some(x) = reg.captures(text) {
                let mut u = x.name("name").map_or("", |m| m.as_str()).to_string();
                u = u.replace('\\'', "").replace('\\"', "");
                next_url_list.push(u);
            }
        }
    }
    if let Some(next_url) = next_url_list.into_iter().next() {
        return if next_url.starts_with("http://") || next_url.starts_with("https://") {
            match Url::parse(&next_url) {
                Ok(next_path) => Some(next_path),
                Err(_) => None,
            }
        } else if let Ok(next_path) = url.join(&next_url) {
            Some(next_path)
        } else {
            None
        };
    };
    None
}

网页编码

lazy_static! {    static ref RE_COMPILE_BY_CHARSET: Regex =        Regex::new(r#"(?im)charset="(.*?)"|charset=(.*?)""#).expect("RE_COMPILE_BY_CHARSET");}
  • 通过网页head标签中的charset获取编码类型,没有就从请求头中的CONTENT_TYPE中获取,都没有默认utf-8

单元测试

  • 正常https目标
#[tokio::test]
    async fn test_send_requests() {
        let test_url = Url::parse("<https://httpbin.org/>").unwrap();
        let fingerprint = WebFingerPrintRequest {
            path: String::from("/"),
            request_method: String::from("GET"),
            request_headers: Default::default(),
            request_data: Str
ing::from(""),
        };
        let timeout = 10_u64;
        let request_config = RequestOption::new(&timeout, "");
        let res = send_requests(&test_url, &fingerprint, &request_config)
            .await
            .unwrap();
        assert!(res.text().await.unwrap().contains("swagger-ui"));
    }

badssl.com

Github Action

https://github.com/0x727/FingerprintHub

指纹更新

  • 每次commit触发pre-commit.yml格式化检查yaml文件,重新生成web_fingerprint_v3.json指纹文件和nuclei的对应关系
  • 同时定时每6小时更新一次nuclei-templates项目的插件,根据nuclei和指纹组件的对应关系将插件复制到以组件名为目录。

https://github.com/0x727/ObserverWard

  • 单元测试和编译测试,格式化代码,和代码风格规范
  • dependabot.yml依赖库版本过低提醒,会自动提交PR,又会触发上面的测试CI,当全部测试通过后,可以合并更新最新依赖库。比封我号的机器人好多了!
  • post-release.yml当我发布版本打tag标签时触发CI根据CHANGELOG.md生成版本描述并且创建release编译和发布版本。

指纹审核

  • 流程:根据issue模板添加指纹,action解析issue内容,生成一个yaml文件,和一个测试目标,机器人使用侦查守卫自动验证指纹,发表评论,等待管理员审核,管理员添加审核通过标签后触发合并action。
flowchart TD
subgraph add [添加指纹]
打开issue添加指纹([打开issue添加指纹])-->解析issue内容{是否符合格式要求}
解析issue内容-->|符合| 验证指纹[验证指纹]
解析issue内容-->|不符合| 提示并且关闭issue[[提示并且关闭issue]]
提示并且关闭issue-->gg[[结束]]
验证指纹-->是否识别成功{是否识别成功}
是否识别成功-->|是| 待审核标签[[待审核标签]]
是否识别成功-->|否| 修改yaml指纹[[评论修改yaml指纹]]
修改yaml指纹-->验证指纹[[验证指纹]]
待审核标签-->是否审核通过[[是否审核通过]]
是否审核通过-->|是| 通过标签[[通过标签]]
是否审核通过-->|否| 原因[[原因]]
通过标签-->合并请求[[合并请求]]
end
  • 类型:添加指纹,修改指纹,删除指纹
    • 添加指纹:标题为:添加指纹-[组件名称],评论内容为Yaml格式,测试目标使用

请在下方留下您的评论.加入TG吹水群