app-layer: websockets protocol support

Ticket: 2695
pull/10873/head
Philippe Antoine 2 years ago committed by Victor Julien
parent 78b766048e
commit 44b6aa5e4b

@ -36,6 +36,7 @@ Suricata Rules
quic-keywords quic-keywords
nfs-keywords nfs-keywords
smtp-keywords smtp-keywords
websocket-keywords
app-layer app-layer
xbits xbits
thresholding thresholding

@ -110,6 +110,7 @@ you can pick from. These are:
* snmp * snmp
* tftp * tftp
* sip * sip
* websocket
The availability of these protocols depends on whether the protocol The availability of these protocols depends on whether the protocol
is enabled in the configuration file, suricata.yaml. is enabled in the configuration file, suricata.yaml.

@ -0,0 +1,63 @@
WebSocket Keywords
==================
websocket.payload
-----------------
A sticky buffer on the unmasked payload,
limited by suricata.yaml config value ``websocket.max-payload-size``.
Examples::
websocket.payload; pcre:"/^123[0-9]*/";
websocket.payload content:"swordfish";
``websocket.payload`` is a 'sticky buffer' and can be used as ``fast_pattern``.
websocket.flags
---------------
Matches on the websocket flags.
It uses a 8-bit unsigned integer as value.
Only the four upper bits are used.
The value can also be a list of strings (comma-separated),
where each string is the name of a specific bit like `fin` and `comp`,
and can be prefixed by `!` for negation.
websocket.flags uses an :ref:`unsigned 8-bits integer <rules-integer-keywords>`
Examples::
websocket.flags:128;
websocket.flags:&0x40=0x40;
websocket.flags:fin,!comp;
websocket.mask
--------------
Matches on the websocket mask if any.
It uses a 32-bit unsigned integer as value (big-endian).
websocket.mask uses an :ref:`unsigned 32-bits integer <rules-integer-keywords>`
Examples::
websocket.mask:123456;
websocket.mask:>0;
websocket.opcode
----------------
Matches on the websocket opcode.
It uses a 8-bit unsigned integer as value.
Only 16 values are relevant.
It can also be specified by text from the enumeration
websocket.opcode uses an :ref:`unsigned 8-bits integer <rules-integer-keywords>`
Examples::
websocket.opcode:1;
websocket.opcode:>8;
websocket.opcode:ping;

@ -3898,6 +3898,9 @@
"tls": { "tls": {
"description": "Errors encountered parsing TLS protocol", "description": "Errors encountered parsing TLS protocol",
"$ref": "#/$defs/stats_applayer_error" "$ref": "#/$defs/stats_applayer_error"
},
"websocket": {
"$ref": "#/$defs/stats_applayer_error"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -4056,6 +4059,9 @@
"tls": { "tls": {
"description": "Number of flows for TLS protocol", "description": "Number of flows for TLS protocol",
"type": "integer" "type": "integer"
},
"websocket": {
"type": "integer"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -4170,6 +4176,9 @@
}, },
"tls": { "tls": {
"type": "integer" "type": "integer"
},
"websocket": {
"type": "integer"
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -5653,6 +5662,21 @@
} }
}, },
"additionalProperties": false "additionalProperties": false
},
"websocket": {
"type": "object",
"properties": {
"fin": {
"type": "boolean"
},
"mask": {
"type": "integer"
},
"opcode": {
"type": "string"
}
},
"additionalProperties": false
} }
}, },
"$defs": { "$defs": {

@ -22,4 +22,5 @@ smb-events.rules \
smtp-events.rules \ smtp-events.rules \
ssh-events.rules \ ssh-events.rules \
stream-events.rules \ stream-events.rules \
tls-events.rules tls-events.rules \
websocket-events.rules

@ -0,0 +1,8 @@
# WebSocket app-layer event rules.
#
# These SIDs fall in the 2235000+ range. See:
# http://doc.emergingthreats.net/bin/view/Main/SidAllocation and
# https://redmine.openinfosecfoundation.org/projects/suricata/wiki/AppLayer
alert websocket any any -> any any (msg:"SURICATA Websocket skipped end of payload"; app-layer-event:websocket.skip_end_of_payload; classtype:protocol-command-decode; sid:2235000; rev:1;)
alert websocket any any -> any any (msg:"SURICATA Websocket reassembly limit reached"; app-layer-event:websocket.reassembly_limit_reached; classtype:protocol-command-decode; sid:2235001; rev:1;)

@ -108,6 +108,7 @@ pub mod rfb;
pub mod mqtt; pub mod mqtt;
pub mod pgsql; pub mod pgsql;
pub mod telnet; pub mod telnet;
pub mod websocket;
pub mod applayertemplate; pub mod applayertemplate;
pub mod rdp; pub mod rdp;
pub mod x509; pub mod x509;

@ -0,0 +1,135 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use super::websocket::WebSocketTransaction;
use crate::detect::uint::{
detect_parse_uint, detect_parse_uint_enum, DetectUintData, DetectUintMode,
};
use crate::websocket::parser::WebSocketOpcode;
use nom7::branch::alt;
use nom7::bytes::complete::{is_a, tag};
use nom7::combinator::{opt, value};
use nom7::multi::many1;
use nom7::IResult;
use std::ffi::CStr;
#[no_mangle]
pub unsafe extern "C" fn SCWebSocketGetOpcode(tx: &mut WebSocketTransaction) -> u8 {
return tx.pdu.opcode;
}
#[no_mangle]
pub unsafe extern "C" fn SCWebSocketGetFlags(tx: &mut WebSocketTransaction) -> u8 {
return tx.pdu.flags;
}
#[no_mangle]
pub unsafe extern "C" fn SCWebSocketGetPayload(
tx: &WebSocketTransaction, buffer: *mut *const u8, buffer_len: *mut u32,
) -> bool {
*buffer = tx.pdu.payload.as_ptr();
*buffer_len = tx.pdu.payload.len() as u32;
return true;
}
#[no_mangle]
pub unsafe extern "C" fn SCWebSocketGetMask(
tx: &mut WebSocketTransaction, value: *mut u32,
) -> bool {
if let Some(xorkey) = tx.pdu.mask {
*value = xorkey;
return true;
}
return false;
}
#[no_mangle]
pub unsafe extern "C" fn SCWebSocketParseOpcode(
ustr: *const std::os::raw::c_char,
) -> *mut DetectUintData<u8> {
let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe
if let Ok(s) = ft_name.to_str() {
if let Some(ctx) = detect_parse_uint_enum::<u8, WebSocketOpcode>(s) {
let boxed = Box::new(ctx);
return Box::into_raw(boxed) as *mut _;
}
}
return std::ptr::null_mut();
}
struct WebSocketFlag {
neg: bool,
value: u8,
}
fn parse_flag_list_item(s: &str) -> IResult<&str, WebSocketFlag> {
let (s, _) = opt(is_a(" "))(s)?;
let (s, neg) = opt(tag("!"))(s)?;
let neg = neg.is_some();
let (s, value) = alt((value(0x80, tag("fin")), value(0x40, tag("comp"))))(s)?;
let (s, _) = opt(is_a(" ,"))(s)?;
Ok((s, WebSocketFlag { neg, value }))
}
fn parse_flag_list(s: &str) -> IResult<&str, Vec<WebSocketFlag>> {
return many1(parse_flag_list_item)(s);
}
fn parse_flags(s: &str) -> Option<DetectUintData<u8>> {
// try first numerical value
if let Ok((_, ctx)) = detect_parse_uint::<u8>(s) {
return Some(ctx);
}
// otherwise, try strings for bitmask
if let Ok((_, l)) = parse_flag_list(s) {
let mut arg1 = 0;
let mut arg2 = 0;
for elem in l.iter() {
if elem.value & arg1 != 0 {
SCLogWarning!("Repeated bitflag for websocket.flags");
return None;
}
arg1 |= elem.value;
if !elem.neg {
arg2 |= elem.value;
}
}
let ctx = DetectUintData::<u8> {
arg1,
arg2,
mode: DetectUintMode::DetectUintModeBitmask,
};
return Some(ctx);
}
return None;
}
#[no_mangle]
pub unsafe extern "C" fn SCWebSocketParseFlags(
ustr: *const std::os::raw::c_char,
) -> *mut DetectUintData<u8> {
let ft_name: &CStr = CStr::from_ptr(ustr); //unsafe
if let Ok(s) = ft_name.to_str() {
if let Some(ctx) = parse_flags(s) {
let boxed = Box::new(ctx);
return Box::into_raw(boxed) as *mut _;
}
}
return std::ptr::null_mut();
}

@ -0,0 +1,45 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use super::parser::WebSocketOpcode;
use super::websocket::WebSocketTransaction;
use crate::detect::EnumString;
use crate::jsonbuilder::{JsonBuilder, JsonError};
use std;
fn log_websocket(tx: &WebSocketTransaction, js: &mut JsonBuilder) -> Result<(), JsonError> {
js.open_object("websocket")?;
js.set_bool("fin", tx.pdu.fin)?;
if let Some(xorkey) = tx.pdu.mask {
js.set_uint("mask", xorkey.into())?;
}
if let Some(opcode) = WebSocketOpcode::from_u(tx.pdu.opcode) {
js.set_string("opcode", opcode.to_str())?;
} else {
js.set_string("opcode", &format!("unknown-{}", tx.pdu.opcode))?;
}
js.close()?;
Ok(())
}
#[no_mangle]
pub unsafe extern "C" fn rs_websocket_logger_log(
tx: *mut std::os::raw::c_void, js: &mut JsonBuilder,
) -> bool {
let tx = cast_pointer!(tx, WebSocketTransaction);
log_websocket(tx, js).is_ok()
}

@ -0,0 +1,23 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
//! Application layer websocket parser and logger module.
pub mod detect;
pub mod logger;
mod parser;
pub mod websocket;

@ -0,0 +1,96 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use nom7::bytes::streaming::take;
use nom7::combinator::cond;
use nom7::number::streaming::{be_u16, be_u32, be_u64, be_u8};
use nom7::IResult;
use suricata_derive::EnumStringU8;
#[derive(Clone, Debug, Default, EnumStringU8)]
#[repr(u8)]
pub enum WebSocketOpcode {
#[default]
Continuation = 0,
Text = 1,
Binary = 2,
Ping = 8,
Pong = 9,
}
#[derive(Clone, Debug, Default)]
pub struct WebSocketPdu {
pub flags: u8,
pub fin: bool,
pub compress: bool,
pub opcode: u8,
pub mask: Option<u32>,
pub payload: Vec<u8>,
pub to_skip: u64,
}
// cf rfc6455#section-5.2
pub fn parse_message(i: &[u8], max_pl_size: u32) -> IResult<&[u8], WebSocketPdu> {
let (i, flags_op) = be_u8(i)?;
let fin = (flags_op & 0x80) != 0;
let compress = (flags_op & 0x40) != 0;
let flags = flags_op & 0xF0;
let opcode = flags_op & 0xF;
let (i, mask_plen) = be_u8(i)?;
let mask_flag = (mask_plen & 0x80) != 0;
let (i, payload_len) = match mask_plen & 0x7F {
126 => {
let (i, val) = be_u16(i)?;
Ok((i, val.into()))
}
127 => be_u64(i),
_ => Ok((i, (mask_plen & 0x7F).into())),
}?;
let (i, xormask) = cond(mask_flag, take(4usize))(i)?;
let mask = if mask_flag {
let (_, m) = be_u32(xormask.unwrap())?;
Some(m)
} else {
None
};
// we limit payload_len to u32, so as to build on 32-bit system
// where we cannot take(usize) with a u64
let (to_skip, payload_len) = if payload_len < max_pl_size.into() {
(0, payload_len as u32)
} else {
(payload_len - (max_pl_size as u64), max_pl_size)
};
let (i, payload_raw) = take(payload_len)(i)?;
let mut payload = payload_raw.to_vec();
if let Some(xorkey) = xormask {
for i in 0..payload.len() {
payload[i] ^= xorkey[i % 4];
}
}
Ok((
i,
WebSocketPdu {
flags,
fin,
compress,
opcode,
mask,
payload,
to_skip,
},
))
}

@ -0,0 +1,383 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
use super::parser;
use crate::applayer::{self, *};
use crate::conf::conf_get;
use crate::core::{AppProto, Direction, Flow, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP};
use crate::frames::Frame;
use nom7 as nom;
use nom7::Needed;
use flate2::read::DeflateDecoder;
use std;
use std::collections::VecDeque;
use std::ffi::CString;
use std::io::Read;
use std::os::raw::{c_char, c_int, c_void};
static mut ALPROTO_WEBSOCKET: AppProto = ALPROTO_UNKNOWN;
static mut WEBSOCKET_MAX_PAYLOAD_SIZE: u32 = 0xFFFF;
// app-layer-frame-documentation tag start: FrameType enum
#[derive(AppLayerFrameType)]
pub enum WebSocketFrameType {
Header,
Pdu,
}
#[derive(AppLayerEvent)]
pub enum WebSocketEvent {
SkipEndOfPayload,
ReassemblyLimitReached,
}
#[derive(Default)]
pub struct WebSocketTransaction {
tx_id: u64,
pub pdu: parser::WebSocketPdu,
tx_data: AppLayerTxData,
}
impl WebSocketTransaction {
pub fn new(direction: Direction) -> WebSocketTransaction {
Self {
tx_data: AppLayerTxData::for_direction(direction),
..Default::default()
}
}
}
impl Transaction for WebSocketTransaction {
fn id(&self) -> u64 {
self.tx_id
}
}
#[derive(Default)]
struct WebSocketReassemblyBuffer {
data: Vec<u8>,
compress: bool,
}
#[derive(Default)]
pub struct WebSocketState {
state_data: AppLayerStateData,
tx_id: u64,
transactions: VecDeque<WebSocketTransaction>,
c2s_buf: WebSocketReassemblyBuffer,
s2c_buf: WebSocketReassemblyBuffer,
to_skip_tc: u64,
to_skip_ts: u64,
}
impl State<WebSocketTransaction> for WebSocketState {
fn get_transaction_count(&self) -> usize {
self.transactions.len()
}
fn get_transaction_by_index(&self, index: usize) -> Option<&WebSocketTransaction> {
self.transactions.get(index)
}
}
impl WebSocketState {
pub fn new() -> Self {
Default::default()
}
// Free a transaction by ID.
fn free_tx(&mut self, tx_id: u64) {
let len = self.transactions.len();
let mut found = false;
let mut index = 0;
for i in 0..len {
let tx = &self.transactions[i];
if tx.tx_id == tx_id + 1 {
found = true;
index = i;
break;
}
}
if found {
self.transactions.remove(index);
}
}
pub fn get_tx(&mut self, tx_id: u64) -> Option<&WebSocketTransaction> {
self.transactions.iter().find(|tx| tx.tx_id == tx_id + 1)
}
fn new_tx(&mut self, direction: Direction) -> WebSocketTransaction {
let mut tx = WebSocketTransaction::new(direction);
self.tx_id += 1;
tx.tx_id = self.tx_id;
return tx;
}
fn parse(
&mut self, stream_slice: StreamSlice, direction: Direction, flow: *const Flow,
) -> AppLayerResult {
let to_skip = if direction == Direction::ToClient {
&mut self.to_skip_tc
} else {
&mut self.to_skip_ts
};
let input = stream_slice.as_slice();
let mut start = input;
if *to_skip > 0 {
if *to_skip >= input.len() as u64 {
*to_skip -= input.len() as u64;
return AppLayerResult::ok();
} else {
start = &input[*to_skip as usize..];
*to_skip = 0;
}
}
let max_pl_size = unsafe { WEBSOCKET_MAX_PAYLOAD_SIZE };
while !start.is_empty() {
match parser::parse_message(start, max_pl_size) {
Ok((rem, pdu)) => {
let _pdu = Frame::new(
flow,
&stream_slice,
start,
(start.len() - rem.len() - pdu.payload.len()) as i64,
WebSocketFrameType::Header as u8,
);
let _pdu = Frame::new(
flow,
&stream_slice,
start,
(start.len() - rem.len()) as i64,
WebSocketFrameType::Pdu as u8,
);
start = rem;
let mut tx = self.new_tx(direction);
if pdu.to_skip > 0 {
if direction == Direction::ToClient {
self.to_skip_tc = pdu.to_skip;
} else {
self.to_skip_ts = pdu.to_skip;
}
tx.tx_data.set_event(WebSocketEvent::SkipEndOfPayload as u8);
}
let buf = if direction == Direction::ToClient {
&mut self.s2c_buf
} else {
&mut self.c2s_buf
};
if !buf.data.is_empty() || !pdu.fin {
if buf.data.is_empty() {
buf.compress = pdu.compress;
}
if buf.data.len() + pdu.payload.len() < max_pl_size as usize {
buf.data.extend(&pdu.payload);
} else if buf.data.len() < max_pl_size as usize {
buf.data
.extend(&pdu.payload[..max_pl_size as usize - buf.data.len()]);
tx.tx_data
.set_event(WebSocketEvent::ReassemblyLimitReached as u8);
}
}
tx.pdu = pdu;
if tx.pdu.fin && !buf.data.is_empty() {
// the final PDU gets the full reassembled payload
std::mem::swap(&mut tx.pdu.payload, &mut buf.data);
buf.data.clear();
}
if buf.compress && tx.pdu.fin {
buf.compress = false;
// cf RFC 7692 section-7.2.2
tx.pdu.payload.extend_from_slice(&[0, 0, 0xFF, 0xFF]);
let mut deflater = DeflateDecoder::new(&tx.pdu.payload[..]);
let mut v = Vec::new();
// do not check result because
// deflate with rust backend fails on good input cf https://github.com/rust-lang/flate2-rs/issues/389
let _ = deflater.read_to_end(&mut v);
if !v.is_empty() {
std::mem::swap(&mut tx.pdu.payload, &mut v);
}
}
self.transactions.push_back(tx);
}
Err(nom::Err::Incomplete(needed)) => {
if let Needed::Size(n) = needed {
let n = usize::from(n);
// Not enough data. just ask for one more byte.
let consumed = input.len() - start.len();
let needed = start.len() + n;
return AppLayerResult::incomplete(consumed as u32, needed as u32);
}
return AppLayerResult::err();
}
Err(_) => {
return AppLayerResult::err();
}
}
}
// Input was fully consumed.
return AppLayerResult::ok();
}
}
// C exports.
#[no_mangle]
pub unsafe extern "C" fn rs_websocket_probing_parser(
_flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8,
) -> AppProto {
if !input.is_null() {
let slice = build_slice!(input, input_len as usize);
if !slice.is_empty() {
// just check reserved bits are zeroed, except RSV1
// as RSV1 is used for compression cf RFC 7692
if slice[0] & 0x30 == 0 {
return ALPROTO_WEBSOCKET;
}
return ALPROTO_FAILED;
}
}
return ALPROTO_UNKNOWN;
}
extern "C" fn rs_websocket_state_new(
_orig_state: *mut c_void, _orig_proto: AppProto,
) -> *mut c_void {
let state = WebSocketState::new();
let boxed = Box::new(state);
return Box::into_raw(boxed) as *mut c_void;
}
unsafe extern "C" fn rs_websocket_state_free(state: *mut c_void) {
std::mem::drop(Box::from_raw(state as *mut WebSocketState));
}
unsafe extern "C" fn rs_websocket_state_tx_free(state: *mut c_void, tx_id: u64) {
let state = cast_pointer!(state, WebSocketState);
state.free_tx(tx_id);
}
unsafe extern "C" fn rs_websocket_parse_request(
flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice,
_data: *const c_void,
) -> AppLayerResult {
let state = cast_pointer!(state, WebSocketState);
state.parse(stream_slice, Direction::ToServer, flow)
}
unsafe extern "C" fn rs_websocket_parse_response(
flow: *const Flow, state: *mut c_void, _pstate: *mut c_void, stream_slice: StreamSlice,
_data: *const c_void,
) -> AppLayerResult {
let state = cast_pointer!(state, WebSocketState);
state.parse(stream_slice, Direction::ToClient, flow)
}
unsafe extern "C" fn rs_websocket_state_get_tx(state: *mut c_void, tx_id: u64) -> *mut c_void {
let state = cast_pointer!(state, WebSocketState);
match state.get_tx(tx_id) {
Some(tx) => {
return tx as *const _ as *mut _;
}
None => {
return std::ptr::null_mut();
}
}
}
unsafe extern "C" fn rs_websocket_state_get_tx_count(state: *mut c_void) -> u64 {
let state = cast_pointer!(state, WebSocketState);
return state.tx_id;
}
unsafe extern "C" fn rs_websocket_tx_get_alstate_progress(
_tx: *mut c_void, _direction: u8,
) -> c_int {
return 1;
}
export_tx_data_get!(rs_websocket_get_tx_data, WebSocketTransaction);
export_state_data_get!(rs_websocket_get_state_data, WebSocketState);
// Parser name as a C style string.
const PARSER_NAME: &[u8] = b"websocket\0";
#[no_mangle]
pub unsafe extern "C" fn rs_websocket_register_parser() {
let parser = RustParser {
name: PARSER_NAME.as_ptr() as *const c_char,
default_port: std::ptr::null(),
ipproto: IPPROTO_TCP,
probe_ts: Some(rs_websocket_probing_parser),
probe_tc: Some(rs_websocket_probing_parser),
min_depth: 0,
max_depth: 16,
state_new: rs_websocket_state_new,
state_free: rs_websocket_state_free,
tx_free: rs_websocket_state_tx_free,
parse_ts: rs_websocket_parse_request,
parse_tc: rs_websocket_parse_response,
get_tx_count: rs_websocket_state_get_tx_count,
get_tx: rs_websocket_state_get_tx,
tx_comp_st_ts: 1,
tx_comp_st_tc: 1,
tx_get_progress: rs_websocket_tx_get_alstate_progress,
get_eventinfo: Some(WebSocketEvent::get_event_info),
get_eventinfo_byid: Some(WebSocketEvent::get_event_info_by_id),
localstorage_new: None,
localstorage_free: None,
get_tx_files: None,
get_tx_iterator: Some(
applayer::state_get_tx_iterator::<WebSocketState, WebSocketTransaction>,
),
get_tx_data: rs_websocket_get_tx_data,
get_state_data: rs_websocket_get_state_data,
apply_tx_config: None,
flags: 0, // do not accept gaps as there is no good way to resync
truncate: None,
get_frame_id_by_name: Some(WebSocketFrameType::ffi_id_from_name),
get_frame_name_by_id: Some(WebSocketFrameType::ffi_name_from_id),
};
let ip_proto_str = CString::new("tcp").unwrap();
if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let alproto = AppLayerRegisterProtocolDetection(&parser, 1);
ALPROTO_WEBSOCKET = alproto;
if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
let _ = AppLayerRegisterParser(&parser, alproto);
}
SCLogDebug!("Rust websocket parser registered.");
if let Some(val) = conf_get("app-layer.protocols.websocket.max-payload-size") {
if let Ok(v) = val.parse::<u32>() {
WEBSOCKET_MAX_PAYLOAD_SIZE = v;
} else {
SCLogError!("Invalid value for websocket.max-payload-size");
}
}
AppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_WEBSOCKET);
} else {
SCLogDebug!("Protocol detector and parser disabled for WEBSOCKET.");
}
}

@ -362,6 +362,7 @@ noinst_HEADERS = \
detect-urilen.h \ detect-urilen.h \
detect-within.h \ detect-within.h \
detect-xbits.h \ detect-xbits.h \
detect-websocket.h \
device-storage.h \ device-storage.h \
feature.h \ feature.h \
flow-bit.h \ flow-bit.h \
@ -975,6 +976,7 @@ libsuricata_c_a_SOURCES = \
detect-urilen.c \ detect-urilen.c \
detect-within.c \ detect-within.c \
detect-xbits.c \ detect-xbits.c \
detect-websocket.c \
device-storage.c \ device-storage.c \
feature.c \ feature.c \
flow-bit.c \ flow-bit.c \

@ -53,6 +53,7 @@
#include "app-layer-protos.h" #include "app-layer-protos.h"
#include "app-layer-parser.h" #include "app-layer-parser.h"
#include "app-layer-expectation.h"
#include "app-layer.h" #include "app-layer.h"
#include "app-layer-detect-proto.h" #include "app-layer-detect-proto.h"
@ -979,11 +980,7 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa
if (tx != NULL && tx->response_status_number == 101) { if (tx != NULL && tx->response_status_number == 101) {
htp_header_t *h = htp_header_t *h =
(htp_header_t *)htp_table_get_c(tx->response_headers, "Upgrade"); (htp_header_t *)htp_table_get_c(tx->response_headers, "Upgrade");
if (h == NULL || bstr_cmp_c(h->value, "h2c") != 0) { if (h == NULL) {
break;
}
if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) {
// if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode
break; break;
} }
uint16_t dp = 0; uint16_t dp = 0;
@ -991,6 +988,11 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa
dp = (uint16_t)tx->request_port_number; dp = (uint16_t)tx->request_port_number;
} }
consumed = htp_connp_res_data_consumed(hstate->connp); consumed = htp_connp_res_data_consumed(hstate->connp);
if (bstr_cmp_c(h->value, "h2c") == 0) {
if (AppLayerProtoDetectGetProtoName(ALPROTO_HTTP2) == NULL) {
// if HTTP2 is disabled, keep the HTP_STREAM_TUNNEL mode
break;
}
hstate->slice = NULL; hstate->slice = NULL;
if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) { if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_HTTP2)) {
HTPSetEvent(hstate, NULL, STREAM_TOCLIENT, HTPSetEvent(hstate, NULL, STREAM_TOCLIENT,
@ -1002,6 +1004,23 @@ static AppLayerResult HTPHandleResponseData(Flow *f, void *htp_state, AppLayerPa
SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed)); SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed));
} }
SCReturnStruct(APP_LAYER_OK); SCReturnStruct(APP_LAYER_OK);
} else if (bstr_cmp_c_nocase(h->value, "WebSocket") == 0) {
if (AppLayerProtoDetectGetProtoName(ALPROTO_WEBSOCKET) == NULL) {
// if WS is disabled, keep the HTP_STREAM_TUNNEL mode
break;
}
hstate->slice = NULL;
if (!AppLayerRequestProtocolChange(hstate->f, dp, ALPROTO_WEBSOCKET)) {
HTPSetEvent(hstate, NULL, STREAM_TOCLIENT,
HTTP_DECODER_EVENT_FAILED_PROTOCOL_CHANGE);
}
// During WS upgrade, we may consume the HTTP1 part of the data
// and we need to parser the remaining part with WS
if (consumed > 0 && consumed < input_len) {
SCReturnStruct(APP_LAYER_INCOMPLETE(consumed, input_len - consumed));
}
SCReturnStruct(APP_LAYER_OK);
}
} }
break; break;
default: default:

@ -1754,6 +1754,7 @@ void AppLayerParserRegisterProtocolParsers(void)
RegisterSNMPParsers(); RegisterSNMPParsers();
RegisterSIPParsers(); RegisterSIPParsers();
RegisterQuicParsers(); RegisterQuicParsers();
rs_websocket_register_parser();
rs_template_register_parser(); rs_template_register_parser();
RegisterRFBParsers(); RegisterRFBParsers();
SCMqttRegisterParser(); SCMqttRegisterParser();

@ -60,6 +60,7 @@ const AppProtoStringTuple AppProtoStrings[ALPROTO_MAX] = {
{ ALPROTO_MQTT, "mqtt" }, { ALPROTO_MQTT, "mqtt" },
{ ALPROTO_PGSQL, "pgsql" }, { ALPROTO_PGSQL, "pgsql" },
{ ALPROTO_TELNET, "telnet" }, { ALPROTO_TELNET, "telnet" },
{ ALPROTO_WEBSOCKET, "websocket" },
{ ALPROTO_TEMPLATE, "template" }, { ALPROTO_TEMPLATE, "template" },
{ ALPROTO_RDP, "rdp" }, { ALPROTO_RDP, "rdp" },
{ ALPROTO_HTTP2, "http2" }, { ALPROTO_HTTP2, "http2" },

@ -56,6 +56,7 @@ enum AppProtoEnum {
ALPROTO_MQTT, ALPROTO_MQTT,
ALPROTO_PGSQL, ALPROTO_PGSQL,
ALPROTO_TELNET, ALPROTO_TELNET,
ALPROTO_WEBSOCKET,
ALPROTO_TEMPLATE, ALPROTO_TEMPLATE,
ALPROTO_RDP, ALPROTO_RDP,
ALPROTO_HTTP2, ALPROTO_HTTP2,

@ -241,6 +241,7 @@
#include "detect-quic-cyu-hash.h" #include "detect-quic-cyu-hash.h"
#include "detect-quic-cyu-string.h" #include "detect-quic-cyu-string.h"
#include "detect-ja4-hash.h" #include "detect-ja4-hash.h"
#include "detect-websocket.h"
#include "detect-bypass.h" #include "detect-bypass.h"
#include "detect-ftpdata.h" #include "detect-ftpdata.h"
@ -709,6 +710,7 @@ void SigTableSetup(void)
DetectQuicCyuHashRegister(); DetectQuicCyuHashRegister();
DetectQuicCyuStringRegister(); DetectQuicCyuStringRegister();
DetectJa4HashRegister(); DetectJa4HashRegister();
DetectWebsocketRegister();
DetectBypassRegister(); DetectBypassRegister();
DetectConfigRegister(); DetectConfigRegister();

@ -319,6 +319,10 @@ enum DetectKeywordId {
DETECT_AL_QUIC_UA, DETECT_AL_QUIC_UA,
DETECT_AL_QUIC_CYU_HASH, DETECT_AL_QUIC_CYU_HASH,
DETECT_AL_QUIC_CYU_STRING, DETECT_AL_QUIC_CYU_STRING,
DETECT_WEBSOCKET_MASK,
DETECT_WEBSOCKET_OPCODE,
DETECT_WEBSOCKET_FLAGS,
DETECT_WEBSOCKET_PAYLOAD,
DETECT_BYPASS, DETECT_BYPASS,

@ -0,0 +1,251 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
/**
* \file
*
* \author Philippe Antoine
*/
#include "suricata-common.h"
#include "detect.h"
#include "detect-parse.h"
#include "detect-engine.h"
#include "detect-engine-content-inspection.h"
#include "detect-engine-uint.h"
#include "detect-engine-prefilter.h"
#include "detect-websocket.h"
#include "rust.h"
static int websocket_tx_id = 0;
static int websocket_payload_id = 0;
/**
* \internal
* \brief this function will free memory associated with DetectWebSocketOpcodeData
*
* \param de pointer to DetectWebSocketOpcodeData
*/
static void DetectWebSocketOpcodeFree(DetectEngineCtx *de_ctx, void *de_ptr)
{
rs_detect_u8_free(de_ptr);
}
/**
* \internal
* \brief Function to match opcode of a websocket tx
*
* \param det_ctx Pointer to the pattern matcher thread.
* \param f Pointer to the current flow.
* \param flags Flags.
* \param state App layer state.
* \param txv Pointer to the transaction.
* \param s Pointer to the Signature.
* \param ctx Pointer to the sigmatch that we will cast into DetectWebSocketOpcodeData.
*
* \retval 0 no match.
* \retval 1 match.
*/
static int DetectWebSocketOpcodeMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags,
void *state, void *txv, const Signature *s, const SigMatchCtx *ctx)
{
const DetectU8Data *de = (const DetectU8Data *)ctx;
uint8_t opc = SCWebSocketGetOpcode(txv);
return DetectU8Match(opc, de);
}
/**
* \internal
* \brief this function is used to add the parsed sigmatch into the current signature
*
* \param de_ctx pointer to the Detection Engine Context
* \param s pointer to the Current Signature
* \param rawstr pointer to the user provided options
*
* \retval 0 on Success
* \retval -1 on Failure
*/
static int DetectWebSocketOpcodeSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
{
if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0)
return -1;
DetectU8Data *de = SCWebSocketParseOpcode(rawstr);
if (de == NULL)
return -1;
if (SigMatchAppendSMToList(
de_ctx, s, DETECT_WEBSOCKET_OPCODE, (SigMatchCtx *)de, websocket_tx_id) == NULL) {
DetectWebSocketOpcodeFree(de_ctx, de);
return -1;
}
return 0;
}
/**
* \internal
* \brief this function will free memory associated with DetectWebSocketMaskData
*
* \param de pointer to DetectWebSocketMaskData
*/
static void DetectWebSocketMaskFree(DetectEngineCtx *de_ctx, void *de_ptr)
{
rs_detect_u32_free(de_ptr);
}
static int DetectWebSocketMaskMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags,
void *state, void *txv, const Signature *s, const SigMatchCtx *ctx)
{
uint32_t val;
const DetectU32Data *du32 = (const DetectU32Data *)ctx;
if (SCWebSocketGetMask(txv, &val)) {
return DetectU32Match(val, du32);
}
return 0;
}
static int DetectWebSocketMaskSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
{
if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0)
return -1;
DetectU32Data *du32 = DetectU32Parse(rawstr);
if (du32 == NULL)
return -1;
if (SigMatchAppendSMToList(
de_ctx, s, DETECT_WEBSOCKET_MASK, (SigMatchCtx *)du32, websocket_tx_id) == NULL) {
DetectWebSocketMaskFree(de_ctx, du32);
return -1;
}
return 0;
}
static void DetectWebSocketFlagsFree(DetectEngineCtx *de_ctx, void *de_ptr)
{
rs_detect_u8_free(de_ptr);
}
static int DetectWebSocketFlagsMatch(DetectEngineThreadCtx *det_ctx, Flow *f, uint8_t flags,
void *state, void *txv, const Signature *s, const SigMatchCtx *ctx)
{
const DetectU8Data *de = (const DetectU8Data *)ctx;
uint8_t val = SCWebSocketGetFlags(txv);
return DetectU8Match(val, de);
}
static int DetectWebSocketFlagsSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rawstr)
{
if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) < 0)
return -1;
DetectU8Data *de = SCWebSocketParseFlags(rawstr);
if (de == NULL)
return -1;
if (SigMatchAppendSMToList(
de_ctx, s, DETECT_WEBSOCKET_FLAGS, (SigMatchCtx *)de, websocket_tx_id) == NULL) {
DetectWebSocketOpcodeFree(de_ctx, de);
return -1;
}
return 0;
}
static int DetectWebSocketPayloadSetup(DetectEngineCtx *de_ctx, Signature *s, const char *rulestr)
{
if (DetectBufferSetActiveList(de_ctx, s, websocket_payload_id) < 0)
return -1;
if (DetectSignatureSetAppProto(s, ALPROTO_WEBSOCKET) != 0)
return -1;
return 0;
}
static InspectionBuffer *GetData(DetectEngineThreadCtx *det_ctx,
const DetectEngineTransforms *transforms, Flow *_f, const uint8_t _flow_flags, void *txv,
const int list_id)
{
InspectionBuffer *buffer = InspectionBufferGet(det_ctx, list_id);
if (buffer->inspect == NULL) {
const uint8_t *b = NULL;
uint32_t b_len = 0;
if (!SCWebSocketGetPayload(txv, &b, &b_len))
return NULL;
if (b == NULL || b_len == 0)
return NULL;
InspectionBufferSetup(det_ctx, list_id, buffer, b, b_len);
InspectionBufferApplyTransforms(buffer, transforms);
}
return buffer;
}
/**
* \brief Registration function for websocket.opcode: keyword
*/
void DetectWebsocketRegister(void)
{
sigmatch_table[DETECT_WEBSOCKET_OPCODE].name = "websocket.opcode";
sigmatch_table[DETECT_WEBSOCKET_OPCODE].desc = "match WebSocket opcode";
sigmatch_table[DETECT_WEBSOCKET_OPCODE].url = "/rules/websocket-keywords.html#websocket-opcode";
sigmatch_table[DETECT_WEBSOCKET_OPCODE].AppLayerTxMatch = DetectWebSocketOpcodeMatch;
sigmatch_table[DETECT_WEBSOCKET_OPCODE].Setup = DetectWebSocketOpcodeSetup;
sigmatch_table[DETECT_WEBSOCKET_OPCODE].Free = DetectWebSocketOpcodeFree;
DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER, 1,
DetectEngineInspectGenericList, NULL);
DetectAppLayerInspectEngineRegister("websocket.tx", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT, 1,
DetectEngineInspectGenericList, NULL);
websocket_tx_id = DetectBufferTypeGetByName("websocket.tx");
sigmatch_table[DETECT_WEBSOCKET_MASK].name = "websocket.mask";
sigmatch_table[DETECT_WEBSOCKET_MASK].desc = "match WebSocket mask";
sigmatch_table[DETECT_WEBSOCKET_MASK].url = "/rules/websocket-keywords.html#websocket-mask";
sigmatch_table[DETECT_WEBSOCKET_MASK].AppLayerTxMatch = DetectWebSocketMaskMatch;
sigmatch_table[DETECT_WEBSOCKET_MASK].Setup = DetectWebSocketMaskSetup;
sigmatch_table[DETECT_WEBSOCKET_MASK].Free = DetectWebSocketMaskFree;
sigmatch_table[DETECT_WEBSOCKET_FLAGS].name = "websocket.flags";
sigmatch_table[DETECT_WEBSOCKET_FLAGS].desc = "match WebSocket flags";
sigmatch_table[DETECT_WEBSOCKET_FLAGS].url = "/rules/websocket-keywords.html#websocket-flags";
sigmatch_table[DETECT_WEBSOCKET_FLAGS].AppLayerTxMatch = DetectWebSocketFlagsMatch;
sigmatch_table[DETECT_WEBSOCKET_FLAGS].Setup = DetectWebSocketFlagsSetup;
sigmatch_table[DETECT_WEBSOCKET_FLAGS].Free = DetectWebSocketFlagsFree;
sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].name = "websocket.payload";
sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].desc = "match WebSocket payload";
sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].url =
"/rules/websocket-keywords.html#websocket-payload";
sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].Setup = DetectWebSocketPayloadSetup;
sigmatch_table[DETECT_WEBSOCKET_PAYLOAD].flags |= SIGMATCH_NOOPT;
DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOSERVER,
0, DetectEngineInspectBufferGeneric, GetData);
DetectAppLayerInspectEngineRegister("websocket.payload", ALPROTO_WEBSOCKET, SIG_FLAG_TOCLIENT,
0, DetectEngineInspectBufferGeneric, GetData);
DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOSERVER, 2,
PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1);
DetectAppLayerMpmRegister("websocket.payload", SIG_FLAG_TOCLIENT, 2,
PrefilterGenericMpmRegister, GetData, ALPROTO_WEBSOCKET, 1);
websocket_payload_id = DetectBufferTypeGetByName("websocket.payload");
}

@ -0,0 +1,29 @@
/* Copyright (C) 2023 Open Information Security Foundation
*
* You can copy, redistribute or modify this Program under the terms of
* the GNU General Public License version 2 as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* version 2 along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
/**
* \file
*
* \author Philippe Antoine
*/
#ifndef __DETECT_WEBSOCKET_H__
#define __DETECT_WEBSOCKET_H__
void DetectWebsocketRegister(void);
#endif /* __DETECT_WEBSOCKET_H__ */

@ -1083,6 +1083,10 @@ void OutputRegisterLoggers(void)
JsonMQTTLogRegister(); JsonMQTTLogRegister();
/* Pgsql JSON logger. */ /* Pgsql JSON logger. */
JsonPgsqlLogRegister(); JsonPgsqlLogRegister();
/* WebSocket JSON logger. */
OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketLog", "eve-log.websocket",
OutputJsonLogInitSub, ALPROTO_WEBSOCKET, JsonGenericDirPacketLogger, JsonLogThreadInit,
JsonLogThreadDeinit, NULL);
/* Template JSON logger. */ /* Template JSON logger. */
OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template", OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template",
OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit, OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit,
@ -1135,6 +1139,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = {
{ ALPROTO_MQTT, JsonMQTTAddMetadata }, { ALPROTO_MQTT, JsonMQTTAddMetadata },
{ ALPROTO_PGSQL, JsonPgsqlAddMetadata }, { ALPROTO_PGSQL, JsonPgsqlAddMetadata },
{ ALPROTO_TELNET, NULL }, // no logging { ALPROTO_TELNET, NULL }, // no logging
{ ALPROTO_WEBSOCKET, rs_websocket_logger_log },
{ ALPROTO_TEMPLATE, rs_template_logger_log }, { ALPROTO_TEMPLATE, rs_template_logger_log },
{ ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json }, { ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json },
{ ALPROTO_HTTP2, rs_http2_log_json }, { ALPROTO_HTTP2, rs_http2_log_json },

@ -284,6 +284,7 @@ outputs:
#md5: [body, subject] #md5: [body, subject]
#- dnp3 #- dnp3
- websocket
- ftp - ftp
- rdp - rdp
- nfs - nfs
@ -927,6 +928,10 @@ app-layer:
ftp: ftp:
enabled: yes enabled: yes
# memcap: 64mb # memcap: 64mb
websocket:
#enabled: yes
# Maximum used payload size, the rest is skipped
# max-payload-size: 65535
rdp: rdp:
#enabled: yes #enabled: yes
ssh: ssh:

Loading…
Cancel
Save