CVE-2021-45098: Insufficient Information (NVD-CWE-noinfo)
value: An issue was discovered in Suricata before 6.0.4. It is possible to bypass/evade any HTTP-based signature by faking an RST TCP packet with random TCP options of the md5header from the client side. After the three-way handshake, it's possible to inject an RST ACK with a random TCP md5header option. Then, the client can send an HTTP GET request with a forbidden URL. The server will ignore the RST ACK and send the response HTTP packet for the client's request. These packets will not trigger a Suricata reject action.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
CVSS v3 base score: 7.5
Published: 2021-12-16T05:15Z
Repository: https://github.com/OISF/suricata
Language: C
Files changed: src/decode-tcp.c, src/decode-tcp.h, src/stream-tcp.c
Commit date: 2021-11-12 15:21:07 +0100
Commit message: stream/tcp: handle RST with MD5 or AO header

Special handling for RST packets if they have an TCP MD5 or AO header option.
The options hash can't be validated. The end host might be able to validate
it, as it can have a key/password that was communicated out of band.

The sender could use this to move the TCP state to 'CLOSED', leading to
a desync of the TCP session.

This patch builds on top of
843d0b7a10bb ("stream: support RST getting lost/ignored")

It flags the receiver as having received an RST and moves the TCP state
into the CLOSED state. It then reverts this if the sender continues to
send traffic. In this case it sets the following event:

    stream-event:suspected_rst_inject;

Bug: #4710.

Diff: diff --git a/src/decode-tcp.c b/src/decode-tcp.c
index a67bfece8c..8fa8fd99ac 100644
--- a/src/decode-tcp.c
+++ b/src/decode-tcp.c
@@ -181,10 +181,30 @@ static void DecodeTCPOptions(Packet *p, const uint8_t *pkt, uint16_t pktlen)
                         }
                     } else {
                         ENGINE_SET_EVENT(p,TCP_OPT_INVALID_LEN);
                     }
                     break;
+                /* RFC 2385 MD5 option */
+                case TCP_OPT_MD5:
+                    SCLogDebug("MD5 option, len %u", olen);
+                    if (olen != 18) {
+                        ENGINE_SET_INVALID_EVENT(p,TCP_OPT_INVALID_LEN);
+                    } else {
+                        /* we can't validate the option as the key is out of band */
+                        p->tcpvars.md5_option_present = true;
+                    }
+                    break;
+                /* RFC 5925 AO option */
+                case TCP_OPT_AO:
+                    SCLogDebug("AU option, len %u", olen);
+                    if (olen < 4) {
+                        ENGINE_SET_INVALID_EVENT(p,TCP_OPT_INVALID_LEN);
+                    } else {
+                        /* we can't validate the option as the key is out of band */
+                        p->tcpvars.ao_option_present = true;
+                    }
+                    break;
             }
 
             pkt += olen;
             plen -= olen;
             tcp_opt_cnt++;
diff --git a/src/decode-tcp.h b/src/decode-tcp.h
index b5804345e6..4bcb7b22e5 100644
--- a/src/decode-tcp.h
+++ b/src/decode-tcp.h
@@ -52,10 +52,12 @@
 #define TCP_OPT_SACK                         0x05
 #define TCP_OPT_TS                           0x08
 #define TCP_OPT_TFO                          0x22   /* TCP Fast Open */
 #define TCP_OPT_EXP1                         0xfd   /* Experimental, could be TFO */
 #define TCP_OPT_EXP2                         0xfe   /* Experimental, could be TFO */
+#define TCP_OPT_MD5                          0x13   /* 19: RFC 2385 TCP MD5 option */
+#define TCP_OPT_AO                           0x1d   /* 29: RFC 5925 TCP AO option */
 
 #define TCP_OPT_SACKOK_LEN                   2
 #define TCP_OPT_WS_LEN                       3
 #define TCP_OPT_TS_LEN                       10
 #define TCP_OPT_MSS_LEN                      4
