From 5856ced29ceb6714d49fa7b394688a22464d99a2 Mon Sep 17 00:00:00 2001 From: Jason Ish Date: Wed, 24 Jun 2026 09:52:05 -0600 Subject: [PATCH] ftp: support LIST and MLSD data channels Support LIST and MLSD on the ftp data channel. Ticket: #8664 --- doc/userguide/rules/ftp-keywords.rst | 6 +++--- rust/src/ftp/constant.rs | 1 + rust/src/ftp/ftp.rs | 1 + src/app-layer-ftp.c | 26 +++++++++++++++++++++++--- src/detect-ftpdata.c | 25 +++++++++++++++++++++++-- 5 files changed, 51 insertions(+), 8 deletions(-) diff --git a/doc/userguide/rules/ftp-keywords.rst b/doc/userguide/rules/ftp-keywords.rst index 9c378a482d..a7ce017f74 100644 --- a/doc/userguide/rules/ftp-keywords.rst +++ b/doc/userguide/rules/ftp-keywords.rst @@ -7,12 +7,12 @@ ftpdata_command --------------- Filter ftp-data channel based on command used on the FTP command channel. -Currently supported commands are RETR (get on a file) and STOR (put on a -file). +Currently supported commands are STOR (put on a file), RETR (get a file), and +NLST, LIST and MLSD (list directory contents). Syntax:: - ftpdata_command:(retr|stor) + ftpdata_command:(stor|retr|nlst|list|mlsd) Signature Example: diff --git a/rust/src/ftp/constant.rs b/rust/src/ftp/constant.rs index 94bea4f5c8..c8de36f76e 100644 --- a/rust/src/ftp/constant.rs +++ b/rust/src/ftp/constant.rs @@ -84,4 +84,5 @@ pub enum FtpRequestCommand { FTP_COMMAND_UMASK, FTP_COMMAND_USER, FTP_COMMAND_EPRT, + FTP_COMMAND_MLSD, } diff --git a/rust/src/ftp/ftp.rs b/rust/src/ftp/ftp.rs index 787042434e..ba9ae6a255 100644 --- a/rust/src/ftp/ftp.rs +++ b/rust/src/ftp/ftp.rs @@ -78,6 +78,7 @@ lazy_static! { FtpCommand::new("MDTM", FtpRequestCommand::FTP_COMMAND_MDTM), FtpCommand::new("MKD", FtpRequestCommand::FTP_COMMAND_MKD), FtpCommand::new("MLFL", FtpRequestCommand::FTP_COMMAND_MLFL), + FtpCommand::new("MLSD", FtpRequestCommand::FTP_COMMAND_MLSD), FtpCommand::new("MODE", FtpRequestCommand::FTP_COMMAND_MODE), FtpCommand::new("MRCP", FtpRequestCommand::FTP_COMMAND_MRCP), FtpCommand::new("MRSQ", FtpRequestCommand::FTP_COMMAND_MRSQ), diff --git a/src/app-layer-ftp.c b/src/app-layer-ftp.c index ea5c176955..e4d3c13cd6 100644 --- a/src/app-layer-ftp.c +++ b/src/app-layer-ftp.c @@ -481,8 +481,10 @@ static AppLayerResult FTPParseRequest(Flow *f, void *ftp_state, AppLayerParserSt * control direction. */ if ((state->active && state->command == FTP_COMMAND_STOR) || - (!state->active && (state->command == FTP_COMMAND_RETR || - state->command == FTP_COMMAND_NLST))) { + (!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; } @@ -523,7 +525,9 @@ static AppLayerResult FTPParseRequest(Flow *f, void *ftp_state, AppLayerParserSt } has_file = true; /* fallthrough */ - case FTP_COMMAND_NLST: { + 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); @@ -1096,6 +1100,16 @@ static AppLayerResult FTPDataParse(Flow *f, FtpDataState *ftpdata_state, 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; } @@ -1453,6 +1467,12 @@ bool EveFTPDataAddMetadata(void *vtx, SCJsonBuilder *jb) 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; } diff --git a/src/detect-ftpdata.c b/src/detect-ftpdata.c index b7d61e91a9..bb1708316c 100644 --- a/src/detect-ftpdata.c +++ b/src/detect-ftpdata.c @@ -37,7 +37,7 @@ /** * \brief Regex for parsing our keyword options */ -#define PARSE_REGEX "^\\s*(stor|retr|nlst)\\s*$" +#define PARSE_REGEX "^\\s*(stor|retr|nlst|list|mlsd)\\s*$" static DetectParseRegex parse_regex; /* Prototypes of functions registered in DetectFtpdataRegister below */ @@ -147,6 +147,10 @@ static DetectFtpdataData *DetectFtpdataParse(const char *ftpcommandstr) ftpcommandd->command = FTP_COMMAND_RETR; } else if (!strcmp(arg1, "nlst")) { ftpcommandd->command = FTP_COMMAND_NLST; + } else if (!strcmp(arg1, "list")) { + ftpcommandd->command = FTP_COMMAND_LIST; + } else if (!strcmp(arg1, "mlsd")) { + ftpcommandd->command = FTP_COMMAND_MLSD; } else { SCLogError("Invalid command value"); goto error; @@ -213,6 +217,16 @@ static int DetectFtpdataParseTest01(void) FAIL_IF_NULL(ftpcommandd); FAIL_IF(!(ftpcommandd->command == FTP_COMMAND_STOR)); DetectFtpdataFree(NULL, ftpcommandd); + + ftpcommandd = DetectFtpdataParse("list"); + FAIL_IF_NULL(ftpcommandd); + FAIL_IF(!(ftpcommandd->command == FTP_COMMAND_LIST)); + DetectFtpdataFree(NULL, ftpcommandd); + + ftpcommandd = DetectFtpdataParse("mlsd"); + FAIL_IF_NULL(ftpcommandd); + FAIL_IF(!(ftpcommandd->command == FTP_COMMAND_MLSD)); + DetectFtpdataFree(NULL, ftpcommandd); PASS; } @@ -225,7 +239,14 @@ static int DetectFtpdataSignatureTest01(void) FAIL_IF_NULL(sig); sig = DetectEngineAppendSig(de_ctx, "alert ip any any -> any any (ftpdata_command:retr; sid:2; rev:1;)"); FAIL_IF_NULL(sig); - sig = DetectEngineAppendSig(de_ctx, "alert ip any any -> any any (ftpdata_command:xxx; sid:3; rev:1;)"); + sig = DetectEngineAppendSig( + de_ctx, "alert ip any any -> any any (ftpdata_command:list; sid:3; rev:1;)"); + FAIL_IF_NULL(sig); + sig = DetectEngineAppendSig( + de_ctx, "alert ip any any -> any any (ftpdata_command:mlsd; sid:4; rev:1;)"); + FAIL_IF_NULL(sig); + sig = DetectEngineAppendSig( + de_ctx, "alert ip any any -> any any (ftpdata_command:xxx; sid:5; rev:1;)"); FAIL_IF_NOT_NULL(sig); DetectEngineCtxFree(de_ctx);