]> git.ipfire.org Git - thirdparty/snort3.git/commitdiff
Pull request #4960: ssh: support fields for extractor
authorAkhilesh MY (amuttuva) <amuttuva@cisco.com>
Fri, 21 Nov 2025 06:49:10 +0000 (06:49 +0000)
committerShanmugam S (shanms) <shanms@cisco.com>
Fri, 21 Nov 2025 06:49:10 +0000 (06:49 +0000)
Merge in SNORT/snort3 from ~AMUTTUVA/snort3:ssh_ext to master

Squashed commit of the following:

commit 0179324498cb13d08a1a23b44eee55ce1fa92e19
Author: Akhilesh MY <amuttuva@cisco.com>
Date:   Mon Oct 27 05:37:19 2025 -0400

    ssh: support fields for extractor

src/pub_sub/CMakeLists.txt
src/pub_sub/ssh_events.cc [new file with mode: 0644]
src/pub_sub/ssh_events.h
src/pub_sub/test/CMakeLists.txt
src/pub_sub/test/pub_sub_ssh_events_test.cc [new file with mode: 0644]
src/service_inspectors/ssh/CMakeLists.txt
src/service_inspectors/ssh/ssh.cc
src/service_inspectors/ssh/ssh.h
src/service_inspectors/ssh/ssh_types.h [new file with mode: 0644]

index a6b0c55828a4ba14bd330b12f0f89f85610dba30..3d4ec80aaba6647a668b756c4cdb4ec86ec40e04 100644 (file)
@@ -53,6 +53,7 @@ add_library( pub_sub OBJECT
     http_transaction_end_event.cc
     quic_events.cc
     sip_events.cc
+    ssh_events.cc
 )
 
 install (FILES ${PUB_SUB_INCLUDES}
diff --git a/src/pub_sub/ssh_events.cc b/src/pub_sub/ssh_events.cc
new file mode 100644 (file)
index 0000000..2be6f5d
--- /dev/null
@@ -0,0 +1,73 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+// ssh_events.cc author Cisco
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "ssh_events.h"
+
+SshEventType SshEvent::get_event_type() const
+{ return event_type; }
+
+SshValidationResult SshEvent::get_validation_result() const
+{ return result; }
+
+const std::string& SshEvent::get_version_str() const
+{ return version_str; }
+
+uint8_t SshEvent::get_direction() const
+{ return direction; }
+
+const snort::Packet* SshEvent::get_packet() const
+{ return packet; }
+
+const char* SshEvent::get_login_direction() const
+{ return login_direction; }
+
+uint8_t SshEvent::get_ssh_version() const
+{ return ssh_version; }
+
+const char* SshAlgoEvent::get_kex_algorithms() const
+{ return algos.named.kex_algorithms; }
+
+const char* SshAlgoEvent::get_server_host_key_algorithms() const
+{ return algos.named.server_host_key_algorithms; }
+
+const char* SshAlgoEvent::get_encryption_algorithms_client_to_server() const
+{ return algos.named.encryption_algorithms_client_to_server; }
+
+const char* SshAlgoEvent::get_encryption_algorithms_server_to_client() const
+{ return algos.named.encryption_algorithms_server_to_client; }
+
+const char* SshAlgoEvent::get_mac_algorithms_client_to_server() const
+{ return algos.named.mac_algorithms_client_to_server; }
+
+const char* SshAlgoEvent::get_mac_algorithms_server_to_client() const
+{ return algos.named.mac_algorithms_server_to_client; }
+
+const char* SshAlgoEvent::get_compression_algorithms_client_to_server() const
+{ return algos.named.compression_algorithms_client_to_server; }
+
+const char* SshAlgoEvent::get_compression_algorithms_server_to_client() const
+{ return algos.named.compression_algorithms_server_to_client; }
+
+uint8_t SshAlgoEvent::get_direction() const
+{ return direction; }
+
index cc83f154c1f425fd782be79b407f943804fa4722..fa778a940504de8214efc7f22b3b2df22ef5c4c5 100644 (file)
@@ -24,9 +24,9 @@
 // for use by data bus subscribers
 
 #include "framework/data_bus.h"
-#include "service_inspectors/ssh/ssh.h"
+#include "service_inspectors/ssh/ssh_types.h"
 
-struct  SshEventIds { enum : unsigned { STATE_CHANGE, num_ids }; };
+struct  SshEventIds { enum : unsigned { STATE_CHANGE, ALGORITHM, num_ids }; };
 
 const snort::PubKey ssh_pub_key { "ssh", SshEventIds::num_ids };
 
@@ -44,28 +44,24 @@ enum SshValidationResult
     SSH_INVALID_KEXINIT
 };
 
