From 87c95ff2404779cfc0d72b4bb29d86aee1dcbaf1 Mon Sep 17 00:00:00 2001 From: _Frky <3105926+Frky@users.noreply.github.com> Date: Thu, 16 Dec 2021 22:13:31 +0100 Subject: [PATCH] Implement FSM for parsing SSH banner --- src/proto/mod.rs | 20 +-- src/proto/ssh.rs | 361 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 369 insertions(+), 12 deletions(-) diff --git a/src/proto/mod.rs b/src/proto/mod.rs index 9be5c02..836965a 100644 --- a/src/proto/mod.rs +++ b/src/proto/mod.rs @@ -256,16 +256,16 @@ mod tests { }; /***** TEST SSH *****/ let payloads = [ - "SSH-2.0-PUTTY", - "SSH-2.0-Go", - "SSH-2.0-libssh2_1.4.3", - "SSH-2.0-PuTTY", - "SSH-2.0-AsyncSSH_2.1.0", - "SSH-2.0-libssh2_1.9.0", - "SSH-2.0-libssh2_1.7.0", - "SSH-2.0-8.35 FlowSsh: FlowSshNet_SftpStress54.38.116.473", - "SSH-2.0-libssh_0.9.5", - "SSH-2.0-OpenSSH_6.7p1 Raspbian-5+deb8u3", + "SSH-2.0-PUTTY\r\n", + "SSH-2.0-Go\r\n", + "SSH-2.0-libssh2_1.4.3\r\n", + "SSH-2.0-PuTTY\r\n", + "SSH-2.0-AsyncSSH_2.1.0\r\n", + "SSH-2.0-libssh2_1.9.0\r\n", + "SSH-2.0-libssh2_1.7.0\r\n", + "SSH-2.0-8.35 FlowSsh: FlowSshNet_SftpStress54.38.116.473\r\n", + "SSH-2.0-libssh_0.9.5\r\n", + "SSH-2.0-OpenSSH_6.7p1 Raspbian-5+deb8u3\r\n", ]; for payload in payloads.iter() { let _ssh_resp = diff --git a/src/proto/ssh.rs b/src/proto/ssh.rs index 77478e4..09272f1 100644 --- a/src/proto/ssh.rs +++ b/src/proto/ssh.rs @@ -23,6 +23,142 @@ use crate::Masscanned; pub const SSH_PATTERN_CLIENT_PROTOCOL: &[u8; 7] = b"SSH-2.0"; +const SSH_STATE_START: usize = 0; +const SSH_STATE_S1: usize = 1; +const SSH_STATE_S2: usize = 2; +const SSH_STATE_H: usize = 3; +const SSH_STATE_DASH: usize = 4; +const SSH_STATE_VERSION: usize = 5; +const SSH_STATE_SOFTWARE: usize = 6; +const SSH_STATE_COMMENT: usize = 7; +const SSH_STATE_EOB: usize = 8; +const SSH_STATE_LF: usize = 9; + +const SSH_STATE_FAIL: usize = 0xFFFF; + +struct ProtocolState { + state: usize, + prev_state: usize, + ssh_version: Vec, + ssh_software: Vec, + ssh_comment: Vec, +} + +impl ProtocolState { + fn new() -> Self { + ProtocolState { + state: SSH_STATE_START, + prev_state: SSH_STATE_START, + ssh_version: Vec::::new(), + ssh_software: Vec::::new(), + ssh_comment: Vec::::new(), + } + } +} + +fn ssh_parse(pstate: &mut ProtocolState, data: &[u8]) { + /* RFC 4253: + * + * 4.2. Protocol Version Exchange + * + * When the connection has been established, both sides MUST send an + * identification string. This identification string MUST be + * + * SSH-protoversion-softwareversion SP comments CR LF + * + * Since the protocol being defined in this set of documents is version + * 2.0, the 'protoversion' MUST be "2.0". The 'comments' string is + * OPTIONAL. If the 'comments' string is included, a 'space' character + * (denoted above as SP, ASCII 32) MUST separate the 'softwareversion' + * and 'comments' strings. The identification MUST be terminated by a + * single Carriage Return (CR) and a single Line Feed (LF) character + * (ASCII 13 and 10, respectively). Implementers who wish to maintain + * compatibility with older, undocumented versions of this protocol may + * want to process the identification string without expecting the + * presence of the carriage return character for reasons described in + * Section 5 of this document. The null character MUST NOT be sent. + * The maximum length of the string is 255 characters, including the + * Carriage Return and Line Feed. + */ + let mut i = 0; + while i < data.len() { + match pstate.state { + SSH_STATE_START => { + pstate.state = SSH_STATE_S1; + continue; + } + /* first bytes should be "SSH-" */ + SSH_STATE_S1 | SSH_STATE_S2 | SSH_STATE_H | SSH_STATE_DASH => { + if data[i] != b"SSH-"[pstate.state - SSH_STATE_S1] { + pstate.state = SSH_STATE_FAIL; + } else { + pstate.state += 1; + } + } + /* expect LF after a CR was read */ + SSH_STATE_LF => { + if data[i] == b'\n' { + pstate.state = SSH_STATE_EOB; + } else { + if pstate.prev_state == SSH_STATE_SOFTWARE { + /* when reading software, \r can be followed by something else than \n */ + pstate.state = pstate.prev_state; + /* cancel the read of this char */ + i -= 1; + /* add the previously read \r to the software string */ + pstate.ssh_software.push(b'\r'); + } else if pstate.prev_state == SSH_STATE_COMMENT { + /* when reading comment, \r can be followed by something else than \n */ + pstate.state = pstate.prev_state; + /* cancel the read of this char */ + i -= 1; + /* add the previously read \r to the software string */ + pstate.ssh_comment.push(b'\r'); + } else { + /* in some other cases, it fails */ + pstate.state = SSH_STATE_FAIL; + } + } + } + SSH_STATE_VERSION => { + if data[i] == b'-' { + pstate.state = SSH_STATE_SOFTWARE; + } else if !data[i].is_ascii_digit() && data[i] != b'.' { + pstate.state = SSH_STATE_FAIL; + } else { + pstate.ssh_version.push(data[i]); + } + } + SSH_STATE_SOFTWARE => { + if data[i] == b'\r' { + /* look for LF in the next char */ + pstate.prev_state = pstate.state; + pstate.state = SSH_STATE_LF; + } else if data[i] == b' ' { + pstate.state = SSH_STATE_COMMENT; + } else { + pstate.ssh_software.push(data[i]); + } + } + SSH_STATE_COMMENT => { + if data[i] == b'\r' { + /* look for LF in the next char */ + pstate.prev_state = pstate.state; + pstate.state = SSH_STATE_LF; + } else { + pstate.ssh_comment.push(data[i]); + } + } + SSH_STATE_FAIL => { + return; + } + SSH_STATE_EOB => { /* so far, do not parse after banner */ } + _ => {} + }; + i += 1; + } +} + pub fn repl<'a>( data: &'a [u8], _masscanned: &Masscanned, @@ -30,10 +166,231 @@ pub fn repl<'a>( _tcb: Option<&mut TCPControlBlock>, ) -> Option> { debug!("receiving SSH data"); + let mut pstate = ProtocolState::new(); + ssh_parse(&mut pstate, data); + if pstate.state != SSH_STATE_EOB { + debug!("data in not correctly formatted - not responding"); + debug!("pstate: {}", pstate.state); + return None; + } let repl_data = b"SSH-2.0-1\r\n".to_vec(); debug!("sending SSH answer"); - warn!("SSH server banner to {}", byte2str(data)); - return Some(repl_data); + warn!("SSH server banner to {}", str::from_utf8(&pstate.ssh_software).unwrap().trim_end()); + Some(repl_data) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ssh_banner_parse() { + /* all at once */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + /* byte by byte */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + for i in 0..test_banner.len() { + if i == 0 { + assert!(pstate.state == SSH_STATE_START); + } else if i > 0 && i < 4 { + assert!(pstate.state == SSH_STATE_S1 + i); + } else if i >= 4 && i < 8 { + assert!(pstate.state == SSH_STATE_VERSION); + } else if i >= 8 && i < 17 { + assert!(pstate.state == SSH_STATE_SOFTWARE); + } else if i >= 17 && i < test_banner.len() - 1 { + assert!(pstate.state == SSH_STATE_COMMENT); + } else { + assert!(pstate.state == SSH_STATE_LF); + } + ssh_parse(&mut pstate, &test_banner[i..i + 1]); + } + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + } + + #[test] + fn test_ssh_banner_space() { + /* space in SSH */ + let test_banner = b"S SH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* space in VERSION */ + let test_banner = b"SSH-2. 0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* space in software */ + let test_banner = b"SSH-2.0-SOFT WARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT"); + assert!(pstate.ssh_comment == b"WARE COMMENT"); + /* space in comment */ + let test_banner = b"SSH-2.0-SOFTWARE COM MENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM MENT"); + /* double space */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b" COMMENT"); + } + + #[test] + fn test_ssh_banner_cr() { + /* CR in SSH */ + let test_banner = b"S\rSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CR in VERSION */ + let test_banner = b"SSH-2.\r0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CR in SOFTWARE */ + let test_banner = b"SSH-2.0-SOFT\rWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT\rWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + /* CR in COMMENT */ + let test_banner = b"SSH-2.0-SOFTWARE COM\rMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM\rMENT"); + /* CR at the end */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT\r"); + } + + #[test] + fn test_ssh_banner_lf() { + /* LF in SSH */ + let test_banner = b"S\nSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* LF in VERSION */ + let test_banner = b"SSH-2.\n0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* LF in SOFTWARE */ + let test_banner = b"SSH-2.0-SOFT\nWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT\nWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + /* LF in COMMENT */ + let test_banner = b"SSH-2.0-SOFTWARE COM\nMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM\nMENT"); + /* LF at the end */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\n\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT\n"); + } + + #[test] + fn test_ssh_banner_crlf() { + /* CRLF in SSH */ + let test_banner = b"S\r\nSH-2.0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CRLF in VERSION */ + let test_banner = b"SSH-2.\r\n0-SOFTWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_FAIL); + /* CRLF in SOFTWARE */ + let test_banner = b"SSH-2.0-SOFT\r\nWARE COMMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFT"); + assert!(pstate.ssh_comment == b""); + /* CRLF in COMMENT */ + let test_banner = b"SSH-2.0-SOFTWARE COM\r\nMENT\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COM"); + /* CRLF at the end */ + let test_banner = b"SSH-2.0-SOFTWARE COMMENT\r\n\r\n"; + let mut pstate = ProtocolState::new(); + assert!(pstate.state == SSH_STATE_START); + ssh_parse(&mut pstate, test_banner); + assert!(pstate.state == SSH_STATE_EOB); + assert!(pstate.ssh_version == b"2.0"); + assert!(pstate.ssh_software == b"SOFTWARE"); + assert!(pstate.ssh_comment == b"COMMENT"); + } +>>>>>>> 29f9e75 (Implement FSM for parsing SSH banner) } #[cfg(test)]