@@ -151,10 +153,12 @@ typedef struct TCPHdr_
 } __attribute__((__packed__)) TCPHdr;
 
 typedef struct TCPVars_
 {
     /* commonly used and needed opts */
+    bool md5_option_present;
+    bool ao_option_present;
     bool ts_set;
     uint32_t ts_val;    /* host-order */
     uint32_t ts_ecr;    /* host-order */
     TCPOpt sack;
     TCPOpt sackok;
diff --git a/src/stream-tcp.c b/src/stream-tcp.c
index 1239cd0ba5..311ce1ed67 100644
--- a/src/stream-tcp.c
+++ b/src/stream-tcp.c
@@ -4431,10 +4431,13 @@ static int StreamTcpPacketStateClosed(ThreadVars *tv, Packet *p,
      * see if we perhaps need to continue processing anyway. */
     if ((stream->flags & STREAMTCP_STREAM_FLAG_RST_RECV) == 0) {
         if (ostream->flags & STREAMTCP_STREAM_FLAG_RST_RECV) {
             if (StreamTcpStateDispatch(tv, p, stt, ssn, &stt->pseudo_queue, ssn->pstate) < 0)
                 return -1;
+            /* if state is still "closed", it wasn't updated by our dispatch. */
+            if (ssn->state == TCP_CLOSED)
+                ssn->state = ssn->pstate;
         }
     }
     return 0;
 }
 
@@ -5408,11 +5411,10 @@ TmEcode StreamTcpThreadDeinit(ThreadVars *tv, void *data)
  *
  */
 
 static int StreamTcpValidateRst(TcpSession *ssn, Packet *p)
 {
-
     uint8_t os_policy;
 
     if (ssn->flags & STREAMTCP_FLAG_TIMESTAMP) {
         if (!StreamTcpValidateTimestamp(ssn, p)) {
             SCReturnInt(0);
@@ -5446,10 +5448,25 @@ static int StreamTcpValidateRst(TcpSession *ssn, Packet *p)
             StreamTcpSetEvent(p, STREAM_RST_INVALID_ACK);
             SCReturnInt(0);
         }
     }
 
+    /* RFC 2385 md5 signature header or RFC 5925 TCP AO headerpresent. Since we can't
+     * validate these (requires key that is set/transfered out of band), we can't know
+     * if the RST will be accepted or rejected by the end host. We accept it, but keep
+     * tracking if the sender of it ignores it, which would be a sign of injection. */
+    if (p->tcpvars.md5_option_present || p->tcpvars.ao_option_present) {
+        TcpStream *receiver_stream;
+        if (PKT_IS_TOSERVER(p)) {
+            receiver_stream = &ssn->server;
+        } else {
+            receiver_stream = &ssn->client;
+        }
+        SCLogDebug("ssn %p: setting STREAMTCP_STREAM_FLAG_RST_RECV on receiver stream", ssn);
+        receiver_stream->flags |= STREAMTCP_STREAM_FLAG_RST_RECV;
+    }
+
     if (ssn->flags & STREAMTCP_FLAG_ASYNC) {
         if (PKT_IS_TOSERVER(p)) {
             if (SEQ_GEQ(TCP_GET_SEQ(p), ssn->client.next_seq)) {
                 SCLogDebug("ssn %p: ASYNC accept RST", ssn);
                 return 1;

Fixed code:                 /* RFC 2385 MD5 option */
                case TCP_OPT_MD5:
                    SCLogDebug("MD5 option, len %u", olen);
                    if (olen != 18) {
                        ENGINE_SET_INVALID_EVENT(p,TCP_OPT_INVALID_LEN);
                    } else {
                        /* we can't validate the option as the key is out of band */
                        p->tcpvars.md5_option_present = true;
                    }
                    break;
                /* RFC 5925 AO option */
                case TCP_OPT_AO:
                    SCLogDebug("AU option, len %u", olen);
                    if (olen < 4) {
                        ENGINE_SET_INVALID_EVENT(p,TCP_OPT_INVALID_LEN);
                    } else {
                        /* we can't validate the option as the key is out of band */
                        p->tcpvars.ao_option_present = true;
                    }
                    break;
#define TCP_OPT_MD5                          0x13   /* 19: RFC 2385 TCP MD5 option */
#define TCP_OPT_AO                           0x1d   /* 29: RFC 5925 TCP AO option */
    bool md5_option_present;
    bool ao_option_present;
            /* if state is still "closed", it wasn't updated by our dispatch. */
            if (ssn->state == TCP_CLOSED)
                ssn->state = ssn->pstate;
    /* RFC 2385 md5 signature header or RFC 5925 TCP AO headerpresent. Since we can't
     * validate these (requires key that is set/transfered out of band), we can't know
     * if the RST will be accepted or rejected by the end host. We accept it, but keep
     * tracking if the sender of it ignores it, which would be a sign of injection. */
    if (p->tcpvars.md5_option_present || p->tcpvars.ao_option_present) {
        TcpStream *receiver_stream;
        if (PKT_IS_TOSERVER(p)) {
            receiver_stream = &ssn->server;
        } else {
            receiver_stream = &ssn->client;
        }
        SCLogDebug("ssn %p: setting STREAMTCP_STREAM_FLAG_RST_RECV on receiver stream", ssn);
        receiver_stream->flags |= STREAMTCP_STREAM_FLAG_RST_RECV;
    }