Udp Scanning (#596)

Added UDP scanning support to RustScan
This commit is contained in:
BrendanGlancy authored and GitHub committed 2024-06-25 15:16:06 +01:00
1 parent f33476468f
commit f544f2240b
7 files changed
+556 -12

No files matched your search

+9 -1
View File
@@ -150,6 +150,10 @@ pub struct Opts {
/// A list of comma separated ports to be excluded from scanning. Example: 80,443,8080.
#[arg(short, long, value_delimiter = ',')]
pub exclude_ports: Option<Vec<u16>>,
/// UDP scanning mode, finds UDP ports that send back responses
#[arg(long)]
pub udp: bool,
}
#[cfg(not(tarpaulin_include))]
@@ -189,7 +193,7 @@ impl Opts {
merge_required!(
addresses, greppable, accessible, batch_size, timeout, tries, scan_order, scripts,
command
command, udp
);
}
@@ -237,6 +241,7 @@ impl Default for Opts {
scripts: ScriptsRequired::Default,
config_path: None,
exclude_ports: None,
udp: false,
}
}
}
@@ -261,6 +266,7 @@ pub struct Config {
command: Option<Vec<String>>,
scripts: Option<ScriptsRequired>,
exclude_ports: Option<Vec<u16>>,
udp: Option<bool>,
}
#[cfg(not(tarpaulin_include))]
@@ -276,6 +282,7 @@ impl Config {
/// greppable = true
/// scan_order: "Serial"
/// exclude_ports = [8080, 9090, 80]
/// udp = false
///
pub fn read(custom_config_path: Option<PathBuf>) -> Self {
let mut content = String::new();
@@ -332,6 +339,7 @@ mod tests {
scan_order: Some(ScanOrder::Random),
scripts: None,
exclude_ports: None,
udp: Some(false),
}
}
}
+3
View File
@@ -31,6 +31,7 @@
//! strategy,
//! true,
//! vec![9000],
//! false,
//! );
//!
//! let scan_result = block_on(scanner.run());
@@ -53,3 +54,5 @@ pub mod benchmark;
pub mod scripts;
pub mod address;
pub mod udp_packets;
+3
View File
@@ -83,6 +83,8 @@ fn main() {
// Added by wasuaje - 01/26/2024:
// exclude_ports is an exclusion port list
//
// Added by brendanglancy - 5/19/2024:
// udp is an option to do a udp scan
let scanner = Scanner::new(
&ips,
batch_size,
@@ -92,6 +94,7 @@ fn main() {
PortStrategy::pick(&opts.range, opts.ports, opts.scan_order),
opts.accessible,
opts.exclude_ports.unwrap_or_default(),
opts.udp,
);
debug!("Scanner finished building: {:?}", scanner);
+204 -11
View File
@@ -1,13 +1,14 @@
//! Core functionality for actual scanning behaviour.
use crate::port_strategy::PortStrategy;
use crate::udp_packets::udp_payload::cust_payload;
use log::debug;
mod socket_iterator;
use socket_iterator::SocketIterator;
use async_std::io;
use async_std::net::TcpStream;
use async_std::prelude::*;
use async_std::{io, net::UdpSocket};
use colored::Colorize;
use futures::stream::FuturesUnordered;
use std::{
@@ -36,6 +37,7 @@ pub struct Scanner {
port_strategy: PortStrategy,
accessible: bool,
exclude_ports: Vec<u16>,
udp: bool,
}
// Allowing too many arguments for clippy.
@@ -50,6 +52,7 @@ impl Scanner {
port_strategy: PortStrategy,
accessible: bool,
exclude_ports: Vec<u16>,
udp: bool,
) -> Self {
Self {
batch_size,
@@ -60,6 +63,7 @@ impl Scanner {
ips: ips.iter().map(ToOwned::to_owned).collect(),
accessible,
exclude_ports,
udp,
}
}
@@ -130,25 +134,32 @@ impl Scanner {
///
/// Note: `self` must contain `self.ip`.
async fn scan_socket(&self, socket: SocketAddr) -> io::Result<SocketAddr> {
let tries = self.tries.get();
if self.udp {
let payload = cust_payload(socket.port());
let tries = self.tries.get();
for _ in 1..=tries {
match self.udp_scan(socket, &payload, self.timeout).await {
Ok(true) => return Ok(socket),
Ok(false) => continue,
Err(e) => return Err(e),
}
}
return Ok(socket);
}
let tries = self.tries.get();
for nr_try in 1..=tries {
match self.connect(socket).await {
Ok(x) => {
Ok(tcp_stream) => {
debug!(
"Connection was successful, shutting down stream {}",
&socket
);
if let Err(e) = x.shutdown(Shutdown::Both) {
if let Err(e) = tcp_stream.shutdown(Shutdown::Both) {
debug!("Shutdown stream error {}", &e);
}
if !self.greppable {
if self.accessible {
println!("Open {socket}");
} else {
println!("Open {}", socket.to_string().purple());
}
}
self.fmt_ports(socket);
debug!("Return Ok after {} tries", nr_try);
return Ok(socket);
@@ -191,6 +202,90 @@ impl Scanner {
.await?;
Ok(stream)
}
/// Binds to a UDP socket so we can send and recieve packets
/// # Example
///
/// ```compile_fail
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
/// let port: u16 = 80;
/// // ip is an IpAddr type
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
/// let socket = SocketAddr::new(ip, port);
/// scanner.udp_bind(socket);
/// // returns Result which is either Ok(stream) for port is open, or Err for port is closed.
/// // Timeout occurs after self.timeout seconds
/// ```
///
async fn udp_bind(&self, socket: SocketAddr) -> io::Result<UdpSocket> {
let local_addr = match socket {
SocketAddr::V4(_) => "0.0.0.0:0".parse::<SocketAddr>().unwrap(),
SocketAddr::V6(_) => "[::]:0".parse::<SocketAddr>().unwrap(),
};
UdpSocket::bind(local_addr).await
}
/// Performs a UDP scan on the specified socket with a payload and wait duration
/// # Example
///
/// ```compile_fail
/// # use std::net::{IpAddr, Ipv6Addr, SocketAddr};
/// # use std::time::Duration;
/// let port: u16 = 123;
/// // ip is an IpAddr type
/// let ip = IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1));
/// let socket = SocketAddr::new(ip, port);
/// let payload = vec![0, 1, 2, 3];
/// let wait = Duration::from_secs(1);
/// let result = scanner.udp_scan(socket, payload, wait).await;
/// // returns Result which is either Ok(true) if response received, or Ok(false) if timed out.
/// // Err is returned for other I/O errors.
async fn udp_scan(
&self,
socket: SocketAddr,
payload: &[u8],
wait: Duration,
) -> io::Result<bool> {
match self.udp_bind(socket).await {
Ok(udp_socket) => {
let mut buf = [0u8; 1024];
udp_socket.connect(socket).await?;
udp_socket.send(payload).await?;
match io::timeout(wait, udp_socket.recv(&mut buf)).await {
Ok(size) => {
debug!("Received {} bytes", size);
self.fmt_ports(socket);
Ok(true)
}
Err(e) => {
if e.kind() == io::ErrorKind::TimedOut {
Ok(false)
} else {
Err(e)
}
}
}
}
Err(e) => {
println!("Err E binding sock {:?}", e);
Err(e)
}
}
}
/// Formats and prints the port status
fn fmt_ports(&self, socket: SocketAddr) {
if !self.greppable {
if self.accessible {
println!("Open {socket}");
} else {
println!("Open {}", socket.to_string().purple());
}
}
}
}
#[cfg(test)]
@@ -218,6 +313,7 @@ mod tests {
strategy,
true,
vec![9000],
false,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
@@ -241,6 +337,7 @@ mod tests {
strategy,
true,
vec![9000],
false,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
@@ -263,6 +360,7 @@ mod tests {
strategy,
true,
vec![9000],
false,
);
block_on(scanner.run());
assert_eq!(1, 1);
@@ -284,6 +382,7 @@ mod tests {
strategy,
true,
vec![9000],
false,
);
block_on(scanner.run());
assert_eq!(1, 1);
@@ -308,6 +407,100 @@ mod tests {
strategy,
true,
vec![9000],
false,
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn udp_scan_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["127.0.0.1".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
10,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
true,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
assert_eq!(1, 1);
}
#[test]
fn udp_ipv6_runs() {
// Makes sure the program still runs and doesn't panic
let addrs = vec!["::1".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
10,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
true,
);
block_on(scanner.run());
// if the scan fails, it wouldn't be able to assert_eq! as it panicked!
assert_eq!(1, 1);
}
#[test]
fn udp_quad_zero_scanner_runs() {
let addrs = vec!["0.0.0.0".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 1,
end: 1_000,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
10,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
true,
);
block_on(scanner.run());
assert_eq!(1, 1);
}
#[test]
fn udp_google_dns_runs() {
let addrs = vec!["8.8.8.8".parse::<IpAddr>().unwrap()];
let range = PortRange {
start: 100,
end: 150,
};
let strategy = PortStrategy::pick(&Some(range), None, ScanOrder::Random);
let scanner = Scanner::new(
&addrs,
10,
Duration::from_millis(100),
1,
true,
strategy,
true,
vec![9000],
true,
);
block_on(scanner.run());
assert_eq!(1, 1);
+4
View File
@@ -89,10 +89,14 @@ macro_rules! funny_opening {
"TCP handshake? More like a friendly high-five!",
"Scanning ports: The virtual equivalent of knocking on doors.",
"RustScan: Making sure 'closed' isn't just a state of mind.",
"RustScan: allowing you to send UDP packets into the void 1200x faster than NMAP",
"Port scanning: Because every port has a story to tell.",
"I scanned ports so fast, even my computer was surprised.",
"Scanning ports faster than you can say 'SYN ACK'",
"RustScan: Where '404 Not Found' meets '200 OK'.",
"RustScan: Exploring the digital landscape, one IP at a time.",
"TreadStone was here 🚀",
"With RustScan, I scan ports so fast, even my firewall gets whiplash 💨",
];
let random_quote = quotes.choose(&mut rand::thread_rng()).unwrap();
+279
View File
@@ -0,0 +1,279 @@
pub mod udp_payload {
// const MAX_PAYLOAD_IPV4: usize = 65507; // 65,535 - 20 (IP header) - 8 (UDP header)
// const MAX_PAYLOAD_IPV6: usize = 65527; // 65,535 - 40 (IPv6 header) - 8 (UDP header)
/// From [RFC 768](https://www.rfc-editor.org/rfc/rfc768.txt)
/// We send blank UDP packets, unless the port is determined to be special.
///
/// ```text
/// HEADER Handled by the OS
/// 0 1 2 3
/// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | Source Port | Destination Port |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | Length | Checksum |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// We append the data, based on the port the the header
/// DATA
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// | Data (based on port) |
/// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/// ```
///
/// # Fields
///
/// - **Source Port**: The source port number.
/// - **Destination Port**: The destination port number.
/// - **Length**: The length of the UDP header and data.
/// - **Checksum**: The checksum for error-checking.
/// - **Data**: The payload data, which can vary based on the port.
#[allow(clippy::match_single_binding)]
pub fn cust_payload(dst_prt: u16) -> Vec<u8> {
match dst_prt {
// 53 => craft_dns_query_packet(),
// 67 => craft_dhcpc_packet(),
// 123 => craft_ntp_packet(),
// 161 => craft_snmp_packet(),
_ => vec![],
}
}
pub fn craft_snmptrap_packet() -> Vec<u16> {
let version = vec![0x30, 0x82]; // SNMP version
let community_string = vec![0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63]; // Community string "public"
let pdu_type = vec![0xa4, 0x82]; // PDU type for SNMP Trap
let enterprise_oid = vec![0x06, 0x09, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00]; // Enterprise OID
// Combine the components into a single SNMP Trap packet
[version, community_string, pdu_type, enterprise_oid].concat()
}
/// From [RFC 1592](https://www.rfc-editor.org/rfc/rfc1592.txt)
/// Crafts an SNMP packet based on the structure from RFC 1592.
///
/// RFC 1592 defines the structure of an SNMP packet as follows
/// - **ASN.1 header**: 0x30
/// - **PDU length**: 37 + length of community name
/// - **SNMP version**: 0x02 0x01 0x00 (integer, length=1, value=0)
/// - **Community name**: A string of varying length
/// - **SNMP GET request**: 0xa0 0x1c (request type=0xa0, length=0x1c)
/// - **SNMP request ID**: 0x02 0x01 0x01 (integer, length=1, ID=1)
/// - **SNMP error status**: 0x02 0x01 0x00 (integer, length=1, error=0)
/// - **SNMP index**: 0x02 0x01 0x00 (integer, length=1, index=0)
/// - **VarBind list**: 0x30 0x11 (length=0x11)
/// - **VarBind**: 0x30 0x0f (length=0x0f)
/// - **Object ID**: 0x06 0x0b (length=0x0b)
/// - **Object instance**: 1.3.6.1.4.1.2.2.1.1.1.0
/// - **Null value**: 0x05 0x00 (length=0)
///
/// The PDU length formula:
/// ```text
/// PDU_length = length of version field and string tag (4 bytes)
/// + length of community length field (1 byte)
/// + length of community name (depends...)
/// + length of SNMP GET request (32 bytes)
/// ```
///
/// # Returns
///
/// A `Vec<u8>` representing the crafted SNMP packet.
pub fn craft_snmp_packet() -> Vec<u8> {
// TODO all versions of snmp
let v1 = vec![0x30, 0x29, 0x02, 0x01, 0x00];
let community_string = vec![0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63];
let pdu_type = vec![0xa0, 0x1c];
let request_id = vec![0x02, 0x04, 0x7a, 0x69, 0x67, 0x71];
let error_status = vec![0x02, 0x01, 0x00];
let error_index = vec![0x02, 0x01, 0x00];
let variable_bindings = vec![
0x30, 0x0e, 0x30, 0x0c, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x01, 0x01, 0x00,
0x05, 0x00,
];
let _basic_snmp = [
0x30, 0x26, 0x02, 0x01, 0x01, 0x04, 0x06, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0xa0,
0x19, 0x02, 0x04, 0x71, 0x64, 0xfe, 0xf1, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, 0x30,
0x0b, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x06, 0x01, 0x02, 0x01, 0x05, 0x00,
];
[
v1,
community_string,
pdu_type,
request_id,
error_status,
error_index,
variable_bindings,
]
.concat()
}
/// Constructs a DHCP packet based on the RFC 1541 Dynamic Host Configuration Protocol specification.
///
///
/// # Returns
///
/// A `Vec<u8>` representing the DHCP packet.
///
/// This function crafts a DHCP Discover packet with the following fields:
/// - **op**: BOOTREQUEST (1)
/// - **htype**: Ethernet (1)
/// - **hlen**: 6
/// - **hops**: 0
/// - **xid**: Random transaction ID
/// - **secs**: 0
/// - **flags**: 0x8000 (Broadcast)
/// - **ciaddr**: 0.0.0.0
/// - **yiaddr**: 0.0.0.0
/// - **siaddr**: 0.0.0.0
/// - **giaddr**: 0.0.0.0
/// - **chaddr**: 00:00:00:00:00:00 (Client MAC address)
/// - **sname**: Not given
/// - **file**: Not given
/// - **options**: DHCP options
pub fn craft_dhcpc_packet() -> Vec<u8> {
let mut packet = Vec::with_capacity(240);
// BOOTP message type: Boot Request (1)
packet.push(0x01);
// Hardware type: Ethernet (10Mb) (1)
packet.push(0x01);
// Hardware address length: 6
packet.push(0x06);
// Hops: 0
packet.push(0x00);
// Transaction ID: random (0x3903F326)
packet.extend(&[0x39, 0x03, 0xF3, 0x26]);
// Seconds elapsed: 0
packet.extend(&[0x00, 0x00]);
// Bootp flags: 0x8000 (Broadcast) (0x8000)
packet.extend(&[0x80, 0x00]);
// Client IP address: 0.0.0.0
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
// Your (client) IP address: 0.0.0.0
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
// Next server IP address: 0.0.0.0
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
// Relay agent IP address: 0.0.0.0
packet.extend(&[0x00, 0x00, 0x00, 0x00]);
// Client MAC address: 00:00:00:00:00:00
packet.extend(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
// Client hardware address padding: 00000000000000000000
packet.extend(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
// Server host name not given
packet.extend(&[0x00; 64]);
// Boot file name not given
packet.extend(&[0x00; 128]);
// Magic cookie: DHCP
packet.extend(&[0x63, 0x82, 0x53, 0x63]);
// DHCP Message Type: DHCP Discover (1)
packet.extend(&[0x35, 0x01, 0x01]);
// DHCP Option: Parameter Request List
packet.extend(&[0x37, 0x04, 0x01, 0x03, 0x06, 0x2a]);
// End Option
packet.push(0xff);
packet
}
/// Constructs a DNS query packet.
///
/// The structure of the DNS packet is as follows:
///
/// ```text
/// +---------------------+
/// | Header |
/// +---------------------+
/// | Question | the question for the name server
/// +---------------------+
/// | Answer | RRs answering the question
/// +---------------------+
/// | Authority | RRs pointing toward an authority
/// +---------------------+
/// | Additional | RRs holding additional information
/// +---------------------+
/// ```
///
/// The function builds a DNS query packet with the following fields:
/// - **Header**: Contains the transaction ID and flags.
/// - **Question**: Specifies the query (e.g., the domain name to look up).
/// - **Answer**: Resource records that answer the question (empty for a query).
/// - **Authority**: Resource records that point toward an authority (empty for a query).
/// - **Additional**: Resource records holding additional information (empty for a query).
///
/// # Returns
///
/// A `Vec<u8>` representing the DNS query packet.
pub fn craft_dns_query_packet() -> Vec<u8> {
let mut packet = Vec::new();
// Transaction ID: Random
packet.extend(&[0x12, 0x34]);
// Flags: Standard query
packet.extend(&[0x01, 0x00]);
// Header ^
// Questions: 1
packet.extend(&[0x00, 0x01]);
// Answer RRs: 0
packet.extend(&[0x00, 0x00]);
// Authority RRs: 0
packet.extend(&[0x00, 0x00]);
// Additional RRs: 0
packet.extend(&[0x00, 0x00]);
// Query: www.google.com
let query_name = "www.google.com";
for part in query_name.split('.') {
packet.push(part.len() as u8);
packet.extend(part.as_bytes());
}
packet.push(0); // End of query name
// Query Type: A (host address)
packet.extend(&[0x00, 0x01]);
// Query Class: IN (internet)
packet.extend(&[0x00, 0x01]);
packet
}
/// Creates the simplest NTP packet possible
/// 0x1b followed by 47 0s, (Version = 3, Mode = 3)
pub fn craft_ntp_packet() -> Vec<u8> {
let mut packet = vec![0u8; 48];
packet[0] = 0x1b;
packet
}
// TODO add more packets for the top 1000 ports
// `sort -r -k3 /usr/share/nmap/nmap-services | grep udp | head -n 1000`
// There has to be a better way to structure this, maybe internal crate for packet payload
// creation
}
+54
View File
@@ -106,4 +106,58 @@ mod timelimits {
super::Duration::from_secs(26),
);
}
#[test]
#[ignore]
fn udp_scan_localhost() {
let timeout = super::Duration::from_secs(25);
super::run_rustscan_with_timeout(&["--greppable", "127.0.0.1", "--udp"], timeout);
}
#[test]
#[ignore]
fn udp_scan_google_com() {
super::run_rustscan_with_timeout(
&[
"--udp",
"--greppable",
"-u",
"5000",
"-b",
"2500",
"google.com",
],
super::Duration::from_secs(28),
);
}
#[test]
#[ignore]
fn udp_scan_example_com() {
super::run_rustscan_with_timeout(
&[
"--udp",
"--greppable",
"-u",
"5000",
"-b",
"2500",
"example.com",
],
super::Duration::from_secs(28),
);
}
#[test]
#[ignore]
fn udp_scan_rustscan_cmnatic_co_uk() {
super::run_rustscan_with_timeout(
&[
"--udp",
"--greppable",
"-u",
"5000",
"-b",
"2500",
"rustscan.cmnatic.co.uk",
],
super::Duration::from_secs(26),
);
}
}