209 - Bad Ordering, Free OpenAI Credits, and Goodbye Passwords?
OpenAI would provide some free credits to a user once they verify their phone number, and then to prevent abuse, ensure phone numbers are unique. Its a sane plan, but Checkmarx did find a way to bypass this. Their first bypass was just to modify the request to add the country code to the number, and it worked. This expanded to just prepending 0s to the number to verify the same number multiple times.
They took this further with some fuzzing using RECollapse to look for normalization issues, and found that one could include unicode-encoded non-ascii characters in the phone number. These would be sufficent to bypass the uniquness check, but then later when sending the verification text the number it would get normalized and send without issues.
A fun little chain to get a one-click CSRF attack on a redacted domain.
It starts off with an understanding of how the application worked, subdomains could retrieve a CSRF token from
redacted.com/profile and make POST requests to
The first problem was that the
/profile endpoint that provided the CSRF token had a permissive CORS policy, any subdomain could reach it. Meaning an XSS on any subdomain could be valuable. And so the hunt moved to finding an insecure subdomain. This led jub0bs to an out of scope domain’s
/search endpoint. This page had a message handler without any origin check, and could be used to make a function call against any property (including nested properties) of the
window.APP object has an
apiCall method, which was designed to communicate with a JSONP endpoint, so that provides an arbitrary function call. And in-fact the server was permissive in what it allowed in the callback parameter. Double-URL encoding the param, it would get decoded and reflected/used exactly. Allowing for not just a function call, but arbitrary JS to be included as the
callback is a bit of a trusted parameter. There was also a quick WAF bypass, by using
& instead of
? for the first url parameter, the server still accepted it but the Akami WAF didn’t seem to recognize it.
The final hurdle was that the out of scope
/search page couldn’t be simply framed to send messages to it because of the
X-Frame-Origin header. So, it becomes a one-click attack, single click, to allow the page to open a popup, and then messages could be sent through that handle.
ANd so the whole chain puts all this together:
- Open a popup to the
- Send a message, triggering the JSONP call and XSS
- Use XSS to hit the
/profileendpoint on the main domain to steal the CSRF token
- Use the primary
If you’re unfamiliar with JSONP, it was more common in the days before CORS and was a way of allowing cross-domain communication getting around same-site policy. The basic idea is that you would include your API call as a script, and include a
Two issues came together here, the first one is the more “fun” issue in a file upload handler.
String jndiname = DES.decrypt(SanitizeParameters.sanitizeServletParamOrUrlString(request.getParameter("jndiname"))); String username = DES.decrypt(SanitizeParameters.sanitizeServletParamOrUrlString(request.getParameter("username")));
It does sanitization, and then it decrypts the data which is simply the wrong order and effectively means the sanitization is meaningless. The decrypted value is what should be sanitized. It is an interesting bug to see, if I had to guess about how it was introduced, I’d guess when they introduced the
SanitizerParameters class, they might have just done a global search and replace on the
request.getParameter replacing it with a wrapped call to sanitize it. Not really considering the context of its usage.
You end up with those parameters being unsanitized, and the
username is used in creating the file path. So if you can encrypt a valid string for it, you can get a easy directory traversal.
The second issue is one we see all too often, static keys were used for the cryptography here. So an attacker could recreate the encryption routine, reuse the static keys and generate a malicious payload.