|
|
|
|
/* Copyright (C) 2018-2020 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.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// written by Victor Julien
|
|
|
|
|
|
|
|
|
|
use nom7::bytes::streaming::take;
|
nfs: log a v4 COMPOUND the record parser cannot read
The error arm of a COMPOUND that is fully in hand only set a state event.
set_event marks the newest transaction, and there is none when the
record's first op is the one that fails: the record was then consumed,
nothing was logged, and nothing asked for the missing bytes. A v4 WRITE
whose count runs past the end of its record is that case - the parser
gives up at the data, so the size check in the WRITE handling, which
needs a parsed count, is never reached.
Reject what cannot be walked the way a partial record is rejected: scan
the bytes the parser was given, and log an oversized WRITE as a
write_request_too_large file transaction or another un-walkable record as
a malformed one, on a transaction of its own. What the parser skips is
not in that set: a defined op this parser does not implement keeps base's
event on the newest tx, and an RPCSEC_GSS privacy compound, whose result
is opaque by RFC 2203 on the call and the reply alike, is left unparsed
with no event at all. Either way the record is bounded by its own marker,
and a flow that carries un-parseable v4 traffic leaves a trace in EVE.
Ticket: #9265.
1 week ago
|
|
|
use nom7::error::ErrorKind;
|
|
|
|
|
use nom7::number::streaming::be_u32;
|
|
|
|
|
use nom7::{Err, IResult};
|
|
|
|
|
|
|
|
|
|
use crate::direction::Direction;
|
|
|
|
|
use crate::flow::Flow;
|
|
|
|
|
use crate::nfs::nfs::*;
|
|
|
|
|
use crate::nfs::nfs4_records::*;
|
|
|
|
|
use crate::nfs::nfs_records::*;
|
|
|
|
|
use crate::nfs::rpc_records::*;
|
|
|
|
|
use crate::nfs::types::*;
|
|
|
|
|
|
|
|
|
|
use crate::kerberos::{parse_kerberos5_request, Kerberos5Ticket, SecBlobError};
|
|
|
|
|
|
|
|
|
|
fn parse_req_gssapi(i: &[u8]) -> IResult<&[u8], Kerberos5Ticket, SecBlobError> {
|
|
|
|
|
let (i, len) = be_u32(i)?;
|
|
|
|
|
let (i, buf) = take(len as usize)(i)?;
|
|
|
|
|
let (_, ap) = parse_kerberos5_request(buf)?;
|
|
|
|
|
Ok((i, ap))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl NFSState {
|
|
|
|
|
/* normal write: PUTFH (file handle), WRITE (write opts/data). File handle
|
|
|
|
|
* is not part of the write record itself so we pass it in here. */
|
|
|
|
|
fn write_v4<'b>(&mut self, r: &RpcPacket<'b>, w: &Nfs4RequestWrite<'b>, fh: &'b [u8]) {
|
|
|
|
|
// for now assume that stable FILE_SYNC flags means a single chunk
|
|
|
|
|
let is_last = w.stable == 2;
|
|
|
|
|
SCLogDebug!("is_last {}", is_last);
|
|
|
|
|
|
nfs: validate WRITE and READ records sizes
Replace the hardcoded 16 MiB record caps with configurable
max-{write,read}-size and observable events: an oversized WRITE
request or READ response is rejected, logged, and its payload skipped
to keep the stream in sync. 0 disables the check.
For v4 records, the issue is more complex due to the COMPOUND type:
A partial v4 COMPOUND record whose frag_len claim is huge must not be
buffered toward that claim (31-bit, attacker-controlled → OOM). The
full parser can't help: on a partial buffer it only returns Incomplete
("need more bytes"), with no information about what's coming. The scan
instead walks the buffered prefix's ops by wire extent and reads the
file op's header words alone, answering the gate's four-way question:
oversized claim detected (reject + skip to the record boundary before
any data buffers), data not yet present (buffer normally), structurall
malformed (bounded rejection), or no oversized op (fall through). It
exists because rejecting before buffering requires the claimed length,
which only a prefix walk can extract.
Ticket: #8791.
2 months ago
|
|
|
if w.write_len == 0 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let file_handle = fh.to_vec();
|
|
|
|
|
let file_name = if let Some(name) = self.namemap.get(fh) {
|
|
|
|
|
SCLogDebug!("WRITE name {:?}", name);
|
|
|
|
|
name.to_vec()
|
|
|
|
|
} else {
|
|
|
|
|
SCLogDebug!("WRITE object {:?} not found", w.stateid.data);
|
|
|
|
|
Vec::new()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Reject an oversized WRITE: log it as a tx and skip to the record
|
|
|
|
|
// boundary to keep the flow in sync.
|
nfs: validate WRITE and READ records sizes
Replace the hardcoded 16 MiB record caps with configurable
max-{write,read}-size and observable events: an oversized WRITE
request or READ response is rejected, logged, and its payload skipped
to keep the stream in sync. 0 disables the check.
For v4 records, the issue is more complex due to the COMPOUND type:
A partial v4 COMPOUND record whose frag_len claim is huge must not be
buffered toward that claim (31-bit, attacker-controlled → OOM). The
full parser can't help: on a partial buffer it only returns Incomplete
("need more bytes"), with no information about what's coming. The scan
instead walks the buffered prefix's ops by wire extent and reads the
file op's header words alone, answering the gate's four-way question:
oversized claim detected (reject + skip to the record boundary before
any data buffers), data not yet present (buffer normally), structurall
malformed (bounded rejection), or no oversized op (fall through). It
exists because rejecting before buffering requires the claimed length,
which only a prefix walk can extract.
Ticket: #8791.
2 months ago
|
|
|
let max_write_size = unsafe { NFS_CFG_MAX_WRITE_SIZE };
|
|
|
|
|
if max_write_size != 0 && w.write_len > max_write_size {
|
|
|
|
|
let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToServer);
|
|
|
|
|
tx.procedure = NFSPROC4_WRITE;
|
|
|
|
|
tx.xid = r.hdr.xid;
|
|
|
|
|
tx.is_first = true;
|
|
|
|
|
tx.nfs_version = r.progver as u16;
|
|
|
|
|
tx.request_done = true;
|
|
|
|
|
tx.response_done = true;
|
|
|
|
|
// Close the tx so a later same-handle op cannot reuse it.
|
|
|
|
|
tx.is_file_closed = true;
|
nfs: validate WRITE and READ records sizes
Replace the hardcoded 16 MiB record caps with configurable
max-{write,read}-size and observable events: an oversized WRITE
request or READ response is rejected, logged, and its payload skipped
to keep the stream in sync. 0 disables the check.
For v4 records, the issue is more complex due to the COMPOUND type:
A partial v4 COMPOUND record whose frag_len claim is huge must not be
buffered toward that claim (31-bit, attacker-controlled → OOM). The
full parser can't help: on a partial buffer it only returns Incomplete
("need more bytes"), with no information about what's coming. The scan
instead walks the buffered prefix's ops by wire extent and reads the
file op's header words alone, answering the gate's four-way question:
oversized claim detected (reject + skip to the record boundary before
any data buffers), data not yet present (buffer normally), structurall
malformed (bounded rejection), or no oversized op (fall through). It
exists because rejecting before buffering requires the claimed length,
which only a prefix walk can extract.
Ticket: #8791.
2 months ago
|
|
|
tx.tx_data.set_event(NFSEvent::WriteRequestTooLarge as u8);
|
|
|
|
|
// Complete the rejected transaction immediately: without
|
|
|
|
|
// this the tx and its event only surface at flow teardown.
|
|
|
|
|
tx.tx_data.0.updated_ts = true;
|
|
|
|
|
tx.tx_data.0.updated_tc = true;
|
nfs: validate WRITE and READ records sizes
Replace the hardcoded 16 MiB record caps with configurable
max-{write,read}-size and observable events: an oversized WRITE
request or READ response is rejected, logged, and its payload skipped
to keep the stream in sync. 0 disables the check.
For v4 records, the issue is more complex due to the COMPOUND type:
A partial v4 COMPOUND record whose frag_len claim is huge must not be
buffered toward that claim (31-bit, attacker-controlled → OOM). The
full parser can't help: on a partial buffer it only returns Incomplete
("need more bytes"), with no information about what's coming. The scan
instead walks the buffered prefix's ops by wire extent and reads the
file op's header words alone, answering the gate's four-way question:
oversized claim detected (reject + skip to the record boundary before
any data buffers), data not yet present (buffer normally), structurall
malformed (bounded rejection), or no oversized op (fall through). It
exists because rejecting before buffering requires the claimed length,
which only a prefix walk can extract.
Ticket: #8791.
2 months ago
|
|
|
// Rejected tx never opened a file: zero files_opened, else the tx
|
|
|
|
|
// (and file) stays live until teardown and grows unbounded on repeats.
|
|
|
|
|
tx.tx_data.0.files_opened = 0;
|
|
|
|
|
// Skip to the RPC record boundary: claim vs data can disagree, and
|
|
|
|
|
// claimed-count math lands mid-padding or in a later record.
|
nfs: take the chunk promise and the record tail from the record
The queue-limit work computed the record tail and the chunk promise from
the declared size, and added a count or a data length to it. A padded or
segmented record therefore left a promise armed past the end of its
record: a continuation could run on into the next RPC, an input that
carries only record slack grew the out-of-order queue and truncated a
file whose queued data fits, and the backstop walked the transactions of
a call that holds no file transaction at all.
Take the promise from the record itself, with record_tail(): what its
marker claims minus what the parser holds. Keep its two halves apart:
the stream side is the record tail, the queue side is the file data the
tracker still misses, so a continuation queues no more than its own
bytes. A pad a record carries is inside its claim, so the tail drains
it; it is neither file data nor fill. Release a promise whose
transaction is gone instead of consuming toward the claim, run the
backstop only when the call carries data and the state holds a file
transaction, and count a chunk only for input that carries file data. A
consume past the end of an input is now a debug assertion, with the
advance clamped to the input.
Ticket: #9263.
1 week ago
|
|
|
let pending = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
|
nfs: validate WRITE and READ records sizes
Replace the hardcoded 16 MiB record caps with configurable
max-{write,read}-size and observable events: an oversized WRITE
request or READ response is rejected, logged, and its payload skipped
to keep the stream in sync. 0 disables the check.
For v4 records, the issue is more complex due to the COMPOUND type:
A partial v4 COMPOUND record whose frag_len claim is huge must not be
buffered toward that claim (31-bit, attacker-controlled → OOM). The
full parser can't help: on a partial buffer it only returns Incomplete
("need more bytes"), with no information about what's coming. The scan
instead walks the buffered prefix's ops by wire extent and reads the
file op's header words alone, answering the gate's four-way question:
oversized claim detected (reject + skip to the record boundary before
any data buffers), data not yet present (buffer normally), structurall
malformed (bounded rejection), or no oversized op (fall through). It
exists because rejecting before buffering requires the claimed length,
which only a prefix walk can extract.
Ticket: #8791.
2 months ago
|
|
|
self.set_skip(Direction::ToServer, pending);
|
|
|
|
|
self.ts_chunk_xid = 0;
|
|
|
|
|
self.ts_chunk_left = 0;
|
|
|
|
|
SCLogDebug!(
|
|
|
|
|
"WRITEv4 too large (claim {} > {}): skipping {} stream bytes",
|
|
|
|
|
w.write_len,
|
|
|
|
|
unsafe { NFS_CFG_MAX_WRITE_SIZE },
|
|
|
|
|
pending
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut queue_exceeded = false;
|
|
|
|
|
|
|
|
|
|
let found = match self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
|
|
|
|
|
Some(tx) => {
|
|
|
|
|
if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
|
|
|
|
|
if let Some(event) =
|
|
|
|
|
write_queue_limit_event(&tdf.file_tracker, w.offset, w.data.len() as u64)
|
|
|
|
|
{
|
|
|
|
|
// Attach to the affected file tx, not the newest tx.
|
|
|
|
|
tx.tx_data.set_event(event as u8);
|
|
|
|
|
tx.tx_data.0.updated_ts = true;
|
|
|
|
|
tx.tx_data.0.updated_tc = true;
|
|
|
|
|
queue_exceeded = true;
|
|
|
|
|
} else {
|
|
|
|
|
filetracker_newchunk(
|
|
|
|
|
&mut tdf.file_tracker,
|
|
|
|
|
&file_name,
|
|
|
|
|
w.data,
|
|
|
|
|
w.offset,
|
|
|
|
|
w.write_len,
|
nfs: take the chunk promise and the record tail from the record
The queue-limit work computed the record tail and the chunk promise from
the declared size, and added a count or a data length to it. A padded or
segmented record therefore left a promise armed past the end of its
record: a continuation could run on into the next RPC, an input that
carries only record slack grew the out-of-order queue and truncated a
file whose queued data fits, and the backstop walked the transactions of
a call that holds no file transaction at all.
Take the promise from the record itself, with record_tail(): what its
marker claims minus what the parser holds. Keep its two halves apart:
the stream side is the record tail, the queue side is the file data the
tracker still misses, so a continuation queues no more than its own
bytes. A pad a record carries is inside its claim, so the tail drains
it; it is neither file data nor fill. Release a promise whose
transaction is gone instead of consuming toward the claim, run the
backstop only when the call carries data and the state holds a file
transaction, and count a chunk only for input that carries file data. A
consume past the end of an input is now a debug assertion, with the
advance clamped to the input.
Ticket: #9263.
1 week ago
|
|
|
0,
|
|
|
|
|
is_last,
|
|
|
|
|
&r.hdr.xid,
|
|
|
|
|
);
|
|
|
|
|
tdf.chunk_count += 1;
|
|
|
|
|
if is_last {
|
|
|
|
|
tdf.file_last_xid = r.hdr.xid;
|
|
|
|
|
tx.is_last = true;
|
|
|
|
|
tx.response_done = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
None => false,
|
|
|
|
|
};
|
|
|
|
|
if !found {
|
|
|
|
|
let tx = self.new_file_tx(&file_handle, &file_name, Direction::ToServer);
|
|
|
|
|
if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
|
|
|
|
|
if let Some(event) =
|
|
|
|
|
write_queue_limit_event(&tdf.file_tracker, w.offset, w.data.len() as u64)
|
|
|
|
|
{
|
|
|
|
|
// tx just created: set the event on it directly.
|
|
|
|
|
tx.tx_data.set_event(event as u8);
|
|
|
|
|
tx.tx_data.0.updated_ts = true;
|
|
|
|
|
tx.tx_data.0.updated_tc = true;
|
|
|
|
|
queue_exceeded = true;
|
|
|
|
|
// Chunk rejected before the file was opened: complete + close the
|
|
|
|
|
// tx and zero files_opened, else it dangles until teardown.
|
|
|
|
|
tx.request_done = true;
|
|
|
|
|
tx.response_done = true;
|
|
|
|
|
tx.is_file_closed = true;
|
|
|
|
|
tx.tx_data.0.files_opened = 0;
|
|
|
|
|
} else {
|
|
|
|
|
filetracker_newchunk(
|
|
|
|
|
&mut tdf.file_tracker,
|
|
|
|
|
&file_name,
|
|
|
|
|
w.data,
|
|
|
|
|
w.offset,
|
|
|
|
|
w.write_len,
|
nfs: take the chunk promise and the record tail from the record
The queue-limit work computed the record tail and the chunk promise from
the declared size, and added a count or a data length to it. A padded or
segmented record therefore left a promise armed past the end of its
record: a continuation could run on into the next RPC, an input that
carries only record slack grew the out-of-order queue and truncated a
file whose queued data fits, and the backstop walked the transactions of
a call that holds no file transaction at all.
Take the promise from the record itself, with record_tail(): what its
marker claims minus what the parser holds. Keep its two halves apart:
the stream side is the record tail, the queue side is the file data the
tracker still misses, so a continuation queues no more than its own
bytes. A pad a record carries is inside its claim, so the tail drains
it; it is neither file data nor fill. Release a promise whose
transaction is gone instead of consuming toward the claim, run the
backstop only when the call carries data and the state holds a file
transaction, and count a chunk only for input that carries file data. A
consume past the end of an input is now a debug assertion, with the
advance clamped to the input.
Ticket: #9263.
1 week ago
|
|
|
0,
|
|
|
|
|
is_last,
|
|
|
|
|
&r.hdr.xid,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if is_last && !queue_exceeded {
|
|
|
|
|
tdf.file_last_xid = r.hdr.xid;
|
|
|
|
|
tx.is_last = true;
|
|
|
|
|
tx.request_done = true;
|
|
|
|
|
tx.is_file_closed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tx.procedure = NFSPROC4_WRITE;
|
|
|
|
|
tx.xid = r.hdr.xid;
|
|
|
|
|
tx.is_first = true;
|
|
|
|
|
tx.nfs_version = r.progver as u16;
|
|
|
|
|
}
|
|
|
|
|
if queue_exceeded {
|
|
|
|
|
// Don't buffer the rest of the record: skip to its RPC boundary.
|
nfs: take the chunk promise and the record tail from the record
The queue-limit work computed the record tail and the chunk promise from
the declared size, and added a count or a data length to it. A padded or
segmented record therefore left a promise armed past the end of its
record: a continuation could run on into the next RPC, an input that
carries only record slack grew the out-of-order queue and truncated a
file whose queued data fits, and the backstop walked the transactions of
a call that holds no file transaction at all.
Take the promise from the record itself, with record_tail(): what its
marker claims minus what the parser holds. Keep its two halves apart:
the stream side is the record tail, the queue side is the file data the
tracker still misses, so a continuation queues no more than its own
bytes. A pad a record carries is inside its claim, so the tail drains
it; it is neither file data nor fill. Release a promise whose
transaction is gone instead of consuming toward the claim, run the
backstop only when the call carries data and the state holds a file
transaction, and count a chunk only for input that carries file data. A
consume past the end of an input is now a debug assertion, with the
advance clamped to the input.
Ticket: #9263.
1 week ago
|
|
|
let pending = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
|
|
|
|
|
self.set_skip(Direction::ToServer, pending);
|
|
|
|
|
self.ts_chunk_xid = 0;
|
|
|
|
|
self.ts_chunk_left = 0;
|
|
|
|
|
self.ts_chunk_tail = 0;
|
|
|
|
|
SCLogDebug!("WRITE queue exceeded: skipping {} stream bytes", pending);
|
|
|
|
|
} else {
|
|
|
|
|
self.ts_chunk_xid = r.hdr.xid;
|
|
|
|
|
debug_validate_bug_on!(w.data.len() as u32 > w.write_len);
|
|
|
|
|
self.ts_chunk_left = w.write_len - w.data.len() as u32;
|
nfs: take the chunk promise and the record tail from the record
The queue-limit work computed the record tail and the chunk promise from
the declared size, and added a count or a data length to it. A padded or
segmented record therefore left a promise armed past the end of its
record: a continuation could run on into the next RPC, an input that
carries only record slack grew the out-of-order queue and truncated a
file whose queued data fits, and the backstop walked the transactions of
a call that holds no file transaction at all.
Take the promise from the record itself, with record_tail(): what its
marker claims minus what the parser holds. Keep its two halves apart:
the stream side is the record tail, the queue side is the file data the
tracker still misses, so a continuation queues no more than its own
bytes. A pad a record carries is inside its claim, so the tail drains
it; it is neither file data nor fill. Release a promise whose
transaction is gone instead of consuming toward the claim, run the
backstop only when the call carries data and the state holds a file
transaction, and count a chunk only for input that carries file data. A
consume past the end of an input is now a debug assertion, with the
advance clamped to the input.
Ticket: #9263.
1 week ago
|
|
|
// v4 records only reach the parser complete: nothing unbuffered,
|
|
|
|
|
// record tail 0 (no aligned-boundary correction like the v3 path)
|
|
|
|
|
self.ts_chunk_tail = crate::nfs::nfs::record_tail(r.prog_data_size, r.prog_data.len());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn close_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
|
|
|
|
|
self.commit_v4(r, fh)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn commit_v4<'b>(&mut self, r: &RpcPacket<'b>, fh: &'b [u8]) {
|
|
|
|
|
SCLogDebug!("COMMIT, closing shop");
|
|
|
|
|
|
|
|
|
|
let file_handle = fh.to_vec();
|
|
|
|
|
if let Some(tx) = self.get_file_tx_by_handle(&file_handle, Direction::ToServer) {
|
|
|
|
|
if let Some(NFSTransactionTypeData::FILE(ref mut tdf)) = tx.type_data {
|
|
|
|
|
filetracker_close(&mut tdf.file_tracker);
|
|
|
|
|
tdf.file_last_xid = r.hdr.xid;
|
|
|
|
|
tx.is_last = true;
|
|
|
|
|
tx.request_done = true;
|
|
|
|
|
tx.is_file_closed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn new_tx_v4(
|
|
|
|
|
&mut self, r: &RpcPacket, xidmap: &NFSRequestXidMap, procedure: u32, _aux_opcodes: &[u32],
|
|
|
|
|
) {
|
|
|
|
|
let mut tx = self.new_tx();
|
|
|
|
|
tx.xid = r.hdr.xid;
|
|
|
|
|
tx.procedure = procedure;
|
|
|
|
|
tx.request_done = true;
|
|
|
|
|
tx.file_name = xidmap.file_name.to_vec();
|
|
|
|
|
tx.nfs_version = r.progver as u16;
|
|
|
|
|
tx.file_handle = xidmap.file_handle.to_vec();
|
|
|
|
|
|
|
|
|
|
tx.auth_type = r.creds_flavor;
|
|
|
|
|
#[allow(clippy::single_match)]
|
|
|
|
|
match r.creds {
|
|
|
|
|
RpcRequestCreds::Unix(ref u) => {
|
|
|
|
|
tx.request_machine_name = u.machine_name_buf.to_vec();
|
|
|
|
|
tx.request_uid = u.uid;
|
|
|
|
|
tx.request_gid = u.gid;
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
SCLogDebug!(
|
|
|
|
|
"NFSv4: TX created: ID {} XID {} PROCEDURE {}",
|
|
|
|
|
tx.id,
|
|
|
|
|
tx.xid,
|
|
|
|
|
tx.procedure
|
|
|
|
|
);
|
|
|
|
|
self.transactions.push(tx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* A normal READ request looks like: PUTFH (file handle) READ (read opts).
|
|
|
|
|
* We need the file handle for the READ.
|
|
|
|
|
*/
|
|
|
|
|
fn compound_request<'b>(
|
|
|
|
|
&mut self, r: &RpcPacket<'b>, cr: &Nfs4RequestCompoundRecord<'b>,
|
|
|
|
|
xidmap: &mut NFSRequestXidMap,
|
|
|
|
|
) {
|
|
|
|
|
let mut last_putfh: Option<&'b [u8]> = None;
|
|
|
|
|
let mut main_opcode: u32 = 0;
|
|
|
|
|
let mut aux_opcodes: Vec<u32> = Vec::new();
|
|
|
|
|
|
|
|
|
|
for c in &cr.commands {
|
|
|
|
|
SCLogDebug!("c {:?}", c);
|
|
|
|
|
match *c {
|
|
|
|
|
Nfs4RequestContent::PutFH(ref rd) => {
|
|
|
|
|
last_putfh = Some(rd.value);
|
|
|
|
|
aux_opcodes.push(NFSPROC4_PUTFH);
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Read(ref rd) => {
|
|
|
|
|
SCLogDebug!("READv4: {:?}", rd);
|
|
|
|
|
if let Some(fh) = last_putfh {
|
|
|
|
|
xidmap.chunk_offset = rd.offset;
|
|
|
|
|
xidmap.file_handle = fh.to_vec();
|
|
|
|
|
self.xidmap_handle2name(xidmap);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Open(ref rd) => {
|
|
|
|
|
SCLogDebug!("OPENv4: {}", String::from_utf8_lossy(rd.filename));
|
|
|
|
|
xidmap.file_name = rd.filename.to_vec();
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Lookup(ref rd) => {
|
|
|
|
|
SCLogDebug!("LOOKUPv4: {}", String::from_utf8_lossy(rd.filename));
|
|
|
|
|
xidmap.file_name = rd.filename.to_vec();
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Write(ref rd) => {
|
|
|
|
|
SCLogDebug!("WRITEv4: {:?}", rd);
|
|
|
|
|
if let Some(fh) = last_putfh {
|
|
|
|
|
self.write_v4(r, rd, fh);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Commit => {
|
|
|
|
|
SCLogDebug!("COMMITv4");
|
|
|
|
|
if let Some(fh) = last_putfh {
|
|
|
|
|
self.commit_v4(r, fh);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Close(ref _rd) => {
|
|
|
|
|
SCLogDebug!("CLOSEv4: {:?}", _rd);
|
|
|
|
|
if let Some(fh) = last_putfh {
|
|
|
|
|
self.close_v4(r, fh);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Create(ref rd) => {
|
|
|
|
|
SCLogDebug!("CREATEv4: {:?}", rd);
|
|
|
|
|
if let Some(fh) = last_putfh {
|
|
|
|
|
xidmap.file_handle = fh.to_vec();
|
|
|
|
|
}
|
|
|
|
|
xidmap.file_name = rd.filename.to_vec();
|
|
|
|
|
main_opcode = NFSPROC4_CREATE;
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::Remove(rd) => {
|
|
|
|
|
SCLogDebug!("REMOVEv4: {:?}", rd);
|
|
|
|
|
xidmap.file_name = rd.to_vec();
|
|
|
|
|
main_opcode = NFSPROC4_REMOVE;
|
|
|
|
|
}
|
|
|
|
|
Nfs4RequestContent::SetClientId(ref _rd) => {
|
|
|
|
|
SCLogDebug!(
|
|
|
|
|
"SETCLIENTIDv4: client id {} r_netid {} r_addr {}",
|
|
|
|
|
String::from_utf8_lossy(_rd.client_id),
|
|
|
|
|
String::from_utf8_lossy(_rd.r_netid),
|
|
|
|
|
String::from_utf8_lossy(_rd.r_addr)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if main_opcode != 0 {
|
|
|
|
|
self.new_tx_v4(r, xidmap, main_opcode, &aux_opcodes);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// complete request record
|
|
|
|
|
pub fn process_request_record_v4(&mut self, r: &RpcPacket) {
|
|
|
|
|
SCLogDebug!(
|
|
|
|
|
"NFSv4 REQUEST {} procedure {} ({}) blob size {}",
|
|
|
|
|
r.hdr.xid,
|
|
|
|
|
r.procedure,
|
|
|
|
|
self.requestmap.len(),
|
|
|
|
|
r.prog_data.len()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let mut xidmap = NFSRequestXidMap::new(r.progver, r.procedure, 0);
|
|
|
|
|
|
|
|
|
|
if r.procedure == NFSPROC4_NULL {
|
|
|
|
|
if let RpcRequestCreds::GssApi(ref creds) = r.creds {
|
|
|
|
|
if creds.procedure == 1 {
|
|
|
|
|
let _x = parse_req_gssapi(r.prog_data);
|
|
|
|
|
SCLogDebug!("RPCSEC_GSS_INIT {:?}", _x);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if r.procedure == NFSPROC4_COMPOUND {
|
|
|
|
|
let mut data = r.prog_data;
|
|
|
|
|
|
|
|
|
|
if let RpcRequestCreds::GssApi(ref creds) = r.creds {
|
nfs: scan a GSS record by the combination of its call
The record scanners unwrapped the (0, 2) combination and took every other
one as a bare compound. A control procedure or a privacy compound was
then scanned as if it were a COMPOUND: the envelope length was read as
the first tag, so the scan either stayed incomplete and buffered toward
the claim, or rejected a record that has nothing to reject.
Scan by the combination the call recorded: hand the credential pair to
gss_compound_scan_data, which leaves (0, 1) raw because rpc_gss_svc_none
wraps nothing, unwraps (0, 2), and reports a privacy (0, 3) compound as
opaque. There is nothing to scan in an encrypted compound, so the record
is skipped to its boundary and neither parsed nor scored: the request
path keeps the call in the request map for its reply, the reply path
drops it. Any other combination is one the full record path rejects as
well, so it is malformed, boundedly and without buffering. The v4
request path records the combination whatever it is, not only when it
unwraps.
Ticket: #9263.
1 week ago
|
|
|
// store proc and serv for the reply; only (0, 2) is
|
|
|
|
|
// unwrapped below
|
|
|
|
|
xidmap.gssapi_proc = creds.procedure;
|
|
|
|
|
xidmap.gssapi_service = creds.service;
|
nfs: log a v4 COMPOUND the record parser cannot read
The error arm of a COMPOUND that is fully in hand only set a state event.
set_event marks the newest transaction, and there is none when the
record's first op is the one that fails: the record was then consumed,
nothing was logged, and nothing asked for the missing bytes. A v4 WRITE
whose count runs past the end of its record is that case - the parser
gives up at the data, so the size check in the WRITE handling, which
needs a parsed count, is never reached.
Reject what cannot be walked the way a partial record is rejected: scan
the bytes the parser was given, and log an oversized WRITE as a
write_request_too_large file transaction or another un-walkable record as
a malformed one, on a transaction of its own. What the parser skips is
not in that set: a defined op this parser does not implement keeps base's
event on the newest tx, and an RPCSEC_GSS privacy compound, whose result
is opaque by RFC 2203 on the call and the reply alike, is left unparsed
with no event at all. Either way the record is bounded by its own marker,
and a flow that carries un-parseable v4 traffic leaves a trace in EVE.
Ticket: #9265.
1 week ago
|
|
|
// privacy (0, 3) carries an encrypted compound: nothing to
|
|
|
|
|
// parse, and it is not malformed: just keep the request state
|
|
|
|
|
if creds.procedure == 0 && creds.service == 3 {
|
|
|
|
|
self.requestmap.put(r.hdr.xid, xidmap);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if creds.procedure == 0 && creds.service == 2 {
|
|
|
|
|
SCLogDebug!("GSS INTEGRITY: {:?}", creds);
|
|
|
|
|
match parse_rpc_gssapi_integrity(r.prog_data) {
|
|
|
|
|
Ok((_rem, rec)) => {
|
|
|
|
|
SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
|
|
|
|
|
data = rec.data;
|
|
|
|
|
}
|
nfs: record the RPCSEC_GSS integrity unwrap reject
A v4 COMPOUND carried with the GSS integrity service (creds procedure 0 =
RPCSEC_GSS_DATA, service 2) is wrapped in an envelope: length of the
wrapped body (sequence number and arguments), sequence number, arguments,
checksum (RFC 2203 5.3.2.2). When that cannot be unwrapped the compound
parser is never reached, and the arm ended with set_event(MalformedData).
set_event stamps the newest transaction of the state, and returns without
doing anything when the state has none. Two consequences: on a flow whose
first RPC is such a call, the reject produced no transaction, no anomaly
and nothing in EVE, so a record no server would ever send went by
unremarked; and on a reply, the event landed on whichever transaction
happened to be last, which on an async mount is a different call than the
one being rejected, while the tx owning the xid stayed open forever.
Give both arms a transaction to carry the event, like the partial-record
rejects already do. The request side reuses the malformed-record helper
(the xidmap is registered by the caller, so the gss creds survive and the
reply of the xid is still known to be wrapped). The reply side marks the
tx owning the xid, and falls back to a new one only when there is none -
which is the common branch, as a compound with no main-opcode result
(PUTFH+GETATTR, a SEQUENCE-led v4.1 one) opens no tx; that one therefore
carries the procedure the xidmap recorded, not the 0 that EVE reads as
NULL. Either way the record is skipped to its RPC boundary, never
buffered toward the claimed length.
Ticket: #9265.
1 week ago
|
|
|
Err(Err::Incomplete(_)) | Err(Err::Error(_)) | Err(Err::Failure(_)) => {
|
|
|
|
|
SCLogDebug!(
|
nfs: record the RPCSEC_GSS integrity unwrap reject
A v4 COMPOUND carried with the GSS integrity service (creds procedure 0 =
RPCSEC_GSS_DATA, service 2) is wrapped in an envelope: length of the
wrapped body (sequence number and arguments), sequence number, arguments,
checksum (RFC 2203 5.3.2.2). When that cannot be unwrapped the compound
parser is never reached, and the arm ended with set_event(MalformedData).
set_event stamps the newest transaction of the state, and returns without
doing anything when the state has none. Two consequences: on a flow whose
first RPC is such a call, the reject produced no transaction, no anomaly
and nothing in EVE, so a record no server would ever send went by
unremarked; and on a reply, the event landed on whichever transaction
happened to be last, which on an async mount is a different call than the
one being rejected, while the tx owning the xid stayed open forever.
Give both arms a transaction to carry the event, like the partial-record
rejects already do. The request side reuses the malformed-record helper
(the xidmap is registered by the caller, so the gss creds survive and the
reply of the xid is still known to be wrapped). The reply side marks the
tx owning the xid, and falls back to a new one only when there is none -
which is the common branch, as a compound with no main-opcode result
(PUTFH+GETATTR, a SEQUENCE-led v4.1 one) opens no tx; that one therefore
carries the procedure the xidmap recorded, not the 0 that EVE reads as
NULL. Either way the record is skipped to its RPC boundary, never
buffered toward the claimed length.
Ticket: #9265.
1 week ago
|
|
|
"RPCSEC_GSS: integrity envelope could not be unwrapped (v4 compound call)"
|
|
|
|
|
);
|
nfs: record the RPCSEC_GSS integrity unwrap reject
A v4 COMPOUND carried with the GSS integrity service (creds procedure 0 =
RPCSEC_GSS_DATA, service 2) is wrapped in an envelope: length of the
wrapped body (sequence number and arguments), sequence number, arguments,
checksum (RFC 2203 5.3.2.2). When that cannot be unwrapped the compound
parser is never reached, and the arm ended with set_event(MalformedData).
set_event stamps the newest transaction of the state, and returns without
doing anything when the state has none. Two consequences: on a flow whose
first RPC is such a call, the reject produced no transaction, no anomaly
and nothing in EVE, so a record no server would ever send went by
unremarked; and on a reply, the event landed on whichever transaction
happened to be last, which on an async mount is a different call than the
one being rejected, while the tx owning the xid stayed open forever.
Give both arms a transaction to carry the event, like the partial-record
rejects already do. The request side reuses the malformed-record helper
(the xidmap is registered by the caller, so the gss creds survive and the
reply of the xid is still known to be wrapped). The reply side marks the
tx owning the xid, and falls back to a new one only when there is none -
which is the common branch, as a compound with no main-opcode result
(PUTFH+GETATTR, a SEQUENCE-led v4.1 one) opens no tx; that one therefore
carries the procedure the xidmap recorded, not the 0 that EVE reads as
NULL. Either way the record is skipped to its RPC boundary, never
buffered toward the claimed length.
Ticket: #9265.
1 week ago
|
|
|
// Keep the gss creds in the map: the reply of this
|
|
|
|
|
// xid is wrapped the same way and must be unwrapped.
|
|
|
|
|
self.requestmap.put(r.hdr.xid, xidmap);
|
|
|
|
|
self.reject_v4_malformed_request_record(r);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match parse_nfs4_request_compound(data) {
|
|
|
|
|
Ok((_, rd)) => {
|
|
|
|
|
SCLogDebug!("NFSPROC4_COMPOUND: {:?}", rd);
|
|
|
|
|
self.compound_request(r, &rd, &mut xidmap);
|
|
|
|
|
}
|
|
|
|
|
Err(Err::Incomplete(_n)) => {
|
|
|
|
|
SCLogDebug!("NFSPROC4_COMPOUND: INCOMPLETE {:?}", _n);
|
nfs: log a v4 COMPOUND the record parser cannot read
The error arm of a COMPOUND that is fully in hand only set a state event.
set_event marks the newest transaction, and there is none when the
record's first op is the one that fails: the record was then consumed,
nothing was logged, and nothing asked for the missing bytes. A v4 WRITE
whose count runs past the end of its record is that case - the parser
gives up at the data, so the size check in the WRITE handling, which
needs a parsed count, is never reached.
Reject what cannot be walked the way a partial record is rejected: scan
the bytes the parser was given, and log an oversized WRITE as a
write_request_too_large file transaction or another un-walkable record as
a malformed one, on a transaction of its own. What the parser skips is
not in that set: a defined op this parser does not implement keeps base's
event on the newest tx, and an RPCSEC_GSS privacy compound, whose result
is opaque by RFC 2203 on the call and the reply alike, is left unparsed
with no event at all. Either way the record is bounded by its own marker,
and a flow that carries un-parseable v4 traffic leaves a trace in EVE.
Ticket: #9265.
1 week ago
|
|
|
self.reject_failed_v4_compound_parse(r, data);
|
|
|
|
|
}
|
|
|
|
|
Err(Err::Error(ref e)) if e.code == ErrorKind::Tag => {
|
|
|
|
|
// a defined v4 op this parser does not implement: the compound is
|
|
|
|
|
// valid, just not parsed. No tx of its own: keeps base's event on
|
|
|
|
|
// the newest tx, so a lock-only flow logs nothing when it has no
|
|
|
|
|
// earlier tx.
|
|
|
|
|
SCLogDebug!("NFSPROC4_COMPOUND: unsupported op {:?}", e);
|
|
|
|
|
self.set_event(NFSEvent::MalformedData);
|
|
|
|
|
}
|
|
|
|
|
Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
|
|
|
|
|
SCLogDebug!("NFSPROC4_COMPOUND: Parsing failed: {:?}", _e);
|
nfs: log a v4 COMPOUND the record parser cannot read
The error arm of a COMPOUND that is fully in hand only set a state event.
set_event marks the newest transaction, and there is none when the
record's first op is the one that fails: the record was then consumed,
nothing was logged, and nothing asked for the missing bytes. A v4 WRITE
whose count runs past the end of its record is that case - the parser
gives up at the data, so the size check in the WRITE handling, which
needs a parsed count, is never reached.
Reject what cannot be walked the way a partial record is rejected: scan
the bytes the parser was given, and log an oversized WRITE as a
write_request_too_large file transaction or another un-walkable record as
a malformed one, on a transaction of its own. What the parser skips is
not in that set: a defined op this parser does not implement keeps base's
event on the newest tx, and an RPCSEC_GSS privacy compound, whose result
is opaque by RFC 2203 on the call and the reply alike, is left unparsed
with no event at all. Either way the record is bounded by its own marker,
and a flow that carries un-parseable v4 traffic leaves a trace in EVE.
Ticket: #9265.
1 week ago
|
|
|
self.reject_failed_v4_compound_parse(r, data);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.requestmap.put(r.hdr.xid, xidmap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn compound_response<'b>(
|
|
|
|
|
&mut self, flow: *mut Flow, r: &RpcReplyPacket<'b>, cr: &Nfs4ResponseCompoundRecord<'b>,
|
|
|
|
|
xidmap: &mut NFSRequestXidMap,
|
|
|
|
|
) {
|
|
|
|
|
let mut insert_filename_with_getfh = false;
|
|
|
|
|
let mut main_opcode_status: u32 = 0;
|
|
|
|
|
let mut main_opcode_status_set: bool = false;
|
|
|
|
|
|
|
|
|
|
for c in &cr.commands {
|
|
|
|
|
SCLogDebug!("c {:?}", c);
|
|
|
|
|
match *c {
|
|
|
|
|
Nfs4ResponseContent::ReadDir(_s, Some(ref rd)) => {
|
|
|
|
|
SCLogDebug!("READDIRv4: status {} eof {}", _s, rd.eof);
|
|
|
|
|
|
|
|
|
|
#[allow(clippy::manual_flatten)]
|
|
|
|
|
for d in &rd.listing {
|
|
|
|
|
if let Some(_d) = d {
|
|
|
|
|
SCLogDebug!("READDIRv4: dir {}", String::from_utf8_lossy(_d.name));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Nfs4ResponseContent::Remove(s) => {
|
|
|
|
|
SCLogDebug!("REMOVE4: status {}", s);
|
|
|
|
|
main_opcode_status = s;
|
|
|
|
|
main_opcode_status_set = true;
|
|
|
|
|
}
|
|
|
|
|
Nfs4ResponseContent::Create(s) => {
|
|
|
|
|
SCLogDebug!("CREATE4: status {}", s);
|
|
|
|
|
main_opcode_status = s;
|
|
|
|
|
main_opcode_status_set = true;
|
|
|
|
|
}
|
|
|
|
|
Nfs4ResponseContent::Read(s, Some(ref rd)) => {
|
|
|
|
|
SCLogDebug!(
|
|
|
|
|
"READ4: xidmap {:?} status {} data {}",
|
|
|
|
|
xidmap,
|
|
|
|
|
s,
|
|
|
|
|
rd.data.len()
|
|
|
|
|
);
|
|
|
|
|
// convert record to generic read reply
|
|
|
|
|
let reply = NfsReplyRead {
|
|
|
|
|
status: s,
|
|
|
|
|
attr_follows: 0,
|
|
|
|
|
attr_blob: &[],
|
|
|
|
|
count: rd.count,
|
|
|
|
|
eof: rd.eof,
|
|
|
|
|
data_len: rd.data.len() as u32,
|
|
|
|
|
data: rd.data,
|
|
|
|
|
};
|
nfs: check a v3 WRITE and READ claim against the record
The args of a WRITE request and of a READ reply were trusted to fit the
record that carries them. Where the claim fell below the extent the parse
covers, the file data ran past the record boundary into the next RPC,
which was then read as a record. A max-tx of 0 was accepted and evicted
every transaction on insert.
Compare the claim against that extent: for a WRITE the handle, offset,
count, stable flag and its data, for a READ the status, the optional
attributes, the count, the eof flag and its data. A record whose claim is
below the extent gets a transaction with the malformed data event and a
skip to the record boundary, as an oversized claim already does. Only the
v3 records arrive in pieces, so process_read_record now learns which
reply it is given and the check is v3's. Reject a max-tx of 0 at config
load.
Ticket: #9263.
1 week ago
|
|
|
self.process_read_record(flow, r, &reply, Some(xidmap), false);
|
|
|
|
|
}
|
|
|
|
|
Nfs4ResponseContent::Open(_s, Some(ref _rd)) => {
|
|
|
|
|
SCLogDebug!("OPENv4: status {} opendata {:?}", _s, _rd);
|
|
|
|
|
insert_filename_with_getfh = true;
|
|
|
|
|
}
|
|
|
|
|
Nfs4ResponseContent::GetFH(_s, Some(ref rd)) if insert_filename_with_getfh => {
|
|
|
|
|
self.namemap
|
|
|
|
|
.put(rd.value.to_vec(), xidmap.file_name.to_vec());
|
|
|
|
|
}
|
|
|
|
|
Nfs4ResponseContent::PutRootFH(s)
|
|
|
|
|
if s == NFS4_OK && xidmap.file_name.is_empty() =>
|
|
|
|
|
{
|
|
|
|
|
xidmap.file_name = b"<mount_root>".to_vec();
|
|
|
|
|
SCLogDebug!("filename {:?}", xidmap.file_name);
|
|
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if main_opcode_status_set {
|
|
|
|
|
let resp_handle = Vec::new();
|
|
|
|
|
self.mark_response_tx_done(
|
|
|
|
|
flow,
|
|
|
|
|
r.hdr.xid,
|
|
|
|
|
r.reply_state,
|
|
|
|
|
main_opcode_status,
|
|
|
|
|
&resp_handle,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn process_reply_record_v4(
|
|
|
|
|
&mut self, flow: *mut Flow, r: &RpcReplyPacket, xidmap: &mut NFSRequestXidMap,
|
|
|
|
|
) {
|
|
|
|
|
if xidmap.procedure == NFSPROC4_COMPOUND {
|
|
|
|
|
let mut data = r.prog_data;
|
|
|
|
|
|
nfs: log a v4 COMPOUND the record parser cannot read
The error arm of a COMPOUND that is fully in hand only set a state event.
set_event marks the newest transaction, and there is none when the
record's first op is the one that fails: the record was then consumed,
nothing was logged, and nothing asked for the missing bytes. A v4 WRITE
whose count runs past the end of its record is that case - the parser
gives up at the data, so the size check in the WRITE handling, which
needs a parsed count, is never reached.
Reject what cannot be walked the way a partial record is rejected: scan
the bytes the parser was given, and log an oversized WRITE as a
write_request_too_large file transaction or another un-walkable record as
a malformed one, on a transaction of its own. What the parser skips is
not in that set: a defined op this parser does not implement keeps base's
event on the newest tx, and an RPCSEC_GSS privacy compound, whose result
is opaque by RFC 2203 on the call and the reply alike, is left unparsed
with no event at all. Either way the record is bounded by its own marker,
and a flow that carries un-parseable v4 traffic leaves a trace in EVE.
Ticket: #9265.
1 week ago
|
|
|
// privacy (0, 3) carries an encrypted response: not parsed,
|
|
|
|
|
// and not malformed (the call logged no tx either)
|
|
|
|
|
if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 3 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if xidmap.gssapi_proc == 0 && xidmap.gssapi_service == 2 {
|
|
|
|
|
SCLogDebug!("GSS INTEGRITY as set by call: {:?}", xidmap);
|
|
|
|
|
match parse_rpc_gssapi_integrity(r.prog_data) {
|
|
|
|
|
Ok((_rem, rec)) => {
|
|
|
|
|
SCLogDebug!("GSS INTEGRITY wrapper: {:?}", rec);
|
|
|
|
|
data = rec.data;
|
|
|
|
|
}
|
nfs: record the RPCSEC_GSS integrity unwrap reject
A v4 COMPOUND carried with the GSS integrity service (creds procedure 0 =
RPCSEC_GSS_DATA, service 2) is wrapped in an envelope: length of the
wrapped body (sequence number and arguments), sequence number, arguments,
checksum (RFC 2203 5.3.2.2). When that cannot be unwrapped the compound
parser is never reached, and the arm ended with set_event(MalformedData).
set_event stamps the newest transaction of the state, and returns without
doing anything when the state has none. Two consequences: on a flow whose
first RPC is such a call, the reject produced no transaction, no anomaly
and nothing in EVE, so a record no server would ever send went by
unremarked; and on a reply, the event landed on whichever transaction
happened to be last, which on an async mount is a different call than the
one being rejected, while the tx owning the xid stayed open forever.
Give both arms a transaction to carry the event, like the partial-record
rejects already do. The request side reuses the malformed-record helper
(the xidmap is registered by the caller, so the gss creds survive and the
reply of the xid is still known to be wrapped). The reply side marks the
tx owning the xid, and falls back to a new one only when there is none -
which is the common branch, as a compound with no main-opcode result
(PUTFH+GETATTR, a SEQUENCE-led v4.1 one) opens no tx; that one therefore
carries the procedure the xidmap recorded, not the 0 that EVE reads as
NULL. Either way the record is skipped to its RPC boundary, never
buffered toward the claimed length.
Ticket: #9265.
1 week ago
|
|
|
Err(Err::Incomplete(_)) | Err(Err::Error(_)) | Err(Err::Failure(_)) => {
|
|
|
|
|
SCLogDebug!(
|
|
|
|
|
"RPCSEC_GSS: integrity envelope could not be unwrapped (v4 compound reply)"
|
|
|
|
|
);
|
|
|
|
|
self.reject_v4_gss_reply_unwrap(r, xidmap.procedure);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
match parse_nfs4_response_compound(data) {
|
|
|
|
|
Ok((_, rd)) => {
|
|
|
|
|
SCLogDebug!("COMPOUNDv4: {:?}", rd);
|
|
|
|
|
self.compound_response(flow, r, &rd, xidmap);
|
|
|
|
|
}
|
|
|
|
|
Err(Err::Incomplete(_)) => {
|
|
|
|
|
self.set_event(NFSEvent::MalformedData);
|
|
|
|
|
}
|
|
|
|
|
Err(Err::Error(_e)) | Err(Err::Failure(_e)) => {
|
|
|
|
|
SCLogDebug!("Parsing failed: {:?}", _e);
|
|
|
|
|
self.set_event(NFSEvent::MalformedData);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|