-class SshEvent : public snort::DataEvent
+class SO_PUBLIC SshEvent : public snort::DataEvent
 {
 public:
     SshEvent(const SshEventType event_type, const SshValidationResult result,
-        const std::string& version_str, const uint8_t direction, const snort::Packet* packet) :
-        event_type(event_type), result(result), version_str(version_str), direction(direction), packet(packet)
+        const std::string& version_str, const uint8_t direction,
+        const snort::Packet* packet, const char* login_direction, uint8_t ssh_version) :
+        event_type(event_type), result(result), version_str(version_str),
+        direction(direction), packet(packet), login_direction(login_direction),
+        ssh_version(ssh_version)
         { }
 
-    SshEventType get_event_type() const
-    { return event_type; }
-
-    SshValidationResult get_validation_result() const
-    { return result; }
-
-    const std::string& get_version_str() const
-    { return version_str; }
-
-    uint8_t get_direction() const
-    { return direction; }
-
-    const snort::Packet* get_packet() const override
-    { return packet; }
+    SshEventType get_event_type() const;
+    SshValidationResult get_validation_result() const;
+    const std::string& get_version_str() const;
+    uint8_t get_direction() const;
+    const snort::Packet* get_packet() const override;
+    const char* get_login_direction() const;
+    uint8_t get_ssh_version() const;
 
 private:
     const SshEventType event_type;
@@ -73,6 +69,45 @@ private:
     const std::string version_str;
     const uint8_t direction;
     const snort::Packet* packet;
+    const char* login_direction;
+    const uint8_t ssh_version;
+};
+
+class SO_PUBLIC SshAlgoEvent : public snort::DataEvent
+{
+public:
+    union Algorithms
+    {
+        struct
+        {
+            const char* kex_algorithms;
+            const char* server_host_key_algorithms;
+            const char* encryption_algorithms_client_to_server;
+            const char* encryption_algorithms_server_to_client;
+            const char* mac_algorithms_client_to_server;
+            const char* mac_algorithms_server_to_client;
+            const char* compression_algorithms_client_to_server;
+            const char* compression_algorithms_server_to_client;
+        } named;
+        const char* unnamed[NUM_KEXINIT_LISTS];
+    };
+
+    SshAlgoEvent(const Algorithms& algos, uint8_t dir) : algos(algos), direction(dir)
+    { }
+
+    const char* get_kex_algorithms() const;
+    const char* get_server_host_key_algorithms() const;
+    const char* get_encryption_algorithms_client_to_server() const;
+    const char* get_encryption_algorithms_server_to_client() const;
+    const char* get_mac_algorithms_client_to_server() const;
+    const char* get_mac_algorithms_server_to_client() const;
+    const char* get_compression_algorithms_client_to_server() const;
+    const char* get_compression_algorithms_server_to_client() const;
+    uint8_t get_direction() const;
+
+private:
+    const Algorithms& algos;
+    uint8_t direction;
 };
 
 #endif
index e1047026ea34bc73570dae93f6cfbcd3dc4ef338..0e2eb58a408cc7983966c7618ad4be6188bbf21c 100644 (file)
@@ -21,6 +21,11 @@ add_cpputest( pub_sub_eve_process_event_test
     SOURCES
         ../eve_process_event.h
 )
+add_cpputest( pub_sub_ssh_events_test
+    SOURCES
+        ../ssh_events.cc
+)
+
 add_cpputest( pub_sub_http_transaction_end_event_test
     SOURCES
         ../http_transaction_end_event.cc
diff --git a/src/pub_sub/test/pub_sub_ssh_events_test.cc b/src/pub_sub/test/pub_sub_ssh_events_test.cc
new file mode 100644 (file)
index 0000000..d398bf5
--- /dev/null
@@ -0,0 +1,117 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// pub_sub_ssh_events_test.cc author Cisco
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include <string>
+#include <cstring>
+
+#include "pub_sub/ssh_events.h"
+#include "service_inspectors/ssh/ssh.h"
+#include "protocols/packet.h"
+
+#include <CppUTest/CommandLineTestRunner.h>
+#include <CppUTest/TestHarness.h>
+
+using namespace snort;
+
+namespace snort
+{
+Packet::Packet(bool)
+    : flow(nullptr), packet_flags(0), xtradata_mask(0), proto_bits(0), alt_dsize(0), num_layers(0),
+    ip_proto_next(IpProtocol::PROTO_NOT_SET), disable_inspect(true), sect(PS_NONE), active_inst(nullptr), pkth(nullptr),
+    pkt(nullptr), layers(nullptr), user_inspection_policy_id(0), user_ips_policy_id(0), user_network_policy_id(0),
+    inspection_started_timestamp(0), vlan_idx(0), ts_packet_flags(0), allocated(false), daq_msg(nullptr)
+{ }
+Packet::~Packet() = default;
+}
+
+TEST_GROUP(pub_sub_ssh_events_test) { };
+
+TEST(pub_sub_ssh_events_test, ssh_algo_event_complete_access)
+{
+    const char* kex_algos = "Diffie-sha256,Diffie-sha512";
+    const char* host_key_algos = "SSH2-sha512,SSH1-sha256,SSHFP";
+    const char* cipher_c2s = "ctrk-SHA256,ctrl-sha512";
+    const char* cipher_s2c = "XSHA256,XSHA1";
+    const char* mac_c2s = "HMAC-SHA256,HMAC-sha512";
+    const char* mac_s2c = "HMAC-sha512,HMAC-SHA256";
+    const char* comp_c2s = "none,OpenSSH";
+    const char* comp_s2c = "none,OpenSSH";
+
+    SshAlgoEvent::Algorithms algos;
+    memset(&algos, 0, sizeof(algos));
+
+    algos.named.kex_algorithms = kex_algos;
+    algos.named.server_host_key_algorithms = host_key_algos;
+    algos.named.encryption_algorithms_client_to_server = cipher_c2s;
+    algos.named.encryption_algorithms_server_to_client = cipher_s2c;
+    algos.named.mac_algorithms_client_to_server = mac_c2s;
+    algos.named.mac_algorithms_server_to_client = mac_s2c;
+    algos.named.compression_algorithms_client_to_server = comp_c2s;
+    algos.named.compression_algorithms_server_to_client = comp_s2c;
+
+    SshAlgoEvent event(algos, PKT_FROM_CLIENT);
+
+    CHECK(event.get_direction() == PKT_FROM_CLIENT);
+
+    CHECK(event.get_kex_algorithms() == kex_algos);
+    CHECK(event.get_server_host_key_algorithms() == host_key_algos);
+    CHECK(event.get_encryption_algorithms_client_to_server() == cipher_c2s);
+    CHECK(event.get_encryption_algorithms_server_to_client() == cipher_s2c);
+    CHECK(event.get_mac_algorithms_client_to_server() == mac_c2s);
+    CHECK(event.get_mac_algorithms_server_to_client() == mac_s2c);
+    CHECK(event.get_compression_algorithms_client_to_server() == comp_c2s);
+    CHECK(event.get_compression_algorithms_server_to_client() == comp_s2c);
+}
+
+TEST(pub_sub_ssh_events_test, ssh_state_change_event)
+{
+    Packet test_packet;
+    const std::string version_str = "SSH-2.0-OpenSSH_8.0";
+    
+    SshEvent inbound_event(SSH_VERSION_STRING, SSH_NOT_FINISHED, version_str,
+                          PKT_FROM_CLIENT, &test_packet, "inbound", SSH_VERSION_2);
+
+    CHECK(inbound_event.get_event_type() == SSH_VERSION_STRING);
+    CHECK(inbound_event.get_validation_result() == SSH_NOT_FINISHED);
+    CHECK(inbound_event.get_version_str() == version_str);
+    CHECK(inbound_event.get_direction() == PKT_FROM_CLIENT);
+    CHECK(inbound_event.get_packet() == &test_packet);
+    STRCMP_EQUAL("inbound", inbound_event.get_login_direction());
+    CHECK(inbound_event.get_ssh_version() == SSH_VERSION_2);
+
+    SshEvent outbound_event(SSH_VALIDATION, SSH_VALID_KEXINIT, version_str,
+                           PKT_FROM_SERVER, &test_packet, "outbound", SSH_VERSION_1);
+
+    CHECK(outbound_event.get_event_type() == SSH_VALIDATION);
+    CHECK(outbound_event.get_validation_result() == SSH_VALID_KEXINIT);
+    CHECK(outbound_event.get_direction() == PKT_FROM_SERVER);
+    CHECK(outbound_event.get_packet() == &test_packet);
+    STRCMP_EQUAL("outbound", outbound_event.get_login_direction());
+    CHECK(outbound_event.get_ssh_version() == SSH_VERSION_1);
+}
+
+int main(int argc, char** argv)
+{
+    return CommandLineTestRunner::RunAllTests(argc, argv);
+}
index b73bf25bfb806e12784e5b6096d27cd491fc8821..255141767ca1e790604eef936461f0f1f3104d2b 100644 (file)
@@ -1,4 +1,8 @@
 
+set (SSH_INCLUDES
+    ssh_types.h
+)
+
 set( FILE_LIST
     ssh.cc
     ssh.h
@@ -7,6 +11,7 @@ set( FILE_LIST
     ssh_module.h
     ssh_splitter.cc
     ssh_splitter.h
+    ${SSH_INCLUDES}
 )
 
 if (STATIC_INSPECTORS)
@@ -16,3 +21,7 @@ else (STATIC_INSPECTORS)
     add_dynamic_module(ssh inspectors ${FILE_LIST})
 
 endif (STATIC_INSPECTORS)
+
+install(FILES ${SSH_INCLUDES}
+    DESTINATION "${INCLUDE_INSTALL_PATH}/service_inspectors/ssh/"
+)
index c410cfd3f5d9c52d286b71e8830c80d9b15691c3..be23d90a85e07c2097e9317f9a5836e9286f5a39 100644 (file)
@@ -136,20 +136,31 @@ static void snort_ssh(SSH_PROTO_CONF* config, Packet* p)
 
     if (!(sessp->state_flags & SSH_FLG_SESS_ENCRYPTED))
     {
+        const char* login_direction = "";
         // If server and client have not performed the protocol
         // version exchange yet, must look for version strings.
         if (!(sessp->state_flags & search_dir_ver))
         {
+            const SfIp* server_ip = &p->flow->server_ip;
+            const SfIp* client_ip = &p->flow->client_ip;
+
+            if (server_ip->is_private() != client_ip->is_private())
+            {
+                login_direction = server_ip->is_private() ? "inbound" : "outbound";
+            }
+
             bool valid_version = process_ssh_version_string(config, sessp, p, direction);
             if (valid_version)
             {
                 std::string proto_string((const char *)(p->data), p->dsize);
-                SshEvent event(SSH_VERSION_STRING, SSH_NOT_FINISHED, proto_string, pkt_direction, p);
+                SshEvent event(SSH_VERSION_STRING, SSH_NOT_FINISHED, proto_string, pkt_direction, p, login_direction,
+                    sessp->version);
                 DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow);
             }
             else
             {
-                SshEvent event(SSH_VALIDATION, SSH_INVALID_VERSION, "", pkt_direction, p);
+                SshEvent event(SSH_VALIDATION, SSH_INVALID_VERSION, "", pkt_direction, p, login_direction,
+                    sessp->version);
                 DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow);
                 if (sessp->version == NON_SSH_TRAFFIC)
                     sessp->ssh_aborted = true;
@@ -173,12 +184,14 @@ static void snort_ssh(SSH_PROTO_CONF* config, Packet* p)
             }
             if (keyx_valid)
             {
-                SshEvent event(SSH_VALIDATION, SSH_VALID_KEXINIT, "", pkt_direction, p);
+                SshEvent event(SSH_VALIDATION, SSH_VALID_KEXINIT, "", pkt_direction, p, login_direction,
+                    sessp->version);
                 DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow);
             }
             else
             {
-                SshEvent event(SSH_VALIDATION, SSH_INVALID_KEXINIT, "", pkt_direction, p);
+                SshEvent event(SSH_VALIDATION, SSH_INVALID_KEXINIT, "", pkt_direction, p, login_direction,
+                    sessp->version);
                 DataBus::publish(pub_id, SshEventIds::STATE_CHANGE, event, p->flow);
                 sessp->state_flags |= SSH_FLG_SESS_ENCRYPTED;
             }
@@ -423,18 +436,27 @@ static bool process_ssh2_kexinit(SSHData *sessionp, Packet *p, uint8_t direction
     default:
         return false;
     }
+
+    SshAlgoEvent::Algorithms algos;
+    
     uint16_t total_length = sizeof(SSH2KeyExchange);
     const uint8_t *data = p->data + sizeof(SSH2KeyExchange);
     for (int i = 0; i < NUM_KEXINIT_LISTS; i++)
     {
-        uint32_t list_length = ntohl(*((const uint32_t*)data)) + sizeof(uint32_t);
-        if (list_length > ssh_length or total_length + list_length > ssh_length)
+        uint32_t list_length = ntohl(*((const uint32_t*)data));
+        uint32_t total_list_length = list_length + sizeof(uint32_t);
+
+        if (total_list_length > ssh_length or total_length + total_list_length > ssh_length)
         {
             DetectionEngine::queue_event(GID_SSH, SSH_EVENT_PAYLOAD_SIZE);
             return false;
         }
-        total_length += list_length;
-        data += list_length;
+
+        if (list_length)
+            algos.unnamed[i] = (const char*)(data + sizeof(uint32_t));
+
+        total_length += total_list_length;
+        data += total_list_length;
     }
     total_length += sizeof(SSHKeyExchangeFinal) + ssh_pkt->msg.plen;
     if (total_length != ssh_length)
@@ -448,6 +470,14 @@ static bool process_ssh2_kexinit(SSHData *sessionp, Packet *p, uint8_t direction
         // using an unsupported future version
         return false;
     }
+
+    if (ssh_pkt->msg.code == SSH_MSG_KEXINIT)
+    {
+        uint8_t pkt_direction = direction == SSH_DIR_FROM_CLIENT ? PKT_FROM_CLIENT : PKT_FROM_SERVER;
+        SshAlgoEvent event(algos, pkt_direction);
+        DataBus::publish(pub_id, SshEventIds::ALGORITHM, event, p->flow);
+    }
+
     return true;
 }
 
index 5e1003a146b1adea86bec8d4f7f03b47dd23ce5c..bca0a93bc8eefc10e99b0640d16b35f1ffecfa18 100644 (file)
@@ -32,6 +32,7 @@
 
 #include "flow/flow.h"
 #include "protocols/packet.h"
+#include "ssh_types.h"
 
 // FIXIT-L move these to ssh.cc
 // Session state flags for SSHData::state_flags
@@ -124,7 +125,6 @@ public:
 #define SSH_PACKET_MAX_SIZE 35000
 #define SSH_MAX_BANNER_LEN 255
 #define SSH2_COOKIE_SIZE 16
-#define NUM_KEXINIT_LISTS 10
 #define SSH_MIN_BANNER_LEN 9 //SSH-2.0-*\n
 #define SSH1_KEYX_MIN_SIZE (4 + 8 + 1) // length + padding + message
 
diff --git a/src/service_inspectors/ssh/ssh_types.h b/src/service_inspectors/ssh/ssh_types.h
new file mode 100644 (file)
index 0000000..28b26ae
--- /dev/null
@@ -0,0 +1,26 @@
+//--------------------------------------------------------------------------
+// Copyright (C) 2024-2025 Cisco and/or its affiliates. All rights reserved.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License Version 2 as published
+// by the Free Software Foundation.  You may not use, modify or distribute
+// this program under any other version of the GNU General Public License.
+//
+// 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 along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+//--------------------------------------------------------------------------
+
+// ssh_types.h - author Cisco
+
+#ifndef SSH_TYPES_H
+#define SSH_TYPES_H
+
+constexpr int NUM_KEXINIT_LISTS = 10;
+
+#endif