Proto/RPC: support RPC over UDP

This commit is contained in:
Pierre Lalet 2021-12-22 18:27:02 +01:00
parent f28e0770f6
commit 166f121d76
3 changed files with 122 additions and 53 deletions

View file

@ -37,13 +37,14 @@ mod ghost;
use ghost::GHOST_PATTERN_SIGNATURE; use ghost::GHOST_PATTERN_SIGNATURE;
mod rpc; mod rpc;
use rpc::RPC_CALL; use rpc::{RPC_CALL_TCP, RPC_CALL_UDP};
const PROTO_HTTP: usize = 1; const PROTO_HTTP: usize = 1;
const PROTO_STUN: usize = 2; const PROTO_STUN: usize = 2;
const PROTO_SSH: usize = 3; const PROTO_SSH: usize = 3;
const PROTO_GHOST: usize = 4; const PROTO_GHOST: usize = 4;
const PROTO_RPC: usize = 5; const PROTO_RPC_TCP: usize = 5;
const PROTO_RPC_UDP: usize = 6;
struct TCPControlBlock { struct TCPControlBlock {
proto_state: usize, proto_state: usize,
@ -90,8 +91,13 @@ fn proto_init() -> Smack {
SmackFlags::ANCHOR_BEGIN, SmackFlags::ANCHOR_BEGIN,
); );
smack.add_pattern( smack.add_pattern(
RPC_CALL, RPC_CALL_TCP,
PROTO_RPC, PROTO_RPC_TCP,
SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS,
);
smack.add_pattern(
RPC_CALL_UDP,
PROTO_RPC_UDP,
SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS, SmackFlags::ANCHOR_BEGIN | SmackFlags::WILDCARDS,
); );
smack.compile(); smack.compile();
@ -141,7 +147,8 @@ pub fn repl<'a>(
PROTO_STUN => stun::repl(data, masscanned, &mut client_info), PROTO_STUN => stun::repl(data, masscanned, &mut client_info),
PROTO_SSH => ssh::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_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); debug!("id: {}", id);
None None

View file

@ -22,8 +22,11 @@ use crate::client::ClientInfo;
use crate::Masscanned; 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) // 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*"; 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)] #[derive(Debug)]
enum RpcState { enum RpcState {
@ -363,19 +366,10 @@ fn build_repl(pstate: ProtocolState, client_info: &ClientInfo) -> Vec<u8> {
}; };
resp.append(&mut specif_resp); resp.append(&mut specif_resp);
} }
let length: u32 = resp.len().try_into().unwrap(); resp
let mut final_resp = Vec::<u8>::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
} }
pub fn repl<'a>( pub fn repl_tcp<'a>(
data: &'a [u8], data: &'a [u8],
_masscanned: &Masscanned, _masscanned: &Masscanned,
client_info: &ClientInfo, client_info: &ClientInfo,
@ -383,6 +377,38 @@ pub fn repl<'a>(
let mut pstate = ProtocolState::new(); let mut pstate = ProtocolState::new();
rpc_parse(&mut pstate, data); rpc_parse(&mut pstate, data);
// warn!("RPC {:#?}", pstate); // 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::<u8>::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<Vec<u8>> {
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 { match pstate.state {
RpcState::End => Some(build_repl(pstate, client_info)), RpcState::End => Some(build_repl(pstate, client_info)),
_ => None, _ => None,
@ -427,7 +453,26 @@ mod tests {
assert!(pstate.verif_flavor == 0); assert!(pstate.verif_flavor == 0);
assert!(pstate.verif_data.len() == 0); assert!(pstate.verif_data.len() == 0);
let resp = build_repl(pstate, &CLIENT_INFO); 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] #[test]
@ -447,7 +492,7 @@ mod tests {
assert!(pstate.verif_flavor == 0); assert!(pstate.verif_flavor == 0);
assert!(pstate.verif_data.len() == 0); assert!(pstate.verif_data.len() == 0);
let resp = build_repl(pstate, &CLIENT_INFO); 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] #[test]
@ -475,7 +520,7 @@ mod tests {
assert!(pstate.verif_flavor == 0); assert!(pstate.verif_flavor == 0);
assert!(pstate.verif_data.len() == 0); assert!(pstate.verif_data.len() == 0);
let resp = build_repl(pstate, &CLIENT_INFO); 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] #[test]

View file

@ -17,6 +17,7 @@
import json import json
import logging import logging
import os import os
import re
from socket import AF_INET6 from socket import AF_INET6
from subprocess import check_call from subprocess import check_call
import struct import struct
@ -1223,40 +1224,45 @@ def test_ipv4_tcp_ghost():
@test @test
def test_rpc_nmap(): def test_rpc_nmap():
with NamedTemporaryFile(delete=False) as xml_result: for scan in "SU":
check_call( with NamedTemporaryFile(delete=False) as xml_result:
[ check_call(
"nmap", [
"-n", "nmap",
"-vv", "-n",
"-oX", "-vv",
"-", "-oX",
IPV4_ADDR, "-",
"-sSV", IPV4_ADDR,
"-p", f"-s{scan}V",
"111", "-p",
"--script", "111",
"rpcinfo,rpc-grind", "--script",
], "rpcinfo,rpc-grind",
stdout=xml_result, ],
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: assert port["service_name"] in {"rpcbind", "nfs"}
DBNmap(output=json_result).store_scan(xml_result.name) assert port["service_extrainfo"] in {"RPC #100000", "RPC #100003"}
os.unlink(xml_result.name) assert (
with open(json_result.name) as fdesc: len(port["scripts"]) == 1
results = [json.loads(line) for line in fdesc] ), f"Expected 1 script, got {len(port['scripts'])}"
os.unlink(json_result.name) script = port["scripts"][0]
assert len(results) == 1, f"Expected 1 result, got {len(results)}" assert script["id"] == "rpcinfo", "Expected rpcinfo script, not found"
result = results[0] assert len(script["rpcinfo"]) == 1
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
@test @test
@ -1276,6 +1282,17 @@ def test_rpcinfo():
for i in range(2, 5): for i in range(2, 5):
assert i in found, f"Missing version {i} in {found}" assert i in found, f"Missing version {i} in {found}"
os.unlink(rpcout.name) 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(): def test_all():