Red Team Stories: The Gordian Lock

lock

One of my favorite ways to hone my security skills is listening to good OffSec stories. It’s like reading an annotated chess game. You not only see their moves but also how they make decisions, what they find surprising, what sparks their curiosity, and how they know when to dive deeper. These subtleties make things much more instructive and entertaining. Over the next several posts I’ll share a few of my own stories, hopefully leaving you with new ways of thinking about security problems and some entertaining anecdotes along the way.

Today’s story is about one of my favorite red team strategies: finding and using custom application layer bugs. This is partly out of necessity. Most environments I’ve operated in are small companies that build their own software, so I see fewer traditionally vulnerable services. They don’t have Tomcat manager exposed; they don’t export / over NFSv3; and they don’t usually have Windows domains (jk (or am I?)). Accordingly, application layer vulnerabilities in their software can represent a decent portion of the available attack surface. During one fun operation, my team and I cut through a Gordian Knot of an application to exfiltrate some very sensitive information from a hardened target.

We started with network level access but no user credentials. Our goal was to exfiltrate a specific type of sensitive customer information from our client’s network. Like all great operations, we began with recon. Through a combination of public Gitlab repositories, world readable artifactory packages, and unauthenticated wikis, we were able to find the URL of a web application responsible for managing this data. Similarly, we discovered that very few employees have access to this application. Since it would likely be difficult to obtain user credentials, we started looking for another way in.

We searched the application’s name in artifactory and found a source JAR bearing the same name. Downloading and extracting this JAR revealed a codebase that looked like a disorienting combination of this and enterprise FizzBuzz. Wading through an endless tar pit of AbstractProxyMediators and NotificationStrategyFactories, it was difficult to find any line of code containing actual business logic. Eventually, by searching for some common security bad words, we found a trivial SQL injection. It seemed like a huge win, but to our dismay, the injection was only reachable by authenticated users.

Setting this currently useless bug aside for a bit, we continued searching the codebase for bad words and came across several in a middleware function: “parse”, “auth”, and “secure”. It turns out this code was responsible for enforcing authentication. It looked something like this pseudo-python:

def require_authentication(request, next_handler):
  if not urlparse(request).path.startswith('/secure/'):
    return next_handler(request)
  
  try:
    return next_handler(authenticate(request))
  except AuthErr:
    return HTTPResponse(code=401, body='Away with you!')

Then the HTTP handlers were defined like this:

app = WebApp()
app.use(require_authentication)
app.get('/', handle_index)
app.post('/secure/home', handle_home)
app.run(port=1337)

At first glance, this check might seem reasonable, but there was something about path.startswith('/secure/') that drew our attention. We wondered if there was a way to send a request where the path did not start with /secure/ but was routed to the correct handler anyway. Since the middleware used a different URL parser than the web framework, any differences could result in this behavior. We started digging through various RFCs looking for useful obscurities when we found URL path segment parameters (section 3.3). It turns out you can specify parameters for each path segment like this: http://foo.com/foo;a=1/bar. In this case, the path is still /foo/bar and a=1 is treated as a request parameter. Amazingly, this worked: the URL parser in the framework respected path segment parameters, but the URL parser in the authentication middleware did not! This means that a request to /secure;rekt=1/home would not require a valid session because the middleware parsed the path as /secure;rekt=1/home but would be routed to the correct HTTP handler because the framework parsed the path as /secure/home. This was the first link in the chain.

Like before, we celebrated a bit too early. We figured with this auth bypass, our target data was a simple GET request away. The problem: many of these handlers needed the authenticated user’s session state and would throw exceptions with 152 line stack traces when we sent requests without a valid session cookie. Then, I remembered the SQL injection vulnerability from earlier. The authentication middleware was the only thing stopping us from exploiting it, and we had a bypass. Mercifully, the application ran the injectable code before accessing any session state, granting us a powerful vulnerability: we could append additional queries, run stored procedures, and even view the results. The only restriction was the application’s account on the database, which had fairly limited permissions. This was the second link in our chain.

We began scouring the database for our target data but came up empty. A closer look at the code revealed that our target was stored in S3. It still felt like we were close though. Our next idea was to leverage this database access to obtain a valid user session. The application authenticated users with an LDAP plugin and didn’t rely on the database for storage, so that wouldn’t work. It did store session cookies in the database, but the application kept a thorough audit trail of user activity, and we didn’t want to trigger any alerts.

Luckily, while looking through the session table, the final link in our chain fell into place. The table contained two columns. The first was a session ID stored in the cookie, and the second was a binary blob column helpfully named “session”. The first two bytes of every value in the “session” column were 0xACED, the infamous magic bytes at the beginning of a Java serialized object. If you’re not sure why this is so exciting, read this, and come back. Since we had the source JAR, we also knew most potential gadgets on the classpath. We found one that allowed us to execute code when deserialized and finally completed the exploit chain:

  • use the parser differential to bypass authentication: POST /secure;rekt=1/injectable/route

  • use the newly accessible SQL injection to insert an object that when deserialized reads and reflects the processes’s environment variables

  • send one final request with our newly minted session ID, this time to /secure/doesnt/matter, deliberately triggering the authentication middleware, which deserializes our malicious object

  • retrieve the AWS access key and secret for the S3 bucket from the reflected environment variables

  • use those to download the objects from S3

There was still one more small problem. The blobs in S3 were encrypted. Getting around this was fairly trivial though because we knew the application needed access to the plaintext data. The source contained the decryption code, and the environment variables contained the encoded key. We carefully extracted the decryption code, decrypted the blobs, and celebrated….with a hunt! (for tomorrow, we hunt).

I hope you enjoyed reading about these vulnerabilities as much as we enjoyed exploiting them. If you want to hear more of these stories, subscribe to my email list at the bottom of the page to get notified about new posts. Also, be sure to follow me on twitter @willbtlr for pithy security principles, tactics, shenanigans and more. Thanks for reading and see you next time!