Show Notes

191 - Param Pollution in Golang, OpenEMR, and CRLF Injection

A couple interesting issues in OpenEMR leading to unauthenticated remote code execution and file disclosure.

The file disclosure bug starts from the fact that the installer/setup functionality can still be triggered after installation. A complete reinstall is not possible, but it is possible to execute some specific methods of the setup process.

The setup.php page will create a new Installer object filled with the configuration information from $_REQUEST. It will then execute some method based on the current $_POST['state']. State will lead to the displayNewThemeDiv method being executed, this method will make a MySQL query using the configuration information derived from $_REQUEST. So an attacker can query a server they control. A rogue MySQL server can potentially read files from the client side of the connection. This is to support a LOAD DATA query with a LOCAL INFILE modifier. By default this is not enabled but as OpenEMR makes legitimate use of the functionality it should be enabled on the PHP server, so the MySQL server can make requests for files of the client.

The second set of issues abuses some existing functionality (a file upload) along with a reflected XSS and LFI due to path traversal for code execution. The reflected XSS is fairly straightforward but also interesting:

<a onclick="dopopup('<?php echo $_SERVER['REQUEST_URI'] . '&display=fullscreen&encounter=' . $encounter; ?>');"
href="JavaScript:void(0);"></a>

You can see that is reflects the REQUEST_URI into the onclick handler, normally any ' or " would be urlencoded in this and so no escape possible, but the author notes that you can include HTML entities within the onclick handler that will be decoded by the time the JavaScript inside is processed. So one can escape the inner single-quoted string by including an &apos; and then inject their JavaScript.

The directory traversal and local file include is rather traditional. It includes a file based on a $_GET paramater and appends .plugin.php to it. Without checking for traversals its possible to load the file from other locations, and combined with the file upload functionality (the post does not detail the intented purpose of the file upload) one can upload a PHP file with the correct name and have it be included.

There are a few issues in this post, the first is SQL injection with nothing very special going on. The later issues though are more of a bypass of application logic which I think is fairly cool.

First, the simple SQL injection, the Feathers web framework assumes Sequelize would only accept valid column names in an argument, whereas Sequelize actually treats that argument as trusted and does no escaping. Allowing for a trivial SQL injection.

The Feathers framework is largely intended for creating APIs, you can define a service and corresponding models. And it’ll create an API you can use to query/update/delete the data. It depends heavily on teh Sequelize ORM to actually craft the database queries, for the most part passing in the URL request object directly to Sequelize to turn into a query.

Which is where things start to go wrong, Feathers will pass in the request query object to Sequelize’s getWhereConditions function. As the name implies it generates the WHERE clause of a query. Sequelize as an ORM will fallback in some cases to simply generating a 1=1 as the where clause. Makes sense in teh context of an ORM that an empty clause is probably just a query for everything.

In the context of a Feathers application though, this isn’t a safe assumption. The application may have earlier logic to enforce certain conditions on the query like only querying for data owned by the logged in user. These would typically be enforced with before-hook functions that will modify the request object to add in the constraints.

If an attacker could somehow get the getWhereConditions function to fall into one of the 1=1 conditions it would be possible to bypass the application layer logic. Which is exactly what the authors were able to find.

Under normal circumstances Feathers would pass in a plain object to Sequelize, simply containing key-value pairs, and in this case it would act as you’d expect. The authors found that if one could pass an [] (empty array) instead of an object to the function it would fall back to using 1=1 even if a before-hook did something like query.userId = <some value>. The assignment would be allowed by JavaScript, but accessing the variable it would still look like an empty array. (Cool trick!).

Of course, they still actually need to be able to provide an empty array as the query object, using the standard web interface this couldn’t done, but Feathers also exposes a socket.io interface. This interface allowed sending an empty array as the request object. They could also abuse Socket.IO’s attachment functionality to provide other values that would fall through to the 1=1 case.

A desync between the parameter the authorization check reads, and the value the actual action reads. Leading to an attacker being able to access resources that would have been denied normally.

In Concourse CI, thecheckAuthoriztionhandler will read the a team name value from the URL explicitly using a call to:

r.URL.Query().Get(":team_name")

On its own this is fine, is presumably does the appropiate authorization checks here to ensure the user is allowed to access this team and perform whatever action they are attempting. Later on, in the pipelineScopedHandler function the team name is parsed again. This time using:

teamName := r.FormValue(":team_name")

In this case, the :team_name parameter in the POST or PUT body (if any) would be given priority over the one in the URL. An attacker could abuse this difference to provide a team name they are authorized to access in teh URL, while accessing a different team’s pipelines in the actual handler.

The vulnerability here isn’t too interesting, just a case of user-input being reflected into a header without sanitizing new-lines (CrLf injection). What is interesting is how they leverage this header injection primitive to bypass Akamai’s web application firewall.

First, Akamai would block any \r\n in the request URL so \n was used instead. Using just a newline does not conform to the HTTP spec and may not work in all cases but many HTTP processors as reasonably permissive in parsing. In just using \n they could still follow the typical technique of injecting a \n\n<XSS payload here> for their XSS payload.

In that approach they also need to contend with the Akamai WAF on the XSS payload. Rather than trying to come up with a payload that would bypass the WAF, the Praetorian researcher leveraged the header injection and injected a Content-Encoding: gzip. Allowing them to provide a compressed payload that would not be properly processed by the WAF but when reflected back as the body of the response would be decompressed by the browser.

They also had to inject Content-Type and Content-Length headers with appropriate values so the response would be treated as HTML, and any extra data send after the payload would be ignored.

Couple vulnerabilities here, the first bad regex allowing for the origin validation on cross-origin messages to be bypass. The second is a pair of innerHTML assignments with data from a cross-origin message.

The first issue was in one of the several regex patterns used to validate the origin of a cross-origin message. The vulnerable regex pattern was: ^https://cosmos-db-dataexplorer-germanycentral.azurewebsites.de$. If you’ve followed our weekly spot the vulns you might recognize this bug pattern. The unescaped . characters in regex will match any character, not just the literal . character. An attacker could register one of many domains and be able to bypass the filter.

With the ability to send cross-origin messages that would pass the validation, cross-site scripting was fairly easy. A chart would be insecurely generated based on the data of the message, and the CORS header allowed for inline scripts.

chartTitleElement.innerHTML = data.chartSettings.chartTitle;
/* ... */
noDataMessageContent.innerHTML = data.errorMessage;
/* ... */
document.getElementById(Heatmap.elementId)!.appendChild(chartTitleElement);
document.getElementById(Heatmap.elementId)!.appendChild(noDataMessageElement);