Show Notes

125 - Facebook Exploits, pfSense RCE, and MySQLjs SQLi

Abusing an otherwise secure call to shell_execallows users to control part of the sed commands leading to code execution. One thing of note is that this is the FreeBSD version of sed which differs from the more common GNU version in that it doesn’t include the commands to directly execute commands. So instead the author used the s (s/search/replace/) and w (w output_location.txt) commands to replace part of the normal output with some PHP code to cretae a simple web-shell and write it to the web-root.

Permissive parsing strikes again, MySQLjs by would accept objects as values for a parameterized query with a somewhat surprising default behaviour. The key issue here though is that MySQLjs exposes an interface entirely like prepared statements, but is actually crafting the query on the client side rather than using server-side prepared statements.

Query:            SELECT * FROM users WHERE ?
Parameter Value:  {"password": "example"}
Results in:       SELECT * FROM users WHERE `password` = "example"

In this example it kinda makes sense, passing in an object for a more complex replacement, but it also supports that replace when you would expect a simple value to be injected.

Query:            SELECT * FROM example WHERE some_column = ?
Parameter Value:  {"some_column": 1}
Results in:       SELECT * FROM example WHERE some_column = `some_column` = 1

Its this second example, that is likely to occur, yet might introduce a vulnerability. Looking at that WHERE condition this breaks down into two comparisions some_column = some_column which will resolve to true since both are pointing to the same column, that is then compared with 1 which is a “truthy” value.

Maxwell Dulin encounted this within his blog’s authentication system:

async viewUserInfo(user,password){
		//prepared statements prevent SQL injections
		var query = "SELECT * FROM Users where username = ? AND password = ?";
		const [rows, fields]  = await pool.query(query, [user,password]);
		return rows;
}

For this code, the user value is passed in directly, but the password will be hashed first, so it cannot be an unexpected object, but the user can be. Which is exactly what the attack was.

{
  "username": {
    "username" : 1
  },
  "password":"<some_password>"
}

The query in this case will resolve to:

SELECT * From Users WHERE username = `username` = 1 AND password = <some-hash>

Leading to the first condition (username) being true for all rows in the database, and then as long as one user has the password the second condition will also be true and the login will proceed.

Two Facebook Canvas issues enabling an attacker application to get privileged first-party API keys by pretending to be Instagram or another first-party application.

The issues are reasonably easy, but they do require a bit of understanding regarding how Facebook Canvas applications work.Simplifying things quite a bit, communication with Facebook happens over postMessages from an iframe.

In a normal oauth flow the iframe would send a showDialog message, which would cause Facebook Canvas to go through a relatively normal oauth flow. One key thing is that the redirect_uri, because all communicate happens through a bit of a proxy (the arbiter) the URI would point to the facebook arbiter and the origin of the request would be added to the fragment (ex: https://www.facebook.com/dialog/return/arbiter#origin=https://example.com).

When the redirect happens, the Arbiter would send the code to the iframe restricting it to that origin. This means that an application might be able to tricker the arbiter into starting an oauth flow as a first party application but unless the https://...#origin=https://attacker.com was in the redirect whitelist they wouldn’t get the oauth token from the arbiter.

Race Condition

With that in mind, the origin that was added could also be registered through another postMessage (XdArbiter/register message type). When doing so, Facebook Canvas would first send a request to /platform/app_owned_url_check/ to ensure that the app_id actually owned the origin it was trying to register. Which is where we come across our first vulnerability.

First, one would try to register a new origin of fbconnect://success. This is an origin that all first-party applications whitelist. Then trigger the oauth dialog specifying a first-party application id.

While the oauth is happening, send another postMessage to register/change your origin, this time back to the attacker controlled origin. Now if this happened before the oauth flow finished, when the token comes back, the arbiter will send it to the registered origin, even though it is different from the origin that it used to make the request.

Fix Bypass

Part of the first for the race condition was to prevent an application from changing the application id away from the current id. This didn’t account for the encrypted_query_string parameter, which could be used to provide parameters that were not verified because they were encrypted. Combined with a parsing issue where proving a param[0] would cause param itself to be revoked/empty. An attacker could revoke the original app_id and redirect_uri parameters and have them parsed out of the encrypted_query_string parameter which wasn’t validated.

A lot of this post, as the title indicates goes into the difficulties in determining the real client-ip in a modern envrionment where reverse proxies are quite common and normal. Pointing out some common issues:

  • Headers are untrustworthy. Unless a proxy strips the original value instead of appending to it, an attacker can set the first value in the X-Forwarded-For` (XFF) list to whatever they want, even non-IPs.
  • Multiple Headers. The same header may be present multiple times, and according to the HTTP/1.1 spec, if the value of the header is a comma-separated list, it must be possible to combine multiple header fields into one. This can be a problem if you only look at the first instance of a header (as is default in Golang for example using http.Header.Get(name)), as the IP should have been appended to the last header not the first.
  • Private IPs, if using an internal proxy, a privatae IP might end up in the header.
  • Inconsistent Splitting, the HTTP/1.1 spec indicates that there can be command-separated lists in headers, but some code may look for a command and a space (", ").
  • Unencrypted data is untrustworthy. If you’re not using HTTPS the header may have been tampered with at any point
  • Other Headers, like nginx’s X-Client-IP, or CloudFlare’s True-Client-IP might be present and trusted in generic code, but not actually provided by a trusted proxy.

After laying all these issues out (in better detail that I have here) the author goes into how to do the parsing correctly, and then points out several instances of real parsing issues, that may or may not have security implications depending on the applications running behind the improperly implemented proxies/middleware.