Skip to content

Commit 37b7f2b

Browse files
committed
new: several improvements to the port.scanner banner grabbing and protocol detection
1 parent ae97a46 commit 37b7f2b

File tree

3 files changed

+155
-66
lines changed

3 files changed

+155
-66
lines changed

src/plugins/port_scanner/grabbers/http.rs

Lines changed: 116 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,121 @@ pub(crate) fn is_http_port(opts: &options::Options, port: u16) -> (bool, bool) {
4646
(false, false)
4747
}
4848

49+
pub(crate) async fn parse_http_response(
50+
opts: &options::Options,
51+
response: reqwest::Response,
52+
banner: &mut Banner,
53+
address: &str,
54+
port: u16,
55+
) {
56+
let headers_of_interest: Vec<&str> = opts
57+
.port_scanner_http_headers
58+
.split(',')
59+
.map(|s| s.trim())
60+
.filter(|s| !s.is_empty())
61+
.collect();
62+
let mut content_type = String::from("text/html");
63+
64+
// collect headers
65+
for (name, value) in response.headers() {
66+
let name = name.to_string();
67+
let mut value = value.to_str().unwrap();
68+
69+
if name == "content-type" {
70+
if value.contains(';') {
71+
value = value.split(';').next().unwrap();
72+
}
73+
value.clone_into(&mut content_type);
74+
}
75+
76+
if headers_of_interest.contains(&name.as_str()) {
77+
banner.insert(name, value.to_owned());
78+
}
79+
}
80+
81+
// collect info from html
82+
let body = response.text().await;
83+
if let Ok(body) = body {
84+
if content_type.contains("text/html") {
85+
if let Some(caps) = HTML_TITLE_PARSER.captures(&body) {
86+
banner.insert(
87+
"html.title".to_owned(),
88+
caps.get(1).unwrap().as_str().trim().to_owned(),
89+
);
90+
}
91+
} else if content_type.contains("application/") || content_type.contains("text/") {
92+
banner.insert("body".to_owned(), body.to_owned());
93+
}
94+
} else {
95+
log::debug!(
96+
"can't read response body from {}:{}: {:?}",
97+
address,
98+
port,
99+
body.err()
100+
);
101+
}
102+
}
103+
104+
pub(crate) async fn parse_http_raw_response(
105+
opts: &options::Options,
106+
response: &str,
107+
banner: &mut Banner,
108+
) {
109+
let headers_of_interest: Vec<&str> = opts
110+
.port_scanner_http_headers
111+
.split(',')
112+
.map(|s| s.trim())
113+
.filter(|s| !s.is_empty())
114+
.collect();
115+
let mut content_type = String::from("text/html");
116+
117+
// split response into headers and body
118+
let lines = response.lines();
119+
let mut headers_section = true;
120+
let mut body_lines = Vec::new();
121+
122+
for line in lines {
123+
if headers_section {
124+
if line.trim().is_empty() {
125+
// empty line indicates end of headers
126+
headers_section = false;
127+
} else if let Some(colon_pos) = line.find(':') {
128+
let name = line[..colon_pos].trim().to_lowercase();
129+
let value = line[colon_pos + 1..].trim();
130+
131+
if name == "content-type" {
132+
let mut ct_value = value;
133+
if ct_value.contains(';') {
134+
ct_value = ct_value.split(';').next().unwrap();
135+
}
136+
ct_value.clone_into(&mut content_type);
137+
}
138+
139+
if headers_of_interest.contains(&name.as_str()) {
140+
banner.insert(name, value.to_owned());
141+
}
142+
}
143+
} else {
144+
body_lines.push(line);
145+
}
146+
}
147+
148+
// process body
149+
if !body_lines.is_empty() {
150+
let body = body_lines.join("\n");
151+
if content_type.contains("text/html") {
152+
if let Some(caps) = HTML_TITLE_PARSER.captures(&body) {
153+
banner.insert(
154+
"html.title".to_owned(),
155+
caps.get(1).unwrap().as_str().trim().to_owned(),
156+
);
157+
}
158+
} else if content_type.contains("application/") || content_type.contains("text/") {
159+
banner.insert("body".to_owned(), body);
160+
}
161+
}
162+
}
163+
49164
pub(crate) async fn http_grabber(
50165
opts: &options::Options,
51166
host: &str,
@@ -161,52 +276,7 @@ pub(crate) async fn http_grabber(
161276
.await;
162277

163278
if let Ok(resp) = resp {
164-
let headers_of_interest: Vec<&str> = opts
165-
.port_scanner_http_headers
166-
.split(',')
167-
.map(|s| s.trim())
168-
.filter(|s| !s.is_empty())
169-
.collect();
170-
let mut content_type = String::from("text/html");
171-
172-
// collect headers
173-
for (name, value) in resp.headers() {
174-
let name = name.to_string();
175-
let mut value = value.to_str().unwrap();
176-
177-
if name == "content-type" {
178-
if value.contains(';') {
179-
value = value.split(';').next().unwrap();
180-
}
181-
value.clone_into(&mut content_type);
182-
}
183-
184-
if headers_of_interest.contains(&name.as_str()) {
185-
banner.insert(name, value.to_owned());
186-
}
187-
}
188-
189-
// collect info from html
190-
let body = resp.text().await;
191-
if let Ok(body) = body {
192-
if content_type.contains("text/html") {
193-
if let Some(caps) = HTML_TITLE_PARSER.captures(&body) {
194-
banner.insert(
195-
"html.title".to_owned(),
196-
caps.get(1).unwrap().as_str().trim().to_owned(),
197-
);
198-
}
199-
} else if content_type.contains("application/") || content_type.contains("text/") {
200-
banner.insert("body".to_owned(), body.to_owned());
201-
}
202-
} else {
203-
log::debug!(
204-
"can't read response body from {}:{}: {:?}",
205-
address,
206-
port,
207-
body.err()
208-
);
209-
}
279+
parse_http_response(opts, resp, &mut banner, address, port).await;
210280
} else {
211281
log::debug!(
212282
"can't connect via http client to {}:{}: {:?}",

src/plugins/port_scanner/grabbers/line.rs

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
11
use tokio::io::{AsyncReadExt, AsyncWriteExt};
22

33
use super::Banner;
4-
use crate::utils::net::StreamLike;
5-
use std::time::Duration;
4+
use crate::{
5+
plugins::port_scanner::{grabbers::http::parse_http_raw_response, options},
6+
utils::net::StreamLike,
7+
};
8+
use std::time::{Duration, Instant};
69

7-
async fn read_line_from(mut stream: Box<dyn StreamLike>) -> String {
8-
let mut line = String::new();
10+
async fn read_response_from(mut stream: Box<dyn StreamLike>, timeout: Duration) -> String {
11+
let mut response = String::new();
912
let mut buf: [u8; 1] = [0];
10-
let max = 1024;
13+
let max = 1024 * 4;
14+
let started = Instant::now();
1115

1216
for _ in 0..max {
13-
let read = stream.read_exact(&mut buf).await;
14-
if read.is_ok() {
15-
let c = buf[0] as char;
16-
if c == '\n' {
17+
if let Ok(read) = tokio::time::timeout(timeout, stream.read_exact(&mut buf)).await {
18+
if read.is_ok() {
19+
response.push(buf[0] as char);
20+
} else {
21+
log::debug!("{:?}", read);
1722
break;
1823
}
19-
line.push(c);
20-
} else {
21-
log::debug!("{:?}", read);
24+
}
25+
26+
if started.elapsed() > timeout {
27+
log::debug!("timeout={:?} after {} bytes", timeout, response.len());
2228
break;
2329
}
2430
}
2531

26-
line
32+
response
2733
}
2834

2935
pub(crate) async fn line_grabber(
36+
opts: &options::Options,
3037
address: &str,
3138
port: u16,
3239
mut stream: Box<dyn StreamLike>,
@@ -37,13 +44,25 @@ pub(crate) async fn line_grabber(
3744
let mut banner = Banner::default();
3845

3946
// send something
40-
let _ = stream.write_all("hello\r\n\r\n".as_bytes()).await;
47+
let _ = stream
48+
.write_all(format!("GET / HTTP/1.1\r\nHost: {}\r\n\r\n", address).as_bytes())
49+
.await;
4150

42-
let timeout = std::time::Duration::from_millis((timeout.as_millis() / 2) as u64);
43-
if let Ok(line) = tokio::time::timeout(timeout, read_line_from(stream)).await
44-
&& !line.is_empty()
45-
{
46-
banner.insert("line".to_owned(), line);
51+
let response = read_response_from(stream, timeout).await;
52+
if !response.is_empty() {
53+
// if we have an http response ...
54+
if response.contains("HTTP/") {
55+
banner.insert("protocol".to_owned(), "http".to_owned());
56+
parse_http_raw_response(opts, &response, &mut banner).await;
57+
} else {
58+
banner.insert(
59+
"data".to_owned(),
60+
response
61+
.trim()
62+
.replace("\r\n", "<crlf>")
63+
.replace("\n", "<lf>"),
64+
);
65+
}
4766
}
4867

4968
banner

src/plugins/port_scanner/grabbers/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ pub(crate) async fn grab_tcp_banner(
2929
http::http_grabber(opts, host, address, port, stream, with_ssl, timeout).await
3030
} else {
3131
// default to an attempt at line grabbing
32-
line::line_grabber(address, port, stream, timeout).await
32+
line::line_grabber(opts, address, port, stream, timeout).await
3333
}
3434
}
3535

0 commit comments

Comments
 (0)