app-layer: websockets protocol support

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

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

@ -110,6 +110,7 @@ you can pick from. These are:
* snmp
* tftp
* sip
* websocket
The availability of these protocols depends on whether the protocol
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": {
"description": "Errors encountered parsing TLS protocol",
"$ref": "#/$defs/stats_applayer_error"
},
"websocket": {
"$ref": "#/$defs/stats_applayer_error"
}
},
"additionalProperties": false
@ -4056,6 +4059,9 @@
"tls": {
"description": "Number of flows for TLS protocol",
"type": "integer"
},
"websocket": {
"type": "integer"
}
},
"additionalProperties": false
@ -4170,6 +4176,9 @@
},
"tls": {
"type": "integer"
},
"websocket": {
"type": "integer"
}
},
"additionalProperties": false
@ -5653,6 +5662,21 @@
}
},
"additionalProperties": false
},
"websocket": {
"type": "object",
"properties": {
"fin": {
"type": "boolean"
},
"mask": {
"type": "integer"
},
"opcode": {
"type": "string"
}
},
"additionalProperties": false
}
},
"$defs": {

@ -22,4 +22,5 @@ smb-events.rules \
smtp-events.rules \
ssh-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 pgsql;
pub mod telnet;
pub mod websocket;
pub mod applayertemplate;
pub mod rdp;
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-within.h \
detect-xbits.h \
detect-websocket.h \
device-storage.h \
feature.h \
flow-bit.h \
@ -975,6 +976,7 @@ libsuricata_c_a_SOURCES = \
detect-urilen.c \
detect-within.c \
detect-xbits.c \
detect-websocket.c \
device-storage.c \
feature.c \
flow-bit.c \

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

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

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

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

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

@ -319,6 +319,10 @@ enum DetectKeywordId {
DETECT_AL_QUIC_UA,
DETECT_AL_QUIC_CYU_HASH,
DETECT_AL_QUIC_CYU_STRING,
DETECT_WEBSOCKET_MASK,
DETECT_WEBSOCKET_OPCODE,
DETECT_WEBSOCKET_FLAGS,
DETECT_WEBSOCKET_PAYLOAD,
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();
/* Pgsql JSON logger. */
JsonPgsqlLogRegister();
/* WebSocket JSON logger. */
OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonWebSocketLog", "eve-log.websocket",
OutputJsonLogInitSub, ALPROTO_WEBSOCKET, JsonGenericDirPacketLogger, JsonLogThreadInit,
JsonLogThreadDeinit, NULL);
/* Template JSON logger. */
OutputRegisterTxSubModule(LOGGER_JSON_TX, "eve-log", "JsonTemplateLog", "eve-log.template",
OutputJsonLogInitSub, ALPROTO_TEMPLATE, JsonGenericDirPacketLogger, JsonLogThreadInit,
@ -1135,6 +1139,7 @@ static EveJsonSimpleAppLayerLogger simple_json_applayer_loggers[ALPROTO_MAX] = {
{ ALPROTO_MQTT, JsonMQTTAddMetadata },
{ ALPROTO_PGSQL, JsonPgsqlAddMetadata },
{ ALPROTO_TELNET, NULL }, // no logging
{ ALPROTO_WEBSOCKET, rs_websocket_logger_log },
{ ALPROTO_TEMPLATE, rs_template_logger_log },
{ ALPROTO_RDP, (EveJsonSimpleTxLogFunc)rs_rdp_to_json },
{ ALPROTO_HTTP2, rs_http2_log_json },

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

Loading…
Cancel
Save