diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 291002b..db058cf 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -37,13 +37,14 @@ mod ghost; use ghost::GHOST_PATTERN_SIGNATURE; mod rpc; -use rpc::RPC_CALL; +use rpc::{RPC_CALL_TCP, RPC_CALL_UDP}; const PROTO_HTTP: usize = 1; const PROTO_STUN: usize = 2; const PROTO_SSH: usize = 3; const PROTO_GHOST: usize = 4; -const PROTO_RPC: usize = 5; +const PROTO_RPC_TCP: usize = 5; +const PROTO_RPC_UDP: usize = 6; struct TCPControlBlock { proto_state: usize, @@ -90,8 +91,13 @@ fn proto_init() -> Smack { SmackFlags::ANCHOR_BEGIN, ); smack.add_pattern( - RPC_CALL, - PROTO_RPC, + RPC_CALL_TCP, + PROTO_RPC_TCP, + SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, + ); + smack.add_pattern( + RPC_CALL_UDP, + PROTO_RPC_UDP, SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, ); smack.compile(); @@ -141,7 +147,8 @@ pub fn repl<'a>( PROTO_STUN => stun::repl(data, masscanned, &mut client_info), PROTO_SSH => ssh::repl(data, masscanned, &mut client_info), PROTO_GHOST => ghost::repl(data, masscanned, &mut client_info), - PROTO_RPC => rpc::repl(data, masscanned, &mut client_info), + PROTO_RPC_TCP => rpc::repl_tcp(data, masscanned, &mut client_info), + PROTO_RPC_UDP => rpc::repl_udp(data, masscanned, &mut client_info), _ => { debug!("id: {}", id); None diff --git a/src/proto/rpc.rs b/src/proto/rpc.rs index 0619ed7..7a30b19 100644 --- a/src/proto/rpc.rs +++ b/src/proto/rpc.rs @@ -22,8 +22,11 @@ use crate::client::ClientInfo; use crate::Masscanned; // last fragment (1 bit) + fragment len (31 bits) / length XID (random) / message type: call (0) / RPC version (0-255) / Program: Portmap (99840 - 100095) / Program version (*, random versions used, see below) / / Procedure: ??? (0-255) -pub const RPC_CALL: &[u8; 28] = +pub const RPC_CALL_TCP: &[u8; 28] = b"********\x00\x00\x00\x00\x00\x00\x00*\x00\x01\x86*****\x00\x00\x00*"; +// UDP: last fragment and fragment len are missing +pub const RPC_CALL_UDP: &[u8; 24] = + b"****\x00\x00\x00\x00\x00\x00\x00*\x00\x01\x86*****\x00\x00\x00*"; #[derive(Debug)] enum RpcState { @@ -363,19 +366,10 @@ fn build_repl(pstate: ProtocolState, client_info: &ClientInfo) -> Vec { }; resp.append(&mut specif_resp); } - let length: u32 = resp.len().try_into().unwrap(); - let mut final_resp = Vec::::new(); - for i in 0..4 { - match i { - 0 => final_resp.push(get_nth_byte(length, i) | 0x80), - _ => final_resp.push(get_nth_byte(length, i)), - }; - } - final_resp.append(&mut resp); - final_resp + resp } -pub fn repl<'a>( +pub fn repl_tcp<'a>( data: &'a [u8], _masscanned: &Masscanned, client_info: &ClientInfo, @@ -383,6 +377,38 @@ pub fn repl<'a>( let mut pstate = ProtocolState::new(); rpc_parse(&mut pstate, data); // warn!("RPC {:#?}", pstate); + let resp = match pstate.state { + RpcState::End => Some(build_repl(pstate, client_info)), + _ => None, + }; + match resp { + Some(mut resp) => { + let length: u32 = resp.len().try_into().unwrap(); + let mut final_resp = Vec::::new(); + for i in 0..4 { + match i { + 0 => final_resp.push(get_nth_byte(length, i) | 0x80), + _ => final_resp.push(get_nth_byte(length, i)), + }; + } + final_resp.append(&mut resp); + Some(final_resp) + } + _ => None, + } +} + +pub fn repl_udp<'a>( + data: &'a [u8], + _masscanned: &Masscanned, + client_info: &ClientInfo, +) -> Option> { + let mut pstate = ProtocolState::new(); + pstate.state = RpcState::Xid; + pstate.last_frag = true; + pstate.frag_len = data.len().try_into().unwrap(); + rpc_parse(&mut pstate, data); + // warn!("RPC {:#?}", pstate); match pstate.state { RpcState::End => Some(build_repl(pstate, client_info)), _ => None, @@ -427,7 +453,26 @@ mod tests { assert!(pstate.verif_flavor == 0); assert!(pstate.verif_data.len() == 0); let resp = build_repl(pstate, &CLIENT_INFO); - assert!(resp == b"\x80\x00\x00\x20\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + } + + #[test] + fn test_probe_nmap_udp() { + let mut pstate = ProtocolState::new(); + pstate.state = RpcState::Xid; + rpc_parse(&mut pstate, b"\x72\xfe\x1d\x13\x00\x00\x00\x00\x00\x00\x00\x02\x00\x01\x86\xa0\x00\x01\x97\x7c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"); + assert!(matches!(pstate.state, RpcState::End)); + assert!(pstate.xid == 0x72fe1d13); + assert!(pstate.rpc_version == 2); + assert!(pstate.program == 100000); + assert!(pstate.prog_version == 104316); + assert!(pstate.procedure == 0); + assert!(pstate.creds_flavor == 0); + assert!(pstate.creds_data.len() == 0); + assert!(pstate.verif_flavor == 0); + assert!(pstate.verif_data.len() == 0); + let resp = build_repl(pstate, &CLIENT_INFO); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); } #[test] @@ -447,7 +492,7 @@ mod tests { assert!(pstate.verif_flavor == 0); assert!(pstate.verif_data.len() == 0); let resp = build_repl(pstate, &CLIENT_INFO); - assert!(resp == b"\x80\x00\x00\x20\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); } #[test] @@ -475,7 +520,7 @@ mod tests { assert!(pstate.verif_flavor == 0); assert!(pstate.verif_data.len() == 0); let resp = build_repl(pstate, &CLIENT_INFO); - assert!(resp == b"\x80\x00\x00\x20\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); + assert!(resp == b"\x72\xfe\x1d\x13\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x04"); } #[test] diff --git a/test/src/all.py b/test/src/all.py index 214a249..c2cd5a9 100644 --- a/test/src/all.py +++ b/test/src/all.py @@ -17,6 +17,7 @@ import json import logging import os +import re from socket import AF_INET6 from subprocess import check_call import struct @@ -1223,40 +1224,45 @@ def test_ipv4_tcp_ghost(): @test def test_rpc_nmap(): - with NamedTemporaryFile(delete=False) as xml_result: - check_call( - [ - "nmap", - "-n", - "-vv", - "-oX", - "-", - IPV4_ADDR, - "-sSV", - "-p", - "111", - "--script", - "rpcinfo,rpc-grind", - ], - stdout=xml_result, + for scan in "SU": + with NamedTemporaryFile(delete=False) as xml_result: + check_call( + [ + "nmap", + "-n", + "-vv", + "-oX", + "-", + IPV4_ADDR, + f"-s{scan}V", + "-p", + "111", + "--script", + "rpcinfo,rpc-grind", + ], + stdout=xml_result, + ) + with NamedTemporaryFile(delete=False, mode="w") as json_result: + DBNmap(output=json_result).store_scan(xml_result.name) + os.unlink(xml_result.name) + with open(json_result.name) as fdesc: + results = [json.loads(line) for line in fdesc] + os.unlink(json_result.name) + assert len(results) == 1, f"Expected 1 result, got {len(results)}" + result = results[0] + assert len(result["ports"]) == 1, f"Expected 1 port, got {len(result['ports'])}" + port = result["ports"][0] + assert port["port"] == 111 and port["protocol"] == ( + "tcp" if scan == "S" else "udp" ) - with NamedTemporaryFile(delete=False, mode="w") as json_result: - DBNmap(output=json_result).store_scan(xml_result.name) - os.unlink(xml_result.name) - with open(json_result.name) as fdesc: - results = [json.loads(line) for line in fdesc] - os.unlink(json_result.name) - assert len(results) == 1, f"Expected 1 result, got {len(results)}" - result = results[0] - assert len(result["ports"]) == 1, f"Expected 1 port, got {len(result['ports'])}" - port = result["ports"][0] - assert port["port"] == 111 and port["protocol"] == "tcp" - assert port["service_name"] in {"rpcbind", "nfs"} - assert port["service_extrainfo"] in {"RPC #100000", "RPC #100003"} - assert len(port["scripts"]) == 1, f"Expected 1 script, got {len(port['scripts'])}" - script = port["scripts"][0] - assert script["id"] == "rpcinfo", "Expected rpcinfo script, not found" - assert len(script["rpcinfo"]) == 1 + assert port["service_name"] in {"rpcbind", "nfs"} + assert port["service_extrainfo"] in {"RPC #100000", "RPC #100003"} + assert ( + len(port["scripts"]) == 1 + ), f"Expected 1 script, got {len(port['scripts'])}" + script = port["scripts"][0] + assert script["id"] == "rpcinfo", "Expected rpcinfo script, not found" + assert len(script["rpcinfo"]) == 1 @test @@ -1276,6 +1282,17 @@ def test_rpcinfo(): for i in range(2, 5): assert i in found, f"Missing version {i} in {found}" os.unlink(rpcout.name) + with NamedTemporaryFile(delete=False) as rpcout: + check_call(["rpcinfo", "-u", IPV4_ADDR, "100000"], stdout=rpcout) + with open(rpcout.name) as fdesc: + found = [] + expr = re.compile("^program 100000 version ([0-9]) ready and waiting$") + for line in fdesc: + found.append(int(expr.search(line.strip()).group(1))) + assert len(found) == 3, f"Expected three versions, got {found}" + for i in range(2, 5): + assert i in found, f"Missing version {i} in {found}" + os.unlink(rpcout.name) def test_all():