ftp: strip trailing whitespace before allocating
CopyCommandLine allocated a buffer for the whole request line, stripped
the trailing whitespace and returned the shorter length. That shorter
length is what the caller stores in tx->request_length and what
FTPTransactionFree later hands to FTPFree, so one byte for every stripped
whitespace character is never subtracted from ftp_memuse. The memory
itself is released -- SCFree ignores the size it is given -- but the
counter keeps climbing for the life of the process, and a client that
pads its commands with spaces can push it up with every command it sends.
Once the counter reaches app-layer.protocols.ftp.memcap, FTPCheckMemcap
refuses any allocation that would cross it, and request buffers are the
first to be refused: the transaction is still created, but tx->request
stays NULL, so command_data is missing from the log and rules that
inspect it stop matching. Whether a new flow can still allocate its state
depends on how much room is left below the cap. With no memcap
configured, the symptom is an ftp.memuse counter that is permanently
wrong.
Strip the whitespace before allocating, so the size accounted for is the
size returned to the caller:
- CopyCommandLine shortens line->len first, then allocates and copies
that length.
- A line that is nothing but whitespace returns without allocating. It
used to get a buffer that was stored on the transaction and freed with
a length of zero, leaving the whole allocation on the counter.
- When the allocation fails, line->len has already been shortened, so
port_line, the transfer file name and the too-long-line check see the
same length there that they see when it succeeds.
- FTPParserTest13 covers the accounting and the stored request;
FTPParserTest14 covers the line the parser uses afterwards.
Issue: 8848
2 months ago
|
|
|
/* Copyright (C) 2007-2026 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 Pablo Rincon Crespo <pablo.rincon.crespo@gmail.com>
|
|
|
|
|
* \author Eric Leblond <eric@regit.org>
|
|
|
|
|
* \author Jeff Lucovsky <jlucovsky@oisf.net>
|
|
|
|
|
*
|
|
|
|
|
* App Layer Parser for FTP
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "suricata-common.h"
|
|
|
|
|
|
|
|
|
|
#include "app-layer-ftp.h"
|
|
|
|
|
#include "app-layer-parser.h"
|
|
|
|
|
#include "app-layer-expectation.h"
|
|
|
|
|
#include "app-layer-detect-proto.h"
|
|
|
|
|
#include "app-layer-events.h"
|
|
|
|
|
|
|
|
|
|
#include "rust.h"
|
|
|
|
|
|
|
|
|
|
#include "util-mpm.h"
|
|
|
|
|
#include "util-memcmp.h"
|
|
|
|
|
#include "util-validate.h"
|
|
|
|
|
#include "app-layer-protos.h"
|
|
|
|
|
#include "flow-storage.h"
|
|
|
|
|
#include "flow.h"
|
|
|
|
|
#include "util-debug.h"
|
|
|
|
|
#include "util-error.h"
|
|
|
|
|
#include "util-prefilter.h"
|
|
|
|
|
#include "util-streaming-buffer.h"
|
|
|
|
|
|
|
|
|
|
typedef struct FTPThreadCtx_ {
|
|
|
|
|
MpmThreadCtx *ftp_mpm_thread_ctx;
|
|
|
|
|
PrefilterRuleStore *pmq;
|
|
|
|
|
} FTPThreadCtx;
|
|
|
|
|
|
|
|
|
|
#define FTP_MPM mpm_default_matcher
|
|
|
|
|
|
|
|
|
|
static MpmCtx *ftp_mpm_ctx = NULL;
|
|
|
|
|
|
|
|
|
|
uint64_t ftp_config_memcap = 0;
|
|
|
|
|
uint32_t ftp_config_maxtx = 1024;
|
|
|
|
|
uint32_t ftp_max_line_len = 4096;
|
|
|
|
|
|
|
|
|
|
SC_ATOMIC_DECLARE(uint64_t, ftp_memuse);
|
|
|
|
|
SC_ATOMIC_DECLARE(uint64_t, ftp_memcap);
|
|
|
|
|
|
|
|
|
|
static FTPTransaction *FTPGetOldestTx(const FtpState *, FTPTransaction *);
|
|
|
|
|
|
|
|
|
|
static void FTPParseMemcap(void)
|
|
|
|
|
{
|
|
|
|
|
SCFTPGetConfigValues(&ftp_config_memcap, &ftp_config_maxtx, &ftp_max_line_len);
|
|
|
|
|
|
|
|
|
|
SC_ATOMIC_INIT(ftp_memuse);
|
|
|
|
|
SC_ATOMIC_INIT(ftp_memcap);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPIncrMemuse(uint64_t size)
|
|
|
|
|
{
|
|
|
|
|
(void)SC_ATOMIC_ADD(ftp_memuse, size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPDecrMemuse(uint64_t size)
|
|
|
|
|
{
|
|
|
|
|
(void)SC_ATOMIC_SUB(ftp_memuse, size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint64_t FTPMemuseGlobalCounter(void)
|
|
|
|
|
{
|
|
|
|
|
uint64_t tmpval = SC_ATOMIC_GET(ftp_memuse);
|
|
|
|
|
return tmpval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
uint64_t FTPMemcapGlobalCounter(void)
|
|
|
|
|
{
|
|
|
|
|
uint64_t tmpval = SC_ATOMIC_GET(ftp_memcap);
|
|
|
|
|
return tmpval;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int FTPSetMemcap(uint64_t size)
|
|
|
|
|
{
|
|
|
|
|
if ((uint64_t)SC_ATOMIC_GET(ftp_memcap) < size) {
|
|
|
|
|
SC_ATOMIC_SET(ftp_memcap, size);
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief Check if alloc'ing "size" would mean we're over memcap
|
|
|
|
|
*
|
|
|
|
|
* \retval 1 if in bounds
|
|
|
|
|
* \retval 0 if not in bounds
|
|
|
|
|
*/
|
|
|
|
|
static int FTPCheckMemcap(uint64_t size)
|
|
|
|
|
{
|
|
|
|
|
if (ftp_config_memcap == 0 || size + SC_ATOMIC_GET(ftp_memuse) <= ftp_config_memcap)
|
|
|
|
|
return 1;
|
|
|
|
|
(void) SC_ATOMIC_ADD(ftp_memcap, 1);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void *FTPCalloc(size_t n, size_t size)
|
|
|
|
|
{
|
|
|
|
|
if (FTPCheckMemcap((uint32_t)(n * size)) == 0) {
|
|
|
|
|
sc_errno = SC_ELIMIT;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *ptr = SCCalloc(n, size);
|
|
|
|
|
|
|
|
|
|
if (unlikely(ptr == NULL)) {
|
|
|
|
|
sc_errno = SC_ENOMEM;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FTPIncrMemuse((uint64_t)(n * size));
|
|
|
|
|
return ptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void *FTPRealloc(void *ptr, size_t orig_size, size_t size)
|
|
|
|
|
{
|
|
|
|
|
if (FTPCheckMemcap((uint32_t)(size - orig_size)) == 0) {
|
|
|
|
|
sc_errno = SC_ELIMIT;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void *rptr = SCRealloc(ptr, size);
|
|
|
|
|
if (rptr == NULL) {
|
|
|
|
|
sc_errno = SC_ENOMEM;
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (size > orig_size) {
|
|
|
|
|
FTPIncrMemuse(size - orig_size);
|
|
|
|
|
} else {
|
|
|
|
|
FTPDecrMemuse(orig_size - size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return rptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPFree(void *ptr, size_t size)
|
|
|
|
|
{
|
|
|
|
|
SCFree(ptr);
|
|
|
|
|
|
|
|
|
|
FTPDecrMemuse((uint64_t)size);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static FTPResponseWrapper *FTPResponseWrapperAlloc(FTPResponseLine *response)
|
|
|
|
|
{
|
|
|
|
|
FTPResponseWrapper *wrapper = FTPCalloc(1, sizeof(FTPResponseWrapper));
|
|
|
|
|
if (likely(wrapper)) {
|
|
|
|
|
FTPIncrMemuse(response->total_size);
|
|
|
|
|
wrapper->response = response;
|
|
|
|
|
}
|
|
|
|
|
return wrapper;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPResponseWrapperFree(FTPResponseWrapper *wrapper)
|
|
|
|
|
{
|
|
|
|
|
if (wrapper->response) {
|
|
|
|
|
FTPDecrMemuse(wrapper->response->total_size);
|
|
|
|
|
SCFTPFreeResponseLine(wrapper->response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FTPFree(wrapper, sizeof(FTPResponseWrapper));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void *FTPLocalStorageAlloc(void)
|
|
|
|
|
{
|
|
|
|
|
/* needed by the mpm */
|
|
|
|
|
FTPThreadCtx *td = SCCalloc(1, sizeof(*td));
|
|
|
|
|
if (td == NULL) {
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
td->pmq = SCCalloc(1, sizeof(*td->pmq));
|
|
|
|
|
if (td->pmq == NULL) {
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
PmqSetup(td->pmq);
|
|
|
|
|
|
|
|
|
|
td->ftp_mpm_thread_ctx = SCCalloc(1, sizeof(MpmThreadCtx));
|
|
|
|
|
if (unlikely(td->ftp_mpm_thread_ctx == NULL)) {
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
MpmInitThreadCtx(td->ftp_mpm_thread_ctx, ftp_mpm_ctx, FTP_MPM);
|
|
|
|
|
return td;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPLocalStorageFree(void *ptr)
|
|
|
|
|
{
|
|
|
|
|
FTPThreadCtx *td = ptr;
|
|
|
|
|
if (td != NULL) {
|
|
|
|
|
if (td->pmq != NULL) {
|
|
|
|
|
PmqFree(td->pmq);
|
|
|
|
|
SCFree(td->pmq);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (td->ftp_mpm_thread_ctx != NULL) {
|
|
|
|
|
MpmDestroyThreadCtx(td->ftp_mpm_thread_ctx, FTP_MPM);
|
|
|
|
|
SCFree(td->ftp_mpm_thread_ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SCFree(td);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
static FTPTransaction *FTPTransactionCreate(FtpState *state)
|
|
|
|
|
{
|
|
|
|
|
SCEnter();
|
|
|
|
|
FTPTransaction *firsttx = TAILQ_FIRST(&state->tx_list);
|
|
|
|
|
if (firsttx && state->tx_cnt - firsttx->tx_id > ftp_config_maxtx) {
|
|
|
|
|
FTPTransaction *tx_old;
|
|
|
|
|
TAILQ_FOREACH (tx_old, &state->tx_list, next) {
|
|
|
|
|
if (!tx_old->done) {
|
|
|
|
|
tx_old->done = true;
|
|
|
|
|
tx_old->tx_data.updated_ts = true;
|
|
|
|
|
tx_old->tx_data.updated_tc = true;
|
|
|
|
|
SCAppLayerDecoderEventsSetEventRaw(
|
|
|
|
|
&tx_old->tx_data.events, FtpEventTooManyTransactions);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
FTPTransaction *tx = FTPCalloc(1, sizeof(*tx));
|
|
|
|
|
if (tx == NULL) {
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TAILQ_INSERT_TAIL(&state->tx_list, tx, next);
|
|
|
|
|
tx->tx_id = state->tx_cnt++;
|
|
|
|
|
|
|
|
|
|
TAILQ_INIT(&tx->response_list);
|
|
|
|
|
|
|
|
|
|
SCLogDebug("new transaction %p (state tx cnt %"PRIu64")", tx, state->tx_cnt);
|
|
|
|
|
return tx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPTransactionFree(FTPTransaction *tx)
|
|
|
|
|
{
|
|
|
|
|
SCEnter();
|
|
|
|
|
|
|
|
|
|
SCAppLayerTxDataCleanup(&tx->tx_data);
|
|
|
|
|
|
|
|
|
|
if (tx->request) {
|
|
|
|
|
FTPFree(tx->request, tx->request_length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FTPResponseWrapper *wrapper;
|
|
|
|
|
while ((wrapper = TAILQ_FIRST(&tx->response_list))) {
|
|
|
|
|
TAILQ_REMOVE(&tx->response_list, wrapper, next);
|
|
|
|
|
FTPResponseWrapperFree(wrapper);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FTPFree(tx, sizeof(*tx));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
typedef struct FtpInput_ {
|
|
|
|
|
const uint8_t *buf;
|
|
|
|
|
int32_t consumed;
|
|
|
|
|
int32_t len;
|
|
|
|
|
int32_t orig_len;
|
|
|
|
|
} FtpInput;
|
|
|
|
|
|
|
|
|
|
static AppLayerResult FTPGetLineForDirection(
|
|
|
|
|
FtpLineState *line, FtpInput *input, bool *current_line_truncated)
|
|
|
|
|
{
|
|
|
|
|
SCEnter();
|
|
|
|
|
|
ftp: mark truncation on the line, not the connection
The truncation flag lived in FtpState, where the line getter set it and
the two callers cleared it once FtpLineState.lf_found said the line had
ended. The callers reuse one FtpLineState for every line in a slice, and
only the LF branch ever set lf_found, so a complete line left it set for
whatever came after. An over-long line behind a complete one in the same
slice therefore had its truncation state cleared, its remainder was parsed
as ordinary traffic, and a "PASS s3cret" sitting in that remainder became
a transaction of its own.
The flag could just as easily be left set. The request loop reached its
clear only for a line that matched an FTP command, continuing to the next
line otherwise, so an over-long run of "Z" left the flag standing and the
next command to arrive was discarded as that line's remainder. The
response loop's clear sat three ifs deep, inside blocks entered only when
the reply parsed, so a line that SCFTPParseResponseLine rejects -- a run
of spaces is empty once trimmed -- left it standing too.
The getter also raised the flag for a line whose LF it had just consumed.
Nothing of that line remained to discard, so the caller had to clear it
straight back.
Truncation is a property of the line, so FtpLineState now carries it. The
getter sets line.truncated for the line it hands back, and the FtpState
flag stays set only where truncation genuinely spans slices: the over-long
line that arrives without an LF. The callers read line.truncated and no
longer touch the shared state, so lf_found goes away entirely.
Issue: 8859
2 months ago
|
|
|
/* the caller reuses one FtpLineState for every line in a slice */
|
|
|
|
|
line->truncated = false;
|
|
|
|
|
|
ftp: keep parsing after a truncated line's LF
When a command or reply runs past the configured maximum length, the
parser keeps what fits, reports it truncated, and discards the remainder
of that line as it arrives. Only the remainder should go, but the parser
was discarding the whole stream slice that the line's LF landed in.
A slice holding "<remainder>\r\nQUIT\r\n" carries a complete QUIT behind
the remainder, and a response slice carries whatever reply comes next. The
recovery branch zeroed the remaining length and returned an error, which
ended the caller's parse loop, so those lines were discarded along with
the remainder they followed. They never became transactions, so nothing
inspected them and nothing logged them, which means a command can be
hidden by placing it after an over-long one.
Consume through the first LF, clear the truncation state there, and pick
up the next line in the same slice. The scan becomes a loop so the
remainder goes through the ordinary path, including when it is over-long
itself; the run-out-of-input check becomes the loop condition.
Raised in review of the 8859 fix.
Issue: 8859
2 months ago
|
|
|
while (input->len > 0) {
|
|
|
|
|
const uint8_t *lf_idx = memchr(input->buf + input->consumed, 0x0a, input->len);
|
|
|
|
|
|
|
|
|
|
if (lf_idx == NULL) {
|
|
|
|
|
if (!(*current_line_truncated) && (uint32_t)input->len >= ftp_max_line_len) {
|
|
|
|
|
*current_line_truncated = true;
|
ftp: mark truncation on the line, not the connection
The truncation flag lived in FtpState, where the line getter set it and
the two callers cleared it once FtpLineState.lf_found said the line had
ended. The callers reuse one FtpLineState for every line in a slice, and
only the LF branch ever set lf_found, so a complete line left it set for
whatever came after. An over-long line behind a complete one in the same
slice therefore had its truncation state cleared, its remainder was parsed
as ordinary traffic, and a "PASS s3cret" sitting in that remainder became
a transaction of its own.
The flag could just as easily be left set. The request loop reached its
clear only for a line that matched an FTP command, continuing to the next
line otherwise, so an over-long run of "Z" left the flag standing and the
next command to arrive was discarded as that line's remainder. The
response loop's clear sat three ifs deep, inside blocks entered only when
the reply parsed, so a line that SCFTPParseResponseLine rejects -- a run
of spaces is empty once trimmed -- left it standing too.
The getter also raised the flag for a line whose LF it had just consumed.
Nothing of that line remained to discard, so the caller had to clear it
straight back.
Truncation is a property of the line, so FtpLineState now carries it. The
getter sets line.truncated for the line it hands back, and the FtpState
flag stays set only where truncation genuinely spans slices: the over-long
line that arrives without an LF. The callers read line.truncated and no
longer touch the shared state, so lf_found goes away entirely.
Issue: 8859
2 months ago
|
|
|
line->truncated = true;
|
ftp: keep parsing after a truncated line's LF
When a command or reply runs past the configured maximum length, the
parser keeps what fits, reports it truncated, and discards the remainder
of that line as it arrives. Only the remainder should go, but the parser
was discarding the whole stream slice that the line's LF landed in.
A slice holding "<remainder>\r\nQUIT\r\n" carries a complete QUIT behind
the remainder, and a response slice carries whatever reply comes next. The
recovery branch zeroed the remaining length and returned an error, which
ended the caller's parse loop, so those lines were discarded along with
the remainder they followed. They never became transactions, so nothing
inspected them and nothing logged them, which means a command can be
hidden by placing it after an over-long one.
Consume through the first LF, clear the truncation state there, and pick
up the next line in the same slice. The scan becomes a loop so the
remainder goes through the ordinary path, including when it is over-long
itself; the run-out-of-input check becomes the loop condition.
Raised in review of the 8859 fix.
Issue: 8859
2 months ago
|
|
|
line->buf = input->buf + input->consumed;
|
|
|
|
|
line->len = ftp_max_line_len;
|
|
|
|
|
line->delim_len = 0;
|
|
|
|
|
/* No caller reads consumed after this; advance it so the cursor
|
|
|
|
|
* still describes the slice it was handed. */
|
|
|
|
|
input->consumed += input->len;
|
|
|
|
|
input->len = 0;
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
SCReturnStruct(APP_LAYER_INCOMPLETE(input->consumed, input->len + 1));
|
|
|
|
|
}
|
|
|
|
|
|
ftp: keep parsing after a truncated line's LF
When a command or reply runs past the configured maximum length, the
parser keeps what fits, reports it truncated, and discards the remainder
of that line as it arrives. Only the remainder should go, but the parser
was discarding the whole stream slice that the line's LF landed in.
A slice holding "<remainder>\r\nQUIT\r\n" carries a complete QUIT behind
the remainder, and a response slice carries whatever reply comes next. The
recovery branch zeroed the remaining length and returned an error, which
ended the caller's parse loop, so those lines were discarded along with
the remainder they followed. They never became transactions, so nothing
inspected them and nothing logged them, which means a command can be
hidden by placing it after an over-long one.
Consume through the first LF, clear the truncation state there, and pick
up the next line in the same slice. The scan becomes a loop so the
remainder goes through the ordinary path, including when it is over-long
itself; the run-out-of-input check becomes the loop condition.
Raised in review of the 8859 fix.
Issue: 8859
2 months ago
|
|
|
const uint32_t o_consumed = input->consumed;
|
|
|
|
|
input->consumed = (uint32_t)(lf_idx - input->buf + 1);
|
|
|
|
|
const uint32_t line_len = (uint32_t)(input->consumed - o_consumed);
|
|
|
|
|
input->len -= (int32_t)line_len;
|
|
|
|
|
DEBUG_VALIDATE_BUG_ON((input->consumed + input->len) != input->orig_len);
|
|
|
|
|
|
ftp: keep parsing after a truncated line's LF
When a command or reply runs past the configured maximum length, the
parser keeps what fits, reports it truncated, and discards the remainder
of that line as it arrives. Only the remainder should go, but the parser
was discarding the whole stream slice that the line's LF landed in.
A slice holding "<remainder>\r\nQUIT\r\n" carries a complete QUIT behind
the remainder, and a response slice carries whatever reply comes next. The
recovery branch zeroed the remaining length and returned an error, which
ended the caller's parse loop, so those lines were discarded along with
the remainder they followed. They never became transactions, so nothing
inspected them and nothing logged them, which means a command can be
hidden by placing it after an over-long one.
Consume through the first LF, clear the truncation state there, and pick
up the next line in the same slice. The scan becomes a loop so the
remainder goes through the ordinary path, including when it is over-long
itself; the run-out-of-input check becomes the loop condition.
Raised in review of the 8859 fix.
Issue: 8859
2 months ago
|
|
|
if (*current_line_truncated) {
|
|
|
|
|
/* The tail of an over-long line ends at this LF. Discard just that
|
|
|
|
|
* tail -- anything after the LF is a line of its own and still has
|
|
|
|
|
* to be parsed. */
|
|
|
|
|
*current_line_truncated = false;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
ftp: keep parsing after a truncated line's LF
When a command or reply runs past the configured maximum length, the
parser keeps what fits, reports it truncated, and discards the remainder
of that line as it arrives. Only the remainder should go, but the parser
was discarding the whole stream slice that the line's LF landed in.
A slice holding "<remainder>\r\nQUIT\r\n" carries a complete QUIT behind
the remainder, and a response slice carries whatever reply comes next. The
recovery branch zeroed the remaining length and returned an error, which
ended the caller's parse loop, so those lines were discarded along with
the remainder they followed. They never became transactions, so nothing
inspected them and nothing logged them, which means a command can be
hidden by placing it after an over-long one.
Consume through the first LF, clear the truncation state there, and pick
up the next line in the same slice. The scan becomes a loop so the
remainder goes through the ordinary path, including when it is over-long
itself; the run-out-of-input check becomes the loop condition.
Raised in review of the 8859 fix.
Issue: 8859
2 months ago
|
|
|
|
|
|
|
|
line->buf = input->buf + o_consumed;
|
|
|
|
|
line->len = line_len;
|
|
|
|
|
// There could be one chunk of command data that has LF but post the line limit
|
|
|
|
|
// e.g. input_len = 5077
|
|
|
|
|
// lf_idx = 5010
|
|
|
|
|
// max_line_len = 4096
|
|
|
|
|
if (line->len >= ftp_max_line_len) {
|
ftp: mark truncation on the line, not the connection
The truncation flag lived in FtpState, where the line getter set it and
the two callers cleared it once FtpLineState.lf_found said the line had
ended. The callers reuse one FtpLineState for every line in a slice, and
only the LF branch ever set lf_found, so a complete line left it set for
whatever came after. An over-long line behind a complete one in the same
slice therefore had its truncation state cleared, its remainder was parsed
as ordinary traffic, and a "PASS s3cret" sitting in that remainder became
a transaction of its own.
The flag could just as easily be left set. The request loop reached its
clear only for a line that matched an FTP command, continuing to the next
line otherwise, so an over-long run of "Z" left the flag standing and the
next command to arrive was discarded as that line's remainder. The
response loop's clear sat three ifs deep, inside blocks entered only when
the reply parsed, so a line that SCFTPParseResponseLine rejects -- a run
of spaces is empty once trimmed -- left it standing too.
The getter also raised the flag for a line whose LF it had just consumed.
Nothing of that line remained to discard, so the caller had to clear it
straight back.
Truncation is a property of the line, so FtpLineState now carries it. The
getter sets line.truncated for the line it hands back, and the FtpState
flag stays set only where truncation genuinely spans slices: the over-long
line that arrives without an LF. The callers read line.truncated and no
longer touch the shared state, so lf_found goes away entirely.
Issue: 8859
2 months ago
|
|
|
/* This LF ends the line, so nothing carries into the next slice;
|
|
|
|
|
* the line is simply reported clipped to the limit. */
|
|
|
|
|
line->truncated = true;
|
|
|
|
|
line->len = ftp_max_line_len;
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
if (input->consumed >= 2 && input->buf[input->consumed - 2] == 0x0D) {
|
|
|
|
|
line->delim_len = 2;
|
|
|
|
|
line->len -= 2;
|
|
|
|
|
} else {
|
|
|
|
|
line->delim_len = 1;
|
|
|
|
|
line->len -= 1;
|
|
|
|
|
}
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
ftp: keep parsing after a truncated line's LF
When a command or reply runs past the configured maximum length, the
parser keeps what fits, reports it truncated, and discards the remainder
of that line as it arrives. Only the remainder should go, but the parser
was discarding the whole stream slice that the line's LF landed in.
A slice holding "<remainder>\r\nQUIT\r\n" carries a complete QUIT behind
the remainder, and a response slice carries whatever reply comes next. The
recovery branch zeroed the remaining length and returned an error, which
ended the caller's parse loop, so those lines were discarded along with
the remainder they followed. They never became transactions, so nothing
inspected them and nothing logged them, which means a command can be
hidden by placing it after an over-long one.
Consume through the first LF, clear the truncation state there, and pick
up the next line in the same slice. The scan becomes a loop so the
remainder goes through the ordinary path, including when it is over-long
itself; the run-out-of-input check becomes the loop condition.
Raised in review of the 8859 fix.
Issue: 8859
2 months ago
|
|
|
|
|
|
|
|
/* we have run out of input */
|
|
|
|
|
return APP_LAYER_ERROR;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief This function is called to determine and set which command is being
|
|
|
|
|
* transferred to the ftp server
|
|
|
|
|
* \param thread context
|
|
|
|
|
* \param input input line of the command
|
|
|
|
|
* \param len of the command
|
|
|
|
|
* \param cmd_descriptor when the command has been parsed
|
|
|
|
|
*
|
|
|
|
|
* \retval 1 when the command is parsed, 0 otherwise
|
|
|
|
|
*/
|
|
|
|
|
static int FTPParseRequestCommand(
|
|
|
|
|
FTPThreadCtx *td, FtpLineState *line, FtpCommandInfo *cmd_descriptor)
|
|
|
|
|
{
|
|
|
|
|
SCEnter();
|
|
|
|
|
|
|
|
|
|
/* I don't like this pmq reset here. We'll devise a method later, that
|
|
|
|
|
* should make the use of the mpm very efficient */
|
|
|
|
|
PmqReset(td->pmq);
|
|
|
|
|
int mpm_cnt = mpm_table[FTP_MPM].Search(
|
|
|
|
|
ftp_mpm_ctx, td->ftp_mpm_thread_ctx, td->pmq, line->buf, line->len);
|
|
|
|
|
if (mpm_cnt) {
|
|
|
|
|
uint8_t command_code;
|
|
|
|
|
if (SCGetFtpCommandInfo(td->pmq->rule_id_array[0], NULL, &command_code, NULL)) {
|
|
|
|
|
cmd_descriptor->command_code = command_code;
|
|
|
|
|
/* FTP command indices are expressed in Rust as a u8 */
|
|
|
|
|
cmd_descriptor->command_index = (uint8_t)td->pmq->rule_id_array[0];
|
|
|
|
|
SCReturnInt(1);
|
|
|
|
|
} else {
|
|
|
|
|
/* Where is out command? */
|
|
|
|
|
DEBUG_VALIDATE_BUG_ON(1);
|
|
|
|
|
}
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
if (SCLogDebugEnabled()) {
|
|
|
|
|
const char *command_name = NULL;
|
|
|
|
|
(void)SCGetFtpCommandInfo(td->pmq->rule_id_array[0], &command_name, NULL, NULL);
|
|
|
|
|
SCLogDebug("matching FTP command is %s [code: %d, index %d]", command_name,
|
|
|
|
|
command_code, td->pmq->rule_id_array[0]);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmd_descriptor->command_code = FTP_COMMAND_UNKNOWN;
|
|
|
|
|
SCReturnInt(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FtpTransferCmdFree(void *data)
|
|
|
|
|
{
|
|
|
|
|
FtpTransferCmd *cmd = (FtpTransferCmd *)data;
|
|
|
|
|
if (cmd == NULL)
|
|
|
|
|
return;
|
|
|
|
|
if (cmd->file_name) {
|
|
|
|
|
FTPFree((void *)cmd->file_name, cmd->file_len + 1);
|
|
|
|
|
}
|
|
|
|
|
SCFTPTransferCmdFree(cmd);
|
|
|
|
|
FTPDecrMemuse((uint64_t)sizeof(FtpTransferCmd));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint32_t CopyCommandLine(uint8_t **dest, FtpLineState *line)
|
|
|
|
|
{
|
ftp: strip trailing whitespace before allocating
CopyCommandLine allocated a buffer for the whole request line, stripped
the trailing whitespace and returned the shorter length. That shorter
length is what the caller stores in tx->request_length and what
FTPTransactionFree later hands to FTPFree, so one byte for every stripped
whitespace character is never subtracted from ftp_memuse. The memory
itself is released -- SCFree ignores the size it is given -- but the
counter keeps climbing for the life of the process, and a client that
pads its commands with spaces can push it up with every command it sends.
Once the counter reaches app-layer.protocols.ftp.memcap, FTPCheckMemcap
refuses any allocation that would cross it, and request buffers are the
first to be refused: the transaction is still created, but tx->request
stays NULL, so command_data is missing from the log and rules that
inspect it stop matching. Whether a new flow can still allocate its state
depends on how much room is left below the cap. With no memcap
configured, the symptom is an ftp.memuse counter that is permanently
wrong.
Strip the whitespace before allocating, so the size accounted for is the
size returned to the caller:
- CopyCommandLine shortens line->len first, then allocates and copies
that length.
- A line that is nothing but whitespace returns without allocating. It
used to get a buffer that was stored on the transaction and freed with
a length of zero, leaving the whole allocation on the counter.
- When the allocation fails, line->len has already been shortened, so
port_line, the transfer file name and the too-long-line check see the
same length there that they see when it succeeds.
- FTPParserTest13 covers the accounting and the stored request;
FTPParserTest14 covers the line the parser uses afterwards.
Issue: 8848
2 months ago
|
|
|
/* Strip trailing whitespace before allocating, so the length accounted by
|
|
|
|
|
* FTPCalloc matches the length the caller later hands to FTPFree. */
|
|
|
|
|
while (line->len && isspace((unsigned char)line->buf[line->len - 1])) {
|
|
|
|
|
line->len--;
|
|
|
|
|
}
|
|
|
|
|
|
ftp: strip trailing whitespace before allocating
CopyCommandLine allocated a buffer for the whole request line, stripped
the trailing whitespace and returned the shorter length. That shorter
length is what the caller stores in tx->request_length and what
FTPTransactionFree later hands to FTPFree, so one byte for every stripped
whitespace character is never subtracted from ftp_memuse. The memory
itself is released -- SCFree ignores the size it is given -- but the
counter keeps climbing for the life of the process, and a client that
pads its commands with spaces can push it up with every command it sends.
Once the counter reaches app-layer.protocols.ftp.memcap, FTPCheckMemcap
refuses any allocation that would cross it, and request buffers are the
first to be refused: the transaction is still created, but tx->request
stays NULL, so command_data is missing from the log and rules that
inspect it stop matching. Whether a new flow can still allocate its state
depends on how much room is left below the cap. With no memcap
configured, the symptom is an ftp.memuse counter that is permanently
wrong.
Strip the whitespace before allocating, so the size accounted for is the
size returned to the caller:
- CopyCommandLine shortens line->len first, then allocates and copies
that length.
- A line that is nothing but whitespace returns without allocating. It
used to get a buffer that was stored on the transaction and freed with
a length of zero, leaving the whole allocation on the counter.
- When the allocation fails, line->len has already been shortened, so
port_line, the transfer file name and the too-long-line check see the
same length there that they see when it succeeds.
- FTPParserTest13 covers the accounting and the stored request;
FTPParserTest14 covers the line the parser uses afterwards.
Issue: 8848
2 months ago
|
|
|
if (unlikely(line->len == 0)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
ftp: strip trailing whitespace before allocating
CopyCommandLine allocated a buffer for the whole request line, stripped
the trailing whitespace and returned the shorter length. That shorter
length is what the caller stores in tx->request_length and what
FTPTransactionFree later hands to FTPFree, so one byte for every stripped
whitespace character is never subtracted from ftp_memuse. The memory
itself is released -- SCFree ignores the size it is given -- but the
counter keeps climbing for the life of the process, and a client that
pads its commands with spaces can push it up with every command it sends.
Once the counter reaches app-layer.protocols.ftp.memcap, FTPCheckMemcap
refuses any allocation that would cross it, and request buffers are the
first to be refused: the transaction is still created, but tx->request
stays NULL, so command_data is missing from the log and rules that
inspect it stop matching. Whether a new flow can still allocate its state
depends on how much room is left below the cap. With no memcap
configured, the symptom is an ftp.memuse counter that is permanently
wrong.
Strip the whitespace before allocating, so the size accounted for is the
size returned to the caller:
- CopyCommandLine shortens line->len first, then allocates and copies
that length.
- A line that is nothing but whitespace returns without allocating. It
used to get a buffer that was stored on the transaction and freed with
a length of zero, leaving the whole allocation on the counter.
- When the allocation fails, line->len has already been shortened, so
port_line, the transfer file name and the too-long-line check see the
same length there that they see when it succeeds.
- FTPParserTest13 covers the accounting and the stored request;
FTPParserTest14 covers the line the parser uses afterwards.
Issue: 8848
2 months ago
|
|
|
uint8_t *where = FTPCalloc(line->len + 1, sizeof(char));
|
|
|
|
|
if (unlikely(where == NULL)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
ftp: strip trailing whitespace before allocating
CopyCommandLine allocated a buffer for the whole request line, stripped
the trailing whitespace and returned the shorter length. That shorter
length is what the caller stores in tx->request_length and what
FTPTransactionFree later hands to FTPFree, so one byte for every stripped
whitespace character is never subtracted from ftp_memuse. The memory
itself is released -- SCFree ignores the size it is given -- but the
counter keeps climbing for the life of the process, and a client that
pads its commands with spaces can push it up with every command it sends.
Once the counter reaches app-layer.protocols.ftp.memcap, FTPCheckMemcap
refuses any allocation that would cross it, and request buffers are the
first to be refused: the transaction is still created, but tx->request
stays NULL, so command_data is missing from the log and rules that
inspect it stop matching. Whether a new flow can still allocate its state
depends on how much room is left below the cap. With no memcap
configured, the symptom is an ftp.memuse counter that is permanently
wrong.
Strip the whitespace before allocating, so the size accounted for is the
size returned to the caller:
- CopyCommandLine shortens line->len first, then allocates and copies
that length.
- A line that is nothing but whitespace returns without allocating. It
used to get a buffer that was stored on the transaction and freed with
a length of zero, leaving the whole allocation on the counter.
- When the allocation fails, line->len has already been shortened, so
port_line, the transfer file name and the too-long-line check see the
same length there that they see when it succeeds.
- FTPParserTest13 covers the accounting and the stored request;
FTPParserTest14 covers the line the parser uses afterwards.
Issue: 8848
2 months ago
|
|
|
memcpy(where, line->buf, line->len);
|
|
|
|
|
where[line->len] = '\0';
|
|
|
|
|
*dest = where;
|
|
|
|
|
|
|
|
|
|
return line->len + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief This function is called to retrieve a ftp request
|
|
|
|
|
* \param ftp_state the ftp state structure for the parser
|
|
|
|
|
*
|
|
|
|
|
* \retval APP_LAYER_OK when input was process successfully
|
|
|
|
|
* \retval APP_LAYER_ERROR when a unrecoverable error was encountered
|
|
|
|
|
*/
|
|
|
|
|
static AppLayerResult FTPParseRequest(Flow *f, void *ftp_state, AppLayerParserState *pstate,
|
|
|
|
|
StreamSlice stream_slice, void *local_data)
|
|
|
|
|
{
|
|
|
|
|
FTPThreadCtx *thread_data = local_data;
|
|
|
|
|
|
|
|
|
|
SCEnter();
|
|
|
|
|
/* PrintRawDataFp(stdout, input,input_len); */
|
|
|
|
|
|
|
|
|
|
FtpState *state = (FtpState *)ftp_state;
|
Fix realloc error handling
This patch is fixing realloc error handling. In case of a realloc
failure, it free the initial memory and continue existing error
handling.
The patch has been obtained via the following semantic patch and
a bit oh hand editing:
@@
expression x, E;
identifier f;
@@
f(...)
{
+ void *ptmp;
<+...
- x = SCRealloc(x, E);
+ ptmp = SCRealloc(x, E);
... when != x
- if (x == NULL)
+ if (ptmp == NULL)
{
+ SCFree(x);
+ x = NULL;
...
- }
+ } else {
+ x = ptmp;
+ }
...+>
}
@@
expression x, E;
identifier f;
statement ES;
@@
f(...) {
+ void *ptmp;
<+...
- x = SCRealloc(x, E);
+ ptmp = SCRealloc(x, E);
... when != x
- if (x == NULL) ES
+ if (ptmp == NULL) {
+ SCFree(x);
+ x = NULL;
+ ES
+ } else {
+ x = ptmp;
+ }
...+>
}
@@
expression x, E;
identifier f;
@@
f(...)
{
+ void *ptmp;
<+...
- x = SCRealloc(x, E);
+ ptmp = SCRealloc(x, E);
... when != x
- if (unlikely(x == NULL))
+ if (unlikely(ptmp == NULL))
{
+ SCFree(x);
+ x = NULL;
...
- }
+ } else {
+ x = ptmp;
+ }
...+>
}
@@
expression x, E;
identifier f;
statement ES;
@@
f(...) {
+ void *ptmp;
<+...
- x = SCRealloc(x, E);
+ ptmp = SCRealloc(x, E);
... when != x
- if (unlikely(x == NULL)) ES
+ if (unlikely(ptmp == NULL)) {
+ SCFree(x);
+ x = NULL;
+ ES
+ } else {
+ x = ptmp;
+ }
...+>
}
13 years ago
|
|
|
void *ptmp;
|
|
|
|
|
|
|
|
|
|
const uint8_t *input = StreamSliceGetData(&stream_slice);
|
|
|
|
|
uint32_t input_len = StreamSliceGetDataLen(&stream_slice);
|
|
|
|
|
|
|
|
|
|
if (input == NULL && SCAppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS)) {
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
} else if (input == NULL || input_len == 0) {
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FtpInput ftpi = { .buf = input, .len = input_len, .orig_len = input_len, .consumed = 0 };
|
ftp: mark truncation on the line, not the connection
The truncation flag lived in FtpState, where the line getter set it and
the two callers cleared it once FtpLineState.lf_found said the line had
ended. The callers reuse one FtpLineState for every line in a slice, and
only the LF branch ever set lf_found, so a complete line left it set for
whatever came after. An over-long line behind a complete one in the same
slice therefore had its truncation state cleared, its remainder was parsed
as ordinary traffic, and a "PASS s3cret" sitting in that remainder became
a transaction of its own.
The flag could just as easily be left set. The request loop reached its
clear only for a line that matched an FTP command, continuing to the next
line otherwise, so an over-long run of "Z" left the flag standing and the
next command to arrive was discarded as that line's remainder. The
response loop's clear sat three ifs deep, inside blocks entered only when
the reply parsed, so a line that SCFTPParseResponseLine rejects -- a run
of spaces is empty once trimmed -- left it standing too.
The getter also raised the flag for a line whose LF it had just consumed.
Nothing of that line remained to discard, so the caller had to clear it
straight back.
Truncation is a property of the line, so FtpLineState now carries it. The
getter sets line.truncated for the line it hands back, and the FtpState
flag stays set only where truncation genuinely spans slices: the over-long
line that arrives without an LF. The callers read line.truncated and no
longer touch the shared state, so lf_found goes away entirely.
Issue: 8859
2 months ago
|
|
|
FtpLineState line = { .buf = NULL, .len = 0, .delim_len = 0, .truncated = false };
|
|
|
|
|
|
|
|
|
|
uint8_t direction = STREAM_TOSERVER;
|
|
|
|
|
AppLayerResult res;
|
|
|
|
|
while (1) {
|
|
|
|
|
res = FTPGetLineForDirection(&line, &ftpi, &state->current_line_truncated_ts);
|
|
|
|
|
if (res.status == 1) {
|
|
|
|
|
return res;
|
|
|
|
|
} else if (res.status == -1) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FtpCommandInfo cmd_descriptor;
|
|
|
|
|
if (!FTPParseRequestCommand(thread_data, &line, &cmd_descriptor)) {
|
|
|
|
|
state->command = FTP_COMMAND_UNKNOWN;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state->command = cmd_descriptor.command_code;
|
|
|
|
|
FTPTransaction *tx = FTPTransactionCreate(state);
|
|
|
|
|
if (unlikely(tx == NULL))
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
tx->tx_data.updated_ts = true;
|
|
|
|
|
state->curr_tx = tx;
|
|
|
|
|
|
|
|
|
|
tx->command_descriptor = cmd_descriptor;
|
|
|
|
|
tx->request_length = CopyCommandLine(&tx->request, &line);
|
ftp: mark truncation on the line, not the connection
The truncation flag lived in FtpState, where the line getter set it and
the two callers cleared it once FtpLineState.lf_found said the line had
ended. The callers reuse one FtpLineState for every line in a slice, and
only the LF branch ever set lf_found, so a complete line left it set for
whatever came after. An over-long line behind a complete one in the same
slice therefore had its truncation state cleared, its remainder was parsed
as ordinary traffic, and a "PASS s3cret" sitting in that remainder became
a transaction of its own.
The flag could just as easily be left set. The request loop reached its
clear only for a line that matched an FTP command, continuing to the next
line otherwise, so an over-long run of "Z" left the flag standing and the
next command to arrive was discarded as that line's remainder. The
response loop's clear sat three ifs deep, inside blocks entered only when
the reply parsed, so a line that SCFTPParseResponseLine rejects -- a run
of spaces is empty once trimmed -- left it standing too.
The getter also raised the flag for a line whose LF it had just consumed.
Nothing of that line remained to discard, so the caller had to clear it
straight back.
Truncation is a property of the line, so FtpLineState now carries it. The
getter sets line.truncated for the line it hands back, and the FtpState
flag stays set only where truncation genuinely spans slices: the over-long
line that arrives without an LF. The callers read line.truncated and no
longer touch the shared state, so lf_found goes away entirely.
Issue: 8859
2 months ago
|
|
|
tx->request_truncated = line.truncated;
|
|
|
|
|
if (tx->request_truncated) {
|
|
|
|
|
SCAppLayerDecoderEventsSetEventRaw(&tx->tx_data.events, FtpEventRequestCommandTooLong);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* change direction (default to server) so expectation will handle
|
|
|
|
|
* the correct message when expectation will match.
|
|
|
|
|
* For ftp active mode, data connection direction is opposite to
|
|
|
|
|
* control direction.
|
|
|
|
|
*/
|
|
|
|
|
if ((state->active &&
|
|
|
|
|
(state->command == FTP_COMMAND_STOR || state->command == FTP_COMMAND_APPE ||
|
|
|
|
|
state->command == FTP_COMMAND_STOU)) ||
|
|
|
|
|
(!state->active &&
|
|
|
|
|
(state->command == FTP_COMMAND_RETR || state->command == FTP_COMMAND_NLST ||
|
|
|
|
|
state->command == FTP_COMMAND_LIST ||
|
|
|
|
|
state->command == FTP_COMMAND_MLSD))) {
|
|
|
|
|
direction = STREAM_TOCLIENT;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool has_file = false;
|
|
|
|
|
|
|
|
|
|
switch (state->command) {
|
|
|
|
|
case FTP_COMMAND_EPRT:
|
|
|
|
|
// fallthrough
|
|
|
|
|
case FTP_COMMAND_PORT:
|
|
|
|
|
if (line.len + 1 > state->port_line_size) {
|
|
|
|
|
/* Allocate an extra byte for a NULL terminator */
|
|
|
|
|
ptmp = FTPRealloc(state->port_line, state->port_line_size, line.len + 1);
|
|
|
|
|
if (ptmp == NULL) {
|
|
|
|
|
if (state->port_line) {
|
|
|
|
|
FTPFree(state->port_line, state->port_line_size);
|
|
|
|
|
state->port_line = NULL;
|
|
|
|
|
state->port_line_size = 0;
|
|
|
|
|
state->port_line_len = 0;
|
|
|
|
|
}
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
state->port_line = ptmp;
|
|
|
|
|
state->port_line_size = line.len + 1;
|
|
|
|
|
}
|
|
|
|
|
memcpy(state->port_line, line.buf, line.len);
|
|
|
|
|
state->port_line_len = line.len;
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_RETR:
|
|
|
|
|
// fallthrough
|
|
|
|
|
case FTP_COMMAND_STOR:
|
|
|
|
|
// fallthrough
|
|
|
|
|
case FTP_COMMAND_APPE:
|
|
|
|
|
/* Ensure that there is a file name
|
|
|
|
|
* -- need more than 5 chars: cmd [4], space, <filename>
|
|
|
|
|
*/
|
|
|
|
|
if (line.len < 6) {
|
|
|
|
|
SCAppLayerDecoderEventsSetEventRaw(
|
|
|
|
|
&tx->tx_data.events, FtpEventFileWithoutName);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
has_file = true;
|
|
|
|
|
/* fallthrough */
|
|
|
|
|
case FTP_COMMAND_STOU:
|
|
|
|
|
if (line.len >= 6) {
|
|
|
|
|
has_file = true;
|
|
|
|
|
}
|
|
|
|
|
/* fallthrough */
|
|
|
|
|
case FTP_COMMAND_NLST:
|
|
|
|
|
case FTP_COMMAND_LIST:
|
|
|
|
|
case FTP_COMMAND_MLSD: {
|
|
|
|
|
/* Ensure a port has been negotiated. */
|
|
|
|
|
if (state->dyn_port == 0) {
|
|
|
|
|
SCAppLayerDecoderEventsSetEventRaw(&tx->tx_data.events, FtpEventFileBeforePort);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FtpTransferCmd *data = SCFTPTransferCmdNew();
|
|
|
|
|
if (data == NULL)
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
FTPIncrMemuse((uint64_t)(sizeof *data));
|
|
|
|
|
data->cmd = state->command;
|
|
|
|
|
data->flow_id = FlowGetId(f);
|
|
|
|
|
data->direction = direction;
|
|
|
|
|
data->data_free = FtpTransferCmdFree;
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Min size has been checked in FTPParseRequestCommand
|
|
|
|
|
* SC_FILENAME_MAX includes the null
|
|
|
|
|
*/
|
|
|
|
|
if (has_file) {
|
|
|
|
|
uint32_t file_name_len = MIN(SC_FILENAME_MAX - 1, line.len - 5);
|
|
|
|
|
#if SC_FILENAME_MAX > UINT16_MAX
|
|
|
|
|
#error SC_FILENAME_MAX is greater than UINT16_MAX
|
|
|
|
|
#endif
|
|
|
|
|
data->file_name = FTPCalloc(file_name_len + 1, sizeof(char));
|
|
|
|
|
if (data->file_name == NULL) {
|
|
|
|
|
FtpTransferCmdFree(data);
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
data->file_name[file_name_len] = 0;
|
|
|
|
|
data->file_len = (uint16_t)file_name_len;
|
|
|
|
|
memcpy(data->file_name, line.buf + 5, file_name_len);
|
|
|
|
|
} else if (state->command == FTP_COMMAND_STOU) {
|
|
|
|
|
const char default_file_name[] = "<stou>";
|
|
|
|
|
uint32_t file_name_len = sizeof(default_file_name);
|
|
|
|
|
data->file_name = FTPCalloc(file_name_len, sizeof(char));
|
|
|
|
|
if (data->file_name == NULL) {
|
|
|
|
|
FtpTransferCmdFree(data);
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
data->file_name[file_name_len - 1] = 0;
|
|
|
|
|
data->file_len = (uint16_t)file_name_len - 1;
|
|
|
|
|
memcpy(data->file_name, default_file_name, file_name_len);
|
|
|
|
|
}
|
|
|
|
|
int ret = AppLayerExpectationCreate(
|
|
|
|
|
f, direction, 0, state->dyn_port, ALPROTO_FTPDATA, data);
|
|
|
|
|
if (ret == -1) {
|
|
|
|
|
FtpTransferCmdFree(data);
|
|
|
|
|
SCLogDebug("No expectation created.");
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
} else {
|
|
|
|
|
SCLogDebug("Expectation created [direction: %s, dynamic port %" PRIu16 "].",
|
|
|
|
|
state->active ? "to server" : "to client", state->dyn_port);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* reset the dyn port to avoid duplicate */
|
|
|
|
|
state->dyn_port = 0;
|
|
|
|
|
/* reset active/passive indicator */
|
|
|
|
|
state->active = false;
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
SCAppLayerParserTriggerRawStreamInspection(f, STREAM_TOSERVER);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int FTPParsePassiveResponse(FtpState *state, const uint8_t *input, uint32_t input_len)
|
|
|
|
|
{
|
|
|
|
|
uint16_t dyn_port = SCFTPParsePortPasv(input, input_len);
|
|
|
|
|
if (dyn_port == 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
SCLogDebug("FTP passive mode (v4): dynamic port %"PRIu16"", dyn_port);
|
|
|
|
|
state->active = false;
|
|
|
|
|
state->dyn_port = dyn_port;
|
|
|
|
|
state->curr_tx->dyn_port = dyn_port;
|
|
|
|
|
state->curr_tx->active = false;
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int FTPParsePassiveResponseV6(FtpState *state, const uint8_t *input, uint32_t input_len)
|
|
|
|
|
{
|
|
|
|
|
uint16_t dyn_port = SCFTPParsePortEpsv(input, input_len);
|
|
|
|
|
if (dyn_port == 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
SCLogDebug("FTP passive mode (v6): dynamic port %"PRIu16"", dyn_port);
|
|
|
|
|
state->active = false;
|
|
|
|
|
state->dyn_port = dyn_port;
|
|
|
|
|
state->curr_tx->dyn_port = dyn_port;
|
|
|
|
|
state->curr_tx->active = false;
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief Handle preliminary replies -- keep tx open
|
|
|
|
|
* \retval bool True for a positive preliminary reply; false otherwise
|
|
|
|
|
*
|
|
|
|
|
* 1yz Positive Preliminary reply
|
|
|
|
|
*
|
|
|
|
|
* The requested action is being initiated; expect another
|
|
|
|
|
* reply before proceeding with a new command
|
|
|
|
|
*/
|
|
|
|
|
static inline bool FTPIsPPR(const uint8_t *input, uint32_t input_len)
|
|
|
|
|
{
|
|
|
|
|
return input_len >= 4 && isdigit(input[0]) && input[0] == '1' &&
|
|
|
|
|
isdigit(input[1]) && isdigit(input[2]) && isspace(input[3]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief This function is called to retrieve a ftp response
|
|
|
|
|
* \param ftp_state the ftp state structure for the parser
|
|
|
|
|
* \param input input line of the command
|
|
|
|
|
* \param input_len length of the request
|
|
|
|
|
* \param output the resulting output
|
|
|
|
|
*
|
|
|
|
|
* \retval 1 when the command is parsed, 0 otherwise
|
|
|
|
|
*/
|
|
|
|
|
static AppLayerResult FTPParseResponse(Flow *f, void *ftp_state, AppLayerParserState *pstate,
|
|
|
|
|
StreamSlice stream_slice, void *local_data)
|
|
|
|
|
{
|
|
|
|
|
FtpState *state = (FtpState *)ftp_state;
|
|
|
|
|
|
|
|
|
|
const uint8_t *input = StreamSliceGetData(&stream_slice);
|
|
|
|
|
uint32_t input_len = StreamSliceGetDataLen(&stream_slice);
|
|
|
|
|
|
|
|
|
|
if (unlikely(input_len == 0)) {
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
FtpInput ftpi = { .buf = input, .len = input_len, .orig_len = input_len, .consumed = 0 };
|
ftp: mark truncation on the line, not the connection
The truncation flag lived in FtpState, where the line getter set it and
the two callers cleared it once FtpLineState.lf_found said the line had
ended. The callers reuse one FtpLineState for every line in a slice, and
only the LF branch ever set lf_found, so a complete line left it set for
whatever came after. An over-long line behind a complete one in the same
slice therefore had its truncation state cleared, its remainder was parsed
as ordinary traffic, and a "PASS s3cret" sitting in that remainder became
a transaction of its own.
The flag could just as easily be left set. The request loop reached its
clear only for a line that matched an FTP command, continuing to the next
line otherwise, so an over-long run of "Z" left the flag standing and the
next command to arrive was discarded as that line's remainder. The
response loop's clear sat three ifs deep, inside blocks entered only when
the reply parsed, so a line that SCFTPParseResponseLine rejects -- a run
of spaces is empty once trimmed -- left it standing too.
The getter also raised the flag for a line whose LF it had just consumed.
Nothing of that line remained to discard, so the caller had to clear it
straight back.
Truncation is a property of the line, so FtpLineState now carries it. The
getter sets line.truncated for the line it hands back, and the FtpState
flag stays set only where truncation genuinely spans slices: the over-long
line that arrives without an LF. The callers read line.truncated and no
longer touch the shared state, so lf_found goes away entirely.
Issue: 8859
2 months ago
|
|
|
FtpLineState line = { .buf = NULL, .len = 0, .delim_len = 0, .truncated = false };
|
|
|
|
|
|
|
|
|
|
FTPTransaction *lasttx = TAILQ_FIRST(&state->tx_list);
|
|
|
|
|
AppLayerResult res;
|
|
|
|
|
while (1) {
|
|
|
|
|
res = FTPGetLineForDirection(&line, &ftpi, &state->current_line_truncated_tc);
|
|
|
|
|
if (res.status == 1) {
|
|
|
|
|
return res;
|
|
|
|
|
} else if (res.status == -1) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
FTPTransaction *tx = FTPGetOldestTx(state, lasttx);
|
|
|
|
|
if (tx == NULL) {
|
|
|
|
|
tx = FTPTransactionCreate(state);
|
|
|
|
|
if (tx != NULL) {
|
|
|
|
|
/* This is a TC only transaction, skip TS inspection. */
|
|
|
|
|
tx->tx_data.flags |= APP_LAYER_TX_SKIP_INSPECT_TS;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (unlikely(tx == NULL)) {
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
lasttx = tx;
|
|
|
|
|
tx->tx_data.updated_tc = true;
|
|
|
|
|
if (state->command == FTP_COMMAND_UNKNOWN) {
|
|
|
|
|
/* unknown */
|
|
|
|
|
tx->command_descriptor.command_code = FTP_COMMAND_UNKNOWN;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state->curr_tx = tx;
|
ftp: a too-long line drops the rest of the stream slice
Both parse loops stopped as soon as a command or reply reached the
configured maximum length: they advanced the input cursor past the end and
broke out. When that line ended with an LF in the same stream slice it was
already complete, and the bytes behind the LF were ordinary FTP lines that
the break discarded. A segment holding "PORT <long>\r\nQUIT\r\n" lost the
QUIT: it never became a transaction, so inspection and logging never saw
it, and a hidden RETR or PORT never reached AppLayerExpectationCreate,
leaving its data connection unrecognized as ftp-data.
Remove both breaks so the loop continues to the next line. The cursor
assignment goes with them, since nothing reads it once the loop ends, and a
line with no LF still finishes the slice on its own because the getter
zeroes the remaining length.
Removing the breaks is not enough for a line that never reaches the clear.
Truncation was cleared deep inside command and reply handling, which is
skipped for an unrecognized request command, a reply the parser rejects (a
run of spaces, empty once trimmed), or an invalid EPRT/PORT that jumps to
tx_complete. The connection flag then stayed set and the getter dropped the
line behind it as this line's remainder. Snapshot and clear the flag up
front in both loops, for every line whose LF was consumed, so the following
line survives.
Issue: 8884
2 months ago
|
|
|
|
|
|
|
|
uint16_t dyn_port;
|
|
|
|
|
switch (state->command) {
|
|
|
|
|
case FTP_COMMAND_AUTH_TLS:
|
|
|
|
|
if (line.len >= 4 && SCMemcmp("234 ", line.buf, 4) == 0) {
|
|
|
|
|
SCAppLayerRequestProtocolTLSUpgrade(f);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FTP_COMMAND_EPRT:
|
|
|
|
|
dyn_port = SCFTPParsePortEprt(state->port_line, state->port_line_len);
|
|
|
|
|
if (dyn_port == 0) {
|
|
|
|
|
goto tx_complete;
|
|
|
|
|
}
|
|
|
|
|
state->dyn_port = dyn_port;
|
|
|
|
|
state->active = true;
|
|
|
|
|
tx->dyn_port = dyn_port;
|
|
|
|
|
tx->active = true;
|
|
|
|
|
SCLogDebug("FTP active mode (v6): dynamic port %" PRIu16 "", dyn_port);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FTP_COMMAND_PORT:
|
|
|
|
|
dyn_port = SCFTPParsePort(state->port_line, state->port_line_len);
|
|
|
|
|
if (dyn_port == 0) {
|
|
|
|
|
goto tx_complete;
|
|
|
|
|
}
|
|
|
|
|
state->dyn_port = dyn_port;
|
|
|
|
|
state->active = true;
|
|
|
|
|
tx->dyn_port = state->dyn_port;
|
|
|
|
|
tx->active = true;
|
|
|
|
|
SCLogDebug("FTP active mode (v4): dynamic port %" PRIu16 "", dyn_port);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FTP_COMMAND_PASV:
|
|
|
|
|
if (line.len >= 4 && SCMemcmp("227 ", line.buf, 4) == 0) {
|
|
|
|
|
FTPParsePassiveResponse(ftp_state, line.buf, line.len);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FTP_COMMAND_EPSV:
|
|
|
|
|
if (line.len >= 4 && SCMemcmp("229 ", line.buf, 4) == 0) {
|
|
|
|
|
FTPParsePassiveResponseV6(ftp_state, line.buf, line.len);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (likely(line.len)) {
|
|
|
|
|
FTPResponseLine *response = SCFTPParseResponseLine((const char *)line.buf, line.len);
|
|
|
|
|
if (likely(response)) {
|
|
|
|
|
FTPResponseWrapper *wrapper = FTPResponseWrapperAlloc(response);
|
|
|
|
|
if (likely(wrapper)) {
|
ftp: mark truncation on the line, not the connection
The truncation flag lived in FtpState, where the line getter set it and
the two callers cleared it once FtpLineState.lf_found said the line had
ended. The callers reuse one FtpLineState for every line in a slice, and
only the LF branch ever set lf_found, so a complete line left it set for
whatever came after. An over-long line behind a complete one in the same
slice therefore had its truncation state cleared, its remainder was parsed
as ordinary traffic, and a "PASS s3cret" sitting in that remainder became
a transaction of its own.
The flag could just as easily be left set. The request loop reached its
clear only for a line that matched an FTP command, continuing to the next
line otherwise, so an over-long run of "Z" left the flag standing and the
next command to arrive was discarded as that line's remainder. The
response loop's clear sat three ifs deep, inside blocks entered only when
the reply parsed, so a line that SCFTPParseResponseLine rejects -- a run
of spaces is empty once trimmed -- left it standing too.
The getter also raised the flag for a line whose LF it had just consumed.
Nothing of that line remained to discard, so the caller had to clear it
straight back.
Truncation is a property of the line, so FtpLineState now carries it. The
getter sets line.truncated for the line it hands back, and the FtpState
flag stays set only where truncation genuinely spans slices: the over-long
line that arrives without an LF. The callers read line.truncated and no
longer touch the shared state, so lf_found goes away entirely.
Issue: 8859
2 months ago
|
|
|
response->truncated = line.truncated;
|
|
|
|
|
if (response->truncated) {
|
|
|
|
|
SCAppLayerDecoderEventsSetEventRaw(
|
|
|
|
|
&tx->tx_data.events, FtpEventResponseCommandTooLong);
|
|
|
|
|
}
|
|
|
|
|
TAILQ_INSERT_TAIL(&tx->response_list, wrapper, next);
|
|
|
|
|
} else {
|
|
|
|
|
SCFTPFreeResponseLine(response);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
SCLogDebug("unable to parse FTP response line \"%s\"", line.buf);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Handle preliminary replies -- keep tx open */
|
|
|
|
|
if (FTPIsPPR(line.buf, line.len)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
tx_complete:
|
|
|
|
|
tx->done = true;
|
|
|
|
|
SCAppLayerParserTriggerRawStreamInspection(f, STREAM_TOCLIENT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
static SCMutex ftp_state_mem_lock = SCMUTEX_INITIALIZER;
|
|
|
|
|
static uint64_t ftp_state_memuse = 0;
|
|
|
|
|
static uint64_t ftp_state_memcnt = 0;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static void *FTPStateAlloc(void *orig_state, AppProto proto_orig)
|
|
|
|
|
{
|
|
|
|
|
void *s = FTPCalloc(1, sizeof(FtpState));
|
|
|
|
|
if (unlikely(s == NULL))
|
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
|
|
FtpState *ftp_state = (FtpState *) s;
|
|
|
|
|
TAILQ_INIT(&ftp_state->tx_list);
|
|
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
SCMutexLock(&ftp_state_mem_lock);
|
|
|
|
|
ftp_state_memcnt++;
|
|
|
|
|
ftp_state_memuse+=sizeof(FtpState);
|
|
|
|
|
SCMutexUnlock(&ftp_state_mem_lock);
|
|
|
|
|
#endif
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPStateFree(void *s)
|
|
|
|
|
{
|
|
|
|
|
FtpState *fstate = (FtpState *) s;
|
|
|
|
|
if (fstate->port_line != NULL)
|
|
|
|
|
FTPFree(fstate->port_line, fstate->port_line_size);
|
|
|
|
|
|
|
|
|
|
FTPTransaction *tx = NULL;
|
|
|
|
|
while ((tx = TAILQ_FIRST(&fstate->tx_list))) {
|
|
|
|
|
TAILQ_REMOVE(&fstate->tx_list, tx, next);
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
if (SCLogDebugEnabled()) {
|
|
|
|
|
const char *command_name = NULL;
|
|
|
|
|
(void)SCGetFtpCommandInfo(
|
|
|
|
|
tx->command_descriptor.command_index, &command_name, NULL, NULL);
|
|
|
|
|
SCLogDebug("[%s] state %p id %" PRIu64 ", Freeing %d bytes at %p",
|
|
|
|
|
command_name != NULL ? command_name : "n/a", s, tx->tx_id, tx->request_length,
|
|
|
|
|
tx->request);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
FTPTransactionFree(tx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FTPFree(s, sizeof(FtpState));
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
SCMutexLock(&ftp_state_mem_lock);
|
|
|
|
|
ftp_state_memcnt--;
|
|
|
|
|
ftp_state_memuse-=sizeof(FtpState);
|
|
|
|
|
SCMutexUnlock(&ftp_state_mem_lock);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief This function returns the oldest open transaction; if none
|
|
|
|
|
* are open, then the oldest transaction is returned
|
|
|
|
|
* \param ftp_state the ftp state structure for the parser
|
|
|
|
|
* \param starttx the ftp transaction where to start looking
|
|
|
|
|
*
|
|
|
|
|
* \retval transaction pointer when a transaction was found; NULL otherwise.
|
|
|
|
|
*/
|
|
|
|
|
static FTPTransaction *FTPGetOldestTx(const FtpState *ftp_state, FTPTransaction *starttx)
|
|
|
|
|
{
|
|
|
|
|
if (unlikely(!ftp_state)) {
|
|
|
|
|
SCLogDebug("NULL state object; no transactions available");
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
FTPTransaction *tx = starttx;
|
|
|
|
|
FTPTransaction *lasttx = NULL;
|
|
|
|
|
while(tx != NULL) {
|
|
|
|
|
/* Return oldest open tx */
|
|
|
|
|
if (!tx->done) {
|
|
|
|
|
SCLogDebug("Returning tx %p id %"PRIu64, tx, tx->tx_id);
|
|
|
|
|
return tx;
|
|
|
|
|
}
|
|
|
|
|
/* save for the end */
|
|
|
|
|
lasttx = tx;
|
|
|
|
|
tx = TAILQ_NEXT(tx, next);
|
|
|
|
|
}
|
|
|
|
|
/* All tx are closed; return last element */
|
|
|
|
|
if (lasttx)
|
|
|
|
|
SCLogDebug("Returning OLDEST tx %p id %"PRIu64, lasttx, lasttx->tx_id);
|
|
|
|
|
return lasttx;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void *FTPGetTx(void *state, uint64_t tx_id)
|
|
|
|
|
{
|
|
|
|
|
FtpState *ftp_state = (FtpState *)state;
|
|
|
|
|
if (ftp_state) {
|
|
|
|
|
FTPTransaction *tx = NULL;
|
|
|
|
|
|
|
|
|
|
if (ftp_state->curr_tx == NULL)
|
|
|
|
|
return NULL;
|
|
|
|
|
if (ftp_state->curr_tx->tx_id == tx_id)
|
|
|
|
|
return ftp_state->curr_tx;
|
|
|
|
|
|
|
|
|
|
TAILQ_FOREACH(tx, &ftp_state->tx_list, next) {
|
|
|
|
|
if (tx->tx_id == tx_id)
|
|
|
|
|
return tx;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppLayerTxData *FTPGetTxData(void *vtx)
|
|
|
|
|
{
|
|
|
|
|
FTPTransaction *tx = (FTPTransaction *)vtx;
|
|
|
|
|
return &tx->tx_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppLayerStateData *FTPGetStateData(void *vstate)
|
|
|
|
|
{
|
|
|
|
|
FtpState *s = (FtpState *)vstate;
|
|
|
|
|
return &s->state_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPStateTransactionFree(void *state, uint64_t tx_id)
|
|
|
|
|
{
|
|
|
|
|
FtpState *ftp_state = state;
|
|
|
|
|
FTPTransaction *tx = NULL;
|
|
|
|
|
TAILQ_FOREACH(tx, &ftp_state->tx_list, next) {
|
|
|
|
|
if (tx_id < tx->tx_id)
|
|
|
|
|
break;
|
|
|
|
|
else if (tx_id > tx->tx_id)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
if (tx == ftp_state->curr_tx)
|
|
|
|
|
ftp_state->curr_tx = NULL;
|
|
|
|
|
TAILQ_REMOVE(&ftp_state->tx_list, tx, next);
|
|
|
|
|
FTPTransactionFree(tx);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint64_t FTPGetTxCnt(void *state)
|
|
|
|
|
{
|
|
|
|
|
uint64_t cnt = 0;
|
|
|
|
|
FtpState *ftp_state = state;
|
|
|
|
|
if (ftp_state) {
|
|
|
|
|
cnt = ftp_state->tx_cnt;
|
|
|
|
|
}
|
|
|
|
|
SCLogDebug("returning state %p %"PRIu64, state, cnt);
|
|
|
|
|
return cnt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int FTPGetAlstateProgress(void *vtx, uint8_t direction)
|
|
|
|
|
{
|
|
|
|
|
SCLogDebug("tx %p", vtx);
|
|
|
|
|
FTPTransaction *tx = vtx;
|
|
|
|
|
|
|
|
|
|
/* having a tx implies request side is done */
|
|
|
|
|
if (direction == STREAM_TOSERVER) {
|
|
|
|
|
return FTP_STATE_FINISHED;
|
|
|
|
|
}
|
|
|
|
|
if (!tx->done) {
|
|
|
|
|
return FTP_STATE_IN_PROGRESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return FTP_STATE_FINISHED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppProto FTPUserProbingParser(
|
|
|
|
|
const Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir)
|
|
|
|
|
{
|
|
|
|
|
if (f->alproto_tc == ALPROTO_POP3) {
|
|
|
|
|
// POP traffic begins by same "USER" pattern as FTP
|
|
|
|
|
return ALPROTO_FAILED;
|
|
|
|
|
}
|
|
|
|
|
if (f->alproto_tc == ALPROTO_IMAP) {
|
|
|
|
|
// USER may be used as an IMAP tag
|
|
|
|
|
return ALPROTO_FAILED;
|
|
|
|
|
}
|
|
|
|
|
return ALPROTO_FTP;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppProto FTPQuitProbingParser(
|
|
|
|
|
const Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir)
|
|
|
|
|
{
|
|
|
|
|
// another check for minimum length
|
|
|
|
|
if (len < 5) {
|
|
|
|
|
return ALPROTO_UNKNOWN;
|
|
|
|
|
}
|
|
|
|
|
// begins by QUIT
|
|
|
|
|
if (SCMemcmp(input, "QUIT", 4) != 0) {
|
|
|
|
|
return ALPROTO_FAILED;
|
|
|
|
|
}
|
|
|
|
|
return ALPROTO_FTP;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppProto FTPServerProbingParser(
|
|
|
|
|
const Flow *f, uint8_t direction, const uint8_t *input, uint32_t len, uint8_t *rdir)
|
|
|
|
|
{
|
|
|
|
|
// another check for minimum length
|
|
|
|
|
if (len < 5) {
|
|
|
|
|
return ALPROTO_UNKNOWN;
|
|
|
|
|
}
|
|
|
|
|
// begins by 220
|
|
|
|
|
if (input[0] != '2' || input[1] != '2' || input[2] != '0') {
|
|
|
|
|
return ALPROTO_FAILED;
|
|
|
|
|
}
|
|
|
|
|
// followed by space or hypen
|
|
|
|
|
if (input[3] != ' ' && input[3] != '-') {
|
|
|
|
|
return ALPROTO_FAILED;
|
|
|
|
|
}
|
|
|
|
|
if (f->alproto_ts == ALPROTO_FTP || (f->todstbytecnt > 4 && f->alproto_ts == ALPROTO_UNKNOWN)) {
|
|
|
|
|
// only validates FTP if client side was FTP
|
|
|
|
|
// or if client side is unknown despite having received bytes
|
|
|
|
|
if (memchr(input + 4, '\n', len - 4) != NULL) {
|
|
|
|
|
return ALPROTO_FTP;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ALPROTO_UNKNOWN;
|
|
|
|
|
}
|
|
|
|
|
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
static int FTPRegisterPatternsForProtocolDetection(void)
|
|
|
|
|
{
|
|
|
|
|
if (SCAppLayerProtoDetectPMRegisterPatternCI(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_FTP, "220 (", 5, 0, STREAM_TOCLIENT) < 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
if (SCAppLayerProtoDetectPMRegisterPatternCI(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_FTP, "FEAT", 4, 0, STREAM_TOSERVER) < 0) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
if (SCAppLayerProtoDetectPMRegisterPatternCSwPP(IPPROTO_TCP, ALPROTO_FTP, "USER ", 5, 0,
|
|
|
|
|
STREAM_TOSERVER, FTPUserProbingParser, 5, 5) < 0) {
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (SCAppLayerProtoDetectPMRegisterPatternCI(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_FTP, "PORT ", 5, 0, STREAM_TOSERVER) < 0) {
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
// Only check FTP on known ports as the banner has nothing special beyond
|
|
|
|
|
// the response code shared with SMTP.
|
|
|
|
|
if (!SCAppLayerProtoDetectPPParseConfPorts(
|
|
|
|
|
"tcp", IPPROTO_TCP, "ftp", ALPROTO_FTP, 0, 5, NULL, FTPServerProbingParser)) {
|
|
|
|
|
// STREAM_TOSERVER here means use 21 as flow destination port
|
|
|
|
|
// and FTPServerProbingParser is probing parser to client
|
|
|
|
|
SCAppLayerProtoDetectPPRegister(IPPROTO_TCP, "21", ALPROTO_FTP, 0, 5, STREAM_TOSERVER,
|
|
|
|
|
FTPQuitProbingParser, FTPServerProbingParser);
|
|
|
|
|
}
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static StreamingBufferConfig sbcfg = STREAMING_BUFFER_CONFIG_INITIALIZER;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief This function is called to retrieve a ftp request
|
|
|
|
|
* \param ftp_state the ftp state structure for the parser
|
|
|
|
|
* \param output the resulting output
|
|
|
|
|
*
|
|
|
|
|
* \retval 1 when the command is parsed, 0 otherwise
|
|
|
|
|
*/
|
|
|
|
|
static AppLayerResult FTPDataParse(Flow *f, FtpDataState *ftpdata_state,
|
|
|
|
|
AppLayerParserState *pstate, StreamSlice stream_slice, void *local_data, uint8_t direction)
|
|
|
|
|
{
|
|
|
|
|
const uint8_t *input = StreamSliceGetData(&stream_slice);
|
|
|
|
|
uint32_t input_len = StreamSliceGetDataLen(&stream_slice);
|
|
|
|
|
const bool eof = (direction & STREAM_TOSERVER)
|
|
|
|
|
? SCAppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TS) != 0
|
|
|
|
|
: SCAppLayerParserStateIssetFlag(pstate, APP_LAYER_PARSER_EOF_TC) != 0;
|
|
|
|
|
|
|
|
|
|
SCTxDataUpdateFileFlags(&ftpdata_state->tx_data, ftpdata_state->state_data.file_flags);
|
|
|
|
|
if (ftpdata_state->tx_data.file_tx == 0)
|
|
|
|
|
ftpdata_state->tx_data.file_tx = direction & (STREAM_TOSERVER | STREAM_TOCLIENT);
|
|
|
|
|
if (direction & STREAM_TOSERVER) {
|
|
|
|
|
ftpdata_state->tx_data.updated_ts = true;
|
|
|
|
|
} else {
|
|
|
|
|
ftpdata_state->tx_data.updated_tc = true;
|
|
|
|
|
}
|
|
|
|
|
/* we depend on detection engine for file pruning */
|
|
|
|
|
const uint16_t flags = SCFileFlowFlagsToFlags(ftpdata_state->tx_data.file_flags, direction);
|
|
|
|
|
int ret = 0;
|
|
|
|
|
|
|
|
|
|
SCLogDebug("FTP-DATA input_len %u flags %04x dir %d/%s EOF %s", input_len, flags, direction,
|
|
|
|
|
(direction & STREAM_TOSERVER) ? "toserver" : "toclient", eof ? "true" : "false");
|
|
|
|
|
|
|
|
|
|
SCLogDebug("FTP-DATA flags %04x dir %d", flags, direction);
|
|
|
|
|
if (!ftpdata_state->initialized && input_len) {
|
|
|
|
|
FtpTransferCmd *data =
|
|
|
|
|
(FtpTransferCmd *)SCFlowGetStorageById(f, AppLayerExpectationGetFlowId());
|
|
|
|
|
if (data == NULL) {
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* we shouldn't get data in the wrong dir. Don't set things up for this dir */
|
|
|
|
|
if ((direction & data->direction) == 0) {
|
|
|
|
|
// TODO set event for data in wrong direction
|
|
|
|
|
SCLogDebug("input %u not for our direction (%s): %s/%s", input_len,
|
|
|
|
|
(direction & STREAM_TOSERVER) ? "toserver" : "toclient",
|
|
|
|
|
data->cmd == FTP_COMMAND_STOR ? "STOR" : "RETR",
|
|
|
|
|
(data->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data->file_name) {
|
|
|
|
|
ftpdata_state->files = FileContainerAlloc();
|
|
|
|
|
if (ftpdata_state->files == NULL) {
|
|
|
|
|
SCFlowFreeStorageById(f, AppLayerExpectationGetFlowId());
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ftpdata_state->file_name = data->file_name;
|
|
|
|
|
ftpdata_state->file_len = data->file_len;
|
|
|
|
|
data->file_name = NULL;
|
|
|
|
|
data->file_len = 0;
|
|
|
|
|
}
|
|
|
|
|
f->parent_id = data->flow_id;
|
|
|
|
|
ftpdata_state->command = data->cmd;
|
|
|
|
|
switch (data->cmd) {
|
|
|
|
|
case FTP_COMMAND_STOR:
|
|
|
|
|
ftpdata_state->direction = data->direction;
|
|
|
|
|
SCLogDebug("STOR data to %s",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_APPE:
|
|
|
|
|
ftpdata_state->direction = data->direction;
|
|
|
|
|
SCLogDebug("APPE data to %s",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_STOU:
|
|
|
|
|
ftpdata_state->direction = data->direction;
|
|
|
|
|
SCLogDebug("STOU data to %s",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_RETR:
|
|
|
|
|
ftpdata_state->direction = data->direction;
|
|
|
|
|
SCLogDebug("RETR data to %s",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_NLST:
|
|
|
|
|
ftpdata_state->direction = data->direction;
|
|
|
|
|
SCLogDebug("NLST data to %s",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_LIST:
|
|
|
|
|
ftpdata_state->direction = data->direction;
|
|
|
|
|
SCLogDebug("LIST data to %s",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_MLSD:
|
|
|
|
|
ftpdata_state->direction = data->direction;
|
|
|
|
|
SCLogDebug("MLSD data to %s",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ftpdata_state->file_name) {
|
|
|
|
|
/* open with fixed track_id 0 as we can have just one
|
|
|
|
|
* file per ftp-data flow. */
|
|
|
|
|
if (FileOpenFileWithId(ftpdata_state->files, &sbcfg, 0ULL,
|
|
|
|
|
(uint8_t *)ftpdata_state->file_name, ftpdata_state->file_len, input,
|
|
|
|
|
input_len, flags) != 0) {
|
|
|
|
|
SCLogDebug("Can't open file");
|
|
|
|
|
ret = -1;
|
|
|
|
|
}
|
|
|
|
|
ftpdata_state->tx_data.files_opened = 1;
|
|
|
|
|
}
|
|
|
|
|
SCFlowFreeStorageById(f, AppLayerExpectationGetFlowId());
|
|
|
|
|
|
|
|
|
|
ftpdata_state->initialized = true;
|
|
|
|
|
} else {
|
|
|
|
|
if ((direction & ftpdata_state->direction) == 0) {
|
|
|
|
|
if (input_len) {
|
|
|
|
|
// TODO set event for data in wrong direction
|
|
|
|
|
}
|
|
|
|
|
SCLogDebug("input %u not for us (%s): %s/%s", input_len,
|
|
|
|
|
(direction & STREAM_TOSERVER) ? "toserver" : "toclient",
|
|
|
|
|
ftpdata_state->command == FTP_COMMAND_STOR ? "STOR" : "RETR",
|
|
|
|
|
(ftpdata_state->direction & STREAM_TOSERVER) ? "toserver" : "toclient");
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
if (ftpdata_state->state == FTPDATA_STATE_FINISHED) {
|
|
|
|
|
SCLogDebug("state is already finished");
|
|
|
|
|
DEBUG_VALIDATE_BUG_ON(input_len); // data after state finished is a bug.
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
if (ftpdata_state->file_name && input_len != 0) {
|
|
|
|
|
ret = FileAppendData(ftpdata_state->files, &sbcfg, input, input_len);
|
|
|
|
|
if (ret == -2) {
|
|
|
|
|
ret = 0;
|
|
|
|
|
SCLogDebug("FileAppendData() - file no longer being extracted");
|
|
|
|
|
goto out;
|
|
|
|
|
} else if (ret < 0) {
|
|
|
|
|
SCLogDebug("FileAppendData() failed: %d", ret);
|
|
|
|
|
ret = -2;
|
|
|
|
|
goto out;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DEBUG_VALIDATE_BUG_ON((direction & ftpdata_state->direction) == 0); // should be unreachable
|
|
|
|
|
if (eof) {
|
|
|
|
|
if (ftpdata_state->file_name) {
|
|
|
|
|
ret = FileCloseFile(ftpdata_state->files, &sbcfg, NULL, 0, flags);
|
|
|
|
|
}
|
|
|
|
|
ftpdata_state->state = FTPDATA_STATE_FINISHED;
|
|
|
|
|
SCLogDebug("closed because of eof: state now FTPDATA_STATE_FINISHED");
|
|
|
|
|
}
|
|
|
|
|
out:
|
|
|
|
|
if (ret < 0) {
|
|
|
|
|
SCReturnStruct(APP_LAYER_ERROR);
|
|
|
|
|
}
|
|
|
|
|
SCReturnStruct(APP_LAYER_OK);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppLayerResult FTPDataParseRequest(Flow *f, void *ftp_state, AppLayerParserState *pstate,
|
|
|
|
|
StreamSlice stream_slice, void *local_data)
|
|
|
|
|
{
|
|
|
|
|
return FTPDataParse(f, ftp_state, pstate, stream_slice, local_data, STREAM_TOSERVER);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppLayerResult FTPDataParseResponse(Flow *f, void *ftp_state, AppLayerParserState *pstate,
|
|
|
|
|
StreamSlice stream_slice, void *local_data)
|
|
|
|
|
{
|
|
|
|
|
return FTPDataParse(f, ftp_state, pstate, stream_slice, local_data, STREAM_TOCLIENT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
static SCMutex ftpdata_state_mem_lock = SCMUTEX_INITIALIZER;
|
|
|
|
|
static uint64_t ftpdata_state_memuse = 0;
|
|
|
|
|
static uint64_t ftpdata_state_memcnt = 0;
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
static void *FTPDataStateAlloc(void *orig_state, AppProto proto_orig)
|
|
|
|
|
{
|
|
|
|
|
void *s = FTPCalloc(1, sizeof(FtpDataState));
|
|
|
|
|
if (unlikely(s == NULL))
|
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
|
|
FtpDataState *state = (FtpDataState *) s;
|
|
|
|
|
state->state = FTPDATA_STATE_IN_PROGRESS;
|
|
|
|
|
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
SCMutexLock(&ftpdata_state_mem_lock);
|
|
|
|
|
ftpdata_state_memcnt++;
|
|
|
|
|
ftpdata_state_memuse+=sizeof(FtpDataState);
|
|
|
|
|
SCMutexUnlock(&ftpdata_state_mem_lock);
|
|
|
|
|
#endif
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPDataStateFree(void *s)
|
|
|
|
|
{
|
|
|
|
|
FtpDataState *fstate = (FtpDataState *) s;
|
|
|
|
|
|
|
|
|
|
SCAppLayerTxDataCleanup(&fstate->tx_data);
|
|
|
|
|
|
|
|
|
|
if (fstate->file_name != NULL) {
|
|
|
|
|
FTPFree(fstate->file_name, fstate->file_len + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FileContainerFree(fstate->files, &sbcfg);
|
|
|
|
|
|
|
|
|
|
FTPFree(s, sizeof(FtpDataState));
|
|
|
|
|
#ifdef DEBUG
|
|
|
|
|
SCMutexLock(&ftpdata_state_mem_lock);
|
|
|
|
|
ftpdata_state_memcnt--;
|
|
|
|
|
ftpdata_state_memuse-=sizeof(FtpDataState);
|
|
|
|
|
SCMutexUnlock(&ftpdata_state_mem_lock);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppLayerTxData *FTPDataGetTxData(void *vtx)
|
|
|
|
|
{
|
|
|
|
|
FtpDataState *ftp_state = (FtpDataState *)vtx;
|
|
|
|
|
return &ftp_state->tx_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppLayerStateData *FTPDataGetStateData(void *vstate)
|
|
|
|
|
{
|
|
|
|
|
FtpDataState *ftp_state = (FtpDataState *)vstate;
|
|
|
|
|
return &ftp_state->state_data;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPDataStateTransactionFree(void *state, uint64_t tx_id)
|
|
|
|
|
{
|
|
|
|
|
/* do nothing */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void *FTPDataGetTx(void *state, uint64_t tx_id)
|
|
|
|
|
{
|
|
|
|
|
FtpDataState *ftp_state = (FtpDataState *)state;
|
|
|
|
|
return ftp_state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint64_t FTPDataGetTxCnt(void *state)
|
|
|
|
|
{
|
|
|
|
|
/* ftp-data is single tx */
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int FTPDataGetAlstateProgress(void *tx, uint8_t direction)
|
|
|
|
|
{
|
|
|
|
|
FtpDataState *ftpdata_state = (FtpDataState *)tx;
|
|
|
|
|
if (direction == ftpdata_state->direction)
|
|
|
|
|
return ftpdata_state->state;
|
|
|
|
|
else
|
|
|
|
|
return FTPDATA_STATE_FINISHED;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static AppLayerGetFileState FTPDataStateGetTxFiles(void *tx, uint8_t direction)
|
|
|
|
|
{
|
|
|
|
|
FtpDataState *ftpdata_state = (FtpDataState *)tx;
|
|
|
|
|
AppLayerGetFileState files = { .fc = NULL, .cfg = &sbcfg };
|
|
|
|
|
|
|
|
|
|
if (direction == ftpdata_state->direction)
|
|
|
|
|
files.fc = ftpdata_state->files;
|
|
|
|
|
|
|
|
|
|
return files;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPSetMpmState(void)
|
|
|
|
|
{
|
|
|
|
|
ftp_mpm_ctx = SCCalloc(1, sizeof(MpmCtx));
|
|
|
|
|
if (unlikely(ftp_mpm_ctx == NULL)) {
|
|
|
|
|
exit(EXIT_FAILURE);
|
|
|
|
|
}
|
|
|
|
|
MpmInitCtx(ftp_mpm_ctx, FTP_MPM);
|
|
|
|
|
|
|
|
|
|
SCFTPSetMpmState(ftp_mpm_ctx);
|
|
|
|
|
mpm_table[FTP_MPM].Prepare(NULL, ftp_mpm_ctx);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void FTPFreeMpmState(void)
|
|
|
|
|
{
|
|
|
|
|
if (ftp_mpm_ctx != NULL) {
|
|
|
|
|
mpm_table[FTP_MPM].DestroyCtx(ftp_mpm_ctx);
|
|
|
|
|
SCFree(ftp_mpm_ctx);
|
|
|
|
|
ftp_mpm_ctx = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \brief FTP tx iterator, specialized for its linked list
|
|
|
|
|
*
|
|
|
|
|
* \retval txptr or NULL if no more txs in list
|
|
|
|
|
*/
|
|
|
|
|
static AppLayerGetTxIterTuple FTPGetTxIterator(const uint8_t ipproto, const AppProto alproto,
|
|
|
|
|
void *alstate, uint64_t min_tx_id, uint64_t max_tx_id, AppLayerGetTxIterState *state)
|
|
|
|
|
{
|
|
|
|
|
FtpState *ftp_state = (FtpState *)alstate;
|
|
|
|
|
AppLayerGetTxIterTuple no_tuple = { NULL, 0, false };
|
|
|
|
|
if (ftp_state) {
|
|
|
|
|
FTPTransaction *tx_ptr;
|
|
|
|
|
if (state->un.ptr == NULL) {
|
|
|
|
|
tx_ptr = TAILQ_FIRST(&ftp_state->tx_list);
|
|
|
|
|
} else {
|
|
|
|
|
tx_ptr = (FTPTransaction *)state->un.ptr;
|
|
|
|
|
}
|
|
|
|
|
if (tx_ptr) {
|
|
|
|
|
while (tx_ptr->tx_id < min_tx_id) {
|
|
|
|
|
tx_ptr = TAILQ_NEXT(tx_ptr, next);
|
|
|
|
|
if (!tx_ptr) {
|
|
|
|
|
return no_tuple;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (tx_ptr->tx_id >= max_tx_id) {
|
|
|
|
|
return no_tuple;
|
|
|
|
|
}
|
|
|
|
|
state->un.ptr = TAILQ_NEXT(tx_ptr, next);
|
|
|
|
|
AppLayerGetTxIterTuple tuple = {
|
|
|
|
|
.tx_ptr = tx_ptr,
|
|
|
|
|
.tx_id = tx_ptr->tx_id,
|
|
|
|
|
.has_next = (state->un.ptr != NULL),
|
|
|
|
|
};
|
|
|
|
|
return tuple;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return no_tuple;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void RegisterFTPParsers(void)
|
|
|
|
|
{
|
|
|
|
|
const char *proto_name = "ftp";
|
|
|
|
|
const char *proto_data_name = "ftp-data";
|
|
|
|
|
|
|
|
|
|
/** FTP */
|
|
|
|
|
if (SCAppLayerProtoDetectConfProtoDetectionEnabled("tcp", proto_name)) {
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
AppLayerProtoDetectRegisterProtocol(ALPROTO_FTP, proto_name);
|
|
|
|
|
if (FTPRegisterPatternsForProtocolDetection() < 0 )
|
|
|
|
|
return;
|
|
|
|
|
AppLayerProtoDetectRegisterProtocol(ALPROTO_FTPDATA, proto_data_name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (SCAppLayerParserConfParserEnabled("tcp", proto_name)) {
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_FTP, STREAM_TOSERVER,
|
|
|
|
|
FTPParseRequest);
|
|
|
|
|
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_FTP, STREAM_TOCLIENT,
|
|
|
|
|
FTPParseResponse);
|
|
|
|
|
AppLayerParserRegisterStateFuncs(IPPROTO_TCP, ALPROTO_FTP, FTPStateAlloc, FTPStateFree);
|
|
|
|
|
SCAppLayerParserRegisterParserAcceptableDataDirection(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_FTP, STREAM_TOSERVER | STREAM_TOCLIENT);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterTxFreeFunc(IPPROTO_TCP, ALPROTO_FTP, FTPStateTransactionFree);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterGetTx(IPPROTO_TCP, ALPROTO_FTP, FTPGetTx);
|
|
|
|
|
AppLayerParserRegisterTxDataFunc(IPPROTO_TCP, ALPROTO_FTP, FTPGetTxData);
|
|
|
|
|
AppLayerParserRegisterGetTxIterator(IPPROTO_TCP, ALPROTO_FTP, FTPGetTxIterator);
|
|
|
|
|
AppLayerParserRegisterStateDataFunc(IPPROTO_TCP, ALPROTO_FTP, FTPGetStateData);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterLocalStorageFunc(IPPROTO_TCP, ALPROTO_FTP, FTPLocalStorageAlloc,
|
|
|
|
|
FTPLocalStorageFree);
|
|
|
|
|
AppLayerParserRegisterGetTxCnt(IPPROTO_TCP, ALPROTO_FTP, FTPGetTxCnt);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterGetStateProgressFunc(IPPROTO_TCP, ALPROTO_FTP, FTPGetAlstateProgress);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterStateProgressCompletionStatus(
|
|
|
|
|
ALPROTO_FTP, FTP_STATE_FINISHED, FTP_STATE_FINISHED);
|
|
|
|
|
|
|
|
|
|
AppLayerRegisterExpectationProto(IPPROTO_TCP, ALPROTO_FTPDATA);
|
|
|
|
|
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_FTPDATA, STREAM_TOSERVER,
|
|
|
|
|
FTPDataParseRequest);
|
|
|
|
|
AppLayerParserRegisterParser(IPPROTO_TCP, ALPROTO_FTPDATA, STREAM_TOCLIENT,
|
|
|
|
|
FTPDataParseResponse);
|
|
|
|
|
AppLayerParserRegisterStateFuncs(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataStateAlloc, FTPDataStateFree);
|
|
|
|
|
SCAppLayerParserRegisterParserAcceptableDataDirection(
|
|
|
|
|
IPPROTO_TCP, ALPROTO_FTPDATA, STREAM_TOSERVER | STREAM_TOCLIENT);
|
|
|
|
|
AppLayerParserRegisterTxFreeFunc(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataStateTransactionFree);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterGetTxFilesFunc(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataStateGetTxFiles);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterGetTx(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataGetTx);
|
|
|
|
|
AppLayerParserRegisterTxDataFunc(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataGetTxData);
|
|
|
|
|
AppLayerParserRegisterStateDataFunc(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataGetStateData);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterGetTxCnt(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataGetTxCnt);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterGetStateProgressFunc(IPPROTO_TCP, ALPROTO_FTPDATA, FTPDataGetAlstateProgress);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterStateProgressCompletionStatus(
|
|
|
|
|
ALPROTO_FTPDATA, FTPDATA_STATE_FINISHED, FTPDATA_STATE_FINISHED);
|
|
|
|
|
|
|
|
|
|
AppLayerParserRegisterGetEventInfo(IPPROTO_TCP, ALPROTO_FTP, ftp_get_event_info);
|
|
|
|
|
AppLayerParserRegisterGetEventInfoById(IPPROTO_TCP, ALPROTO_FTP, ftp_get_event_info_by_id);
|
|
|
|
|
|
|
|
|
|
SCAppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_FTP);
|
|
|
|
|
SCAppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_FTPDATA);
|
|
|
|
|
|
|
|
|
|
sbcfg.buf_size = 4096;
|
|
|
|
|
sbcfg.Calloc = FTPCalloc;
|
|
|
|
|
sbcfg.Realloc = FTPRealloc;
|
|
|
|
|
sbcfg.Free = FTPFree;
|
|
|
|
|
|
|
|
|
|
FTPParseMemcap();
|
|
|
|
|
} else {
|
|
|
|
|
SCLogInfo("Parser disabled for %s protocol. Protocol detection still on.", proto_name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FTPSetMpmState();
|
|
|
|
|
|
|
|
|
|
#ifdef UNITTESTS
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
AppLayerParserRegisterProtocolUnittests(IPPROTO_TCP, ALPROTO_FTP, FTPParserRegisterTests);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* \brief Returns the ending offset of the next line from a multi-line buffer.
|
|
|
|
|
*
|
|
|
|
|
* "Buffer" refers to a FTP response in a single buffer containing multiple lines.
|
|
|
|
|
* Here, "next line" is defined as terminating on
|
|
|
|
|
* - Newline character
|
|
|
|
|
* - Null character
|
|
|
|
|
*
|
|
|
|
|
* \param buffer Contains zero or more characters.
|
|
|
|
|
* \param len Size, in bytes, of buffer.
|
|
|
|
|
*
|
|
|
|
|
* \retval Offset from the start of buffer indicating the where the
|
|
|
|
|
* next "line ends". The characters between the input buffer and this
|
|
|
|
|
* value comprise the line.
|
|
|
|
|
*
|
|
|
|
|
* NULL is found first or a newline isn't found, then UINT16_MAX is returned.
|
|
|
|
|
*/
|
|
|
|
|
uint16_t JsonGetNextLineFromBuffer(const char *buffer, const uint16_t len)
|
|
|
|
|
{
|
|
|
|
|
if (!buffer || *buffer == '\0') {
|
|
|
|
|
return UINT16_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const char *c = strchr(buffer, '\n');
|
|
|
|
|
return c == NULL ? len : (uint16_t)(c - buffer + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool EveFTPDataAddMetadata(const void *vtx, void *jb)
|
|
|
|
|
{
|
|
|
|
|
const FtpDataState *ftp_state = vtx;
|
|
|
|
|
SCJbOpenObject(jb, "ftp_data");
|
|
|
|
|
|
|
|
|
|
if (ftp_state->file_name) {
|
|
|
|
|
SCJbSetStringFromBytes(jb, "filename", ftp_state->file_name, ftp_state->file_len);
|
|
|
|
|
}
|
|
|
|
|
switch (ftp_state->command) {
|
|
|
|
|
case FTP_COMMAND_STOR:
|
|
|
|
|
JB_SET_STRING(jb, "command", "STOR");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_APPE:
|
|
|
|
|
JB_SET_STRING(jb, "command", "APPE");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_STOU:
|
|
|
|
|
JB_SET_STRING(jb, "command", "STOU");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_RETR:
|
|
|
|
|
JB_SET_STRING(jb, "command", "RETR");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_NLST:
|
|
|
|
|
JB_SET_STRING(jb, "command", "NLST");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_LIST:
|
|
|
|
|
JB_SET_STRING(jb, "command", "LIST");
|
|
|
|
|
break;
|
|
|
|
|
case FTP_COMMAND_MLSD:
|
|
|
|
|
JB_SET_STRING(jb, "command", "MLSD");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
SCJbClose(jb);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* \brief Free memory allocated for global FTP parser state.
|
|
|
|
|
*/
|
|
|
|
|
void FTPParserCleanup(void)
|
|
|
|
|
{
|
|
|
|
|
FTPFreeMpmState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* UNITTESTS */
|
|
|
|
|
#ifdef UNITTESTS
|
|
|
|
|
#include "flow-util.h"
|
|
|
|
|
#include "stream-tcp.h"
|
|
|
|
|
|
|
|
|
|
/** \test Send a get request in one chunk. */
|
|
|
|
|
static int FTPParserTest01(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t ftpbuf[] = "PORT 192,168,1,1,0,80\r\n";
|
|
|
|
|
uint32_t ftplen = sizeof(ftpbuf) - 1; /* minus the \0 */
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
App layer API rewritten. The main files in question are:
app-layer.[ch], app-layer-detect-proto.[ch] and app-layer-parser.[ch].
Things addressed in this commit:
- Brings out a proper separation between protocol detection phase and the
parser phase.
- The dns app layer now is registered such that we don't use "dnstcp" and
"dnsudp" in the rules. A user who previously wrote a rule like this -
"alert dnstcp....." or
"alert dnsudp....."
would now have to use,
alert dns (ipproto:tcp;) or
alert udp (app-layer-protocol:dns;) or
alert ip (ipproto:udp; app-layer-protocol:dns;)
The same rules extend to other another such protocol, dcerpc.
- The app layer parser api now takes in the ipproto while registering
callbacks.
- The app inspection/detection engine also takes an ipproto.
- All app layer parser functions now take direction as STREAM_TOSERVER or
STREAM_TOCLIENT, as opposed to 0 or 1, which was taken by some of the
functions.
- FlowInitialize() and FlowRecycle() now resets proto to 0. This is
needed by unittests, which would try to clean the flow, and that would
call the api, AppLayerParserCleanupParserState(), which would try to
clean the app state, but the app layer now needs an ipproto to figure
out which api to internally call to clean the state, and if the ipproto
is 0, it would return without trying to clean the state.
- A lot of unittests are now updated where if they are using a flow and
they need to use the app layer, we would set a flow ipproto.
- The "app-layer" section in the yaml conf has also been updated as well.
13 years ago
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_FTP;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP,
|
|
|
|
|
STREAM_TOSERVER | STREAM_EOF, ftpbuf, ftplen);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
FtpState *ftp_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ftp_state);
|
|
|
|
|
FAIL_IF(ftp_state->command != FTP_COMMAND_PORT);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Supply RETR without a filename */
|
|
|
|
|
static int FTPParserTest11(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t ftpbuf1[] = "PORT 192,168,1,1,0,80\r\n";
|
|
|
|
|
uint8_t ftpbuf2[] = "RETR\r\n";
|
|
|
|
|
uint8_t ftpbuf3[] = "227 OK\r\n";
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_FTP;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP,
|
|
|
|
|
STREAM_TOSERVER | STREAM_START, ftpbuf1,
|
|
|
|
|
sizeof(ftpbuf1) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
/* Response */
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP,
|
|
|
|
|
STREAM_TOCLIENT,
|
|
|
|
|
ftpbuf3,
|
|
|
|
|
sizeof(ftpbuf3) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP,
|
|
|
|
|
STREAM_TOSERVER, ftpbuf2,
|
|
|
|
|
sizeof(ftpbuf2) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
FtpState *ftp_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ftp_state);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(ftp_state->command != FTP_COMMAND_RETR);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test Supply STOR without a filename */
|
|
|
|
|
static int FTPParserTest12(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t ftpbuf1[] = "PORT 192,168,1,1,0,80\r\n";
|
|
|
|
|
uint8_t ftpbuf2[] = "STOR\r\n";
|
|
|
|
|
uint8_t ftpbuf3[] = "227 OK\r\n";
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_FTP;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP,
|
|
|
|
|
STREAM_TOSERVER | STREAM_START, ftpbuf1,
|
|
|
|
|
sizeof(ftpbuf1) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
/* Response */
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP,
|
|
|
|
|
STREAM_TOCLIENT,
|
|
|
|
|
ftpbuf3,
|
|
|
|
|
sizeof(ftpbuf3) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP,
|
|
|
|
|
STREAM_TOSERVER, ftpbuf2,
|
|
|
|
|
sizeof(ftpbuf2) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
FtpState *ftp_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ftp_state);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(ftp_state->command != FTP_COMMAND_STOR);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
ftp: strip trailing whitespace before allocating
CopyCommandLine allocated a buffer for the whole request line, stripped
the trailing whitespace and returned the shorter length. That shorter
length is what the caller stores in tx->request_length and what
FTPTransactionFree later hands to FTPFree, so one byte for every stripped
whitespace character is never subtracted from ftp_memuse. The memory
itself is released -- SCFree ignores the size it is given -- but the
counter keeps climbing for the life of the process, and a client that
pads its commands with spaces can push it up with every command it sends.
Once the counter reaches app-layer.protocols.ftp.memcap, FTPCheckMemcap
refuses any allocation that would cross it, and request buffers are the
first to be refused: the transaction is still created, but tx->request
stays NULL, so command_data is missing from the log and rules that
inspect it stop matching. Whether a new flow can still allocate its state
depends on how much room is left below the cap. With no memcap
configured, the symptom is an ftp.memuse counter that is permanently
wrong.
Strip the whitespace before allocating, so the size accounted for is the
size returned to the caller:
- CopyCommandLine shortens line->len first, then allocates and copies
that length.
- A line that is nothing but whitespace returns without allocating. It
used to get a buffer that was stored on the transaction and freed with
a length of zero, leaving the whole allocation on the counter.
- When the allocation fails, line->len has already been shortened, so
port_line, the transfer file name and the too-long-line check see the
same length there that they see when it succeeds.
- FTPParserTest13 covers the accounting and the stored request;
FTPParserTest14 covers the line the parser uses afterwards.
Issue: 8848
2 months ago
|
|
|
|
|
|
|
|
/** \test A command padded with trailing whitespace must leave the memuse
|
|
|
|
|
* counter where it found it, and the padding must not be kept. */
|
|
|
|
|
static int FTPParserTest13(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t ftpbuf[] = "USER anonymous \r\n";
|
|
|
|
|
const char expected[] = "USER anonymous";
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_FTP;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
const uint64_t memuse = SC_ATOMIC_GET(ftp_memuse);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP, STREAM_TOSERVER | STREAM_EOF,
|
|
|
|
|
ftpbuf, sizeof(ftpbuf) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
FtpState *ftp_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ftp_state);
|
|
|
|
|
FAIL_IF(ftp_state->command != FTP_COMMAND_USER);
|
|
|
|
|
|
|
|
|
|
/* the request is what the transaction is freed with, so it has to be the
|
|
|
|
|
* stripped line: the padding is neither stored nor accounted */
|
|
|
|
|
FTPTransaction *tx = TAILQ_FIRST(&ftp_state->tx_list);
|
|
|
|
|
FAIL_IF_NULL(tx);
|
|
|
|
|
FAIL_IF_NULL(tx->request);
|
|
|
|
|
FAIL_IF(tx->request_length != sizeof(expected));
|
|
|
|
|
FAIL_IF(memcmp(tx->request, expected, sizeof(expected)) != 0);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
FAIL_IF(SC_ATOMIC_GET(ftp_memuse) != memuse);
|
|
|
|
|
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** \test The padding is stripped from the line itself, not just from the copy
|
|
|
|
|
* kept on the transaction: what follows the copy has to see the
|
|
|
|
|
* shortened line. */
|
|
|
|
|
static int FTPParserTest14(void)
|
|
|
|
|
{
|
|
|
|
|
Flow f;
|
|
|
|
|
uint8_t ftpbuf1[] = "PORT 192,168,1,1,0,80 \r\n";
|
|
|
|
|
uint8_t ftpbuf2[] = "227 OK\r\n";
|
|
|
|
|
const char expected[] = "PORT 192,168,1,1,0,80";
|
|
|
|
|
TcpSession ssn;
|
|
|
|
|
|
|
|
|
|
AppLayerParserThreadCtx *alp_tctx = AppLayerParserThreadCtxAlloc();
|
|
|
|
|
|
|
|
|
|
memset(&f, 0, sizeof(f));
|
|
|
|
|
memset(&ssn, 0, sizeof(ssn));
|
|
|
|
|
|
|
|
|
|
f.protoctx = (void *)&ssn;
|
|
|
|
|
f.proto = IPPROTO_TCP;
|
|
|
|
|
f.alproto = ALPROTO_FTP;
|
|
|
|
|
|
|
|
|
|
StreamTcpInitConfig(true);
|
|
|
|
|
|
|
|
|
|
int r = AppLayerParserParse(NULL, alp_tctx, &f, ALPROTO_FTP, STREAM_TOSERVER | STREAM_START,
|
|
|
|
|
ftpbuf1, sizeof(ftpbuf1) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
FtpState *ftp_state = f.alstate;
|
|
|
|
|
FAIL_IF_NULL(ftp_state);
|
|
|
|
|
FAIL_IF(ftp_state->command != FTP_COMMAND_PORT);
|
|
|
|
|
|
|
|
|
|
/* the port line is taken from the line, so the padding must be gone */
|
|
|
|
|
FAIL_IF(ftp_state->port_line_len != sizeof(expected) - 1);
|
|
|
|
|
FAIL_IF(memcmp(ftp_state->port_line, expected, sizeof(expected) - 1) != 0);
|
|
|
|
|
|
|
|
|
|
r = AppLayerParserParse(
|
|
|
|
|
NULL, alp_tctx, &f, ALPROTO_FTP, STREAM_TOCLIENT, ftpbuf2, sizeof(ftpbuf2) - 1);
|
|
|
|
|
FAIL_IF(r != 0);
|
|
|
|
|
|
|
|
|
|
FAIL_IF(ftp_state->dyn_port != 80);
|
|
|
|
|
|
|
|
|
|
FLOW_DESTROY(&f);
|
|
|
|
|
AppLayerParserThreadCtxFree(alp_tctx);
|
|
|
|
|
StreamTcpFreeConfig(true);
|
|
|
|
|
PASS;
|
|
|
|
|
}
|
|
|
|
|
#endif /* UNITTESTS */
|
|
|
|
|
|
|
|
|
|
void FTPParserRegisterTests(void)
|
|
|
|
|
{
|
|
|
|
|
#ifdef UNITTESTS
|
|
|
|
|
UtRegisterTest("FTPParserTest01", FTPParserTest01);
|
|
|
|
|
UtRegisterTest("FTPParserTest11", FTPParserTest11);
|
|
|
|
|
UtRegisterTest("FTPParserTest12", FTPParserTest12);
|
ftp: strip trailing whitespace before allocating
CopyCommandLine allocated a buffer for the whole request line, stripped
the trailing whitespace and returned the shorter length. That shorter
length is what the caller stores in tx->request_length and what
FTPTransactionFree later hands to FTPFree, so one byte for every stripped
whitespace character is never subtracted from ftp_memuse. The memory
itself is released -- SCFree ignores the size it is given -- but the
counter keeps climbing for the life of the process, and a client that
pads its commands with spaces can push it up with every command it sends.
Once the counter reaches app-layer.protocols.ftp.memcap, FTPCheckMemcap
refuses any allocation that would cross it, and request buffers are the
first to be refused: the transaction is still created, but tx->request
stays NULL, so command_data is missing from the log and rules that
inspect it stop matching. Whether a new flow can still allocate its state
depends on how much room is left below the cap. With no memcap
configured, the symptom is an ftp.memuse counter that is permanently
wrong.
Strip the whitespace before allocating, so the size accounted for is the
size returned to the caller:
- CopyCommandLine shortens line->len first, then allocates and copies
that length.
- A line that is nothing but whitespace returns without allocating. It
used to get a buffer that was stored on the transaction and freed with
a length of zero, leaving the whole allocation on the counter.
- When the allocation fails, line->len has already been shortened, so
port_line, the transfer file name and the too-long-line check see the
same length there that they see when it succeeds.
- FTPParserTest13 covers the accounting and the stored request;
FTPParserTest14 covers the line the parser uses afterwards.
Issue: 8848
2 months ago
|
|
|
UtRegisterTest("FTPParserTest13", FTPParserTest13);
|
|
|
|
|
UtRegisterTest("FTPParserTest14", FTPParserTest14);
|
|
|
|
|
#endif /* UNITTESTS */
|
|
|
|
|
}
|