RuCTFE 2019 - Household

dotnet-jwt-xxe

About

Household is a website which manages cooking recipes.

A user can register for an account, either as a cook or as a customer.

A cook can:

  • Add products
  • Import prodcuts
  • Add a dish containing a recipe
  • Add a menu

Most of that information entered can also be viewed on the website, but sometimes the site just asks to user to call the API instead.

User registration and login is done with OpenID Connect (OIDC). The website uses cookies for authorization, the API a JWT as a bearer token.

The site is written using ASP.NET Core 3.0. We are provided with

  • 2 DLLs containing the application (Household.dll and Household.Views.dll), plus a lot of DLLs with dependencies
  • A Dockerfile and a docker-compose file (which also starts a postgres database)
  • A RSA keypair (private.pem and pub.pem) which are used to sign the JWTs.

Flags

The flags were posted to the API by the bot as a recipe of a dish and as the manufacturer of a prodcut.

Using just the website, a user cannot see that information entered by another user.

Vulnerabilities

Three vulnerabilities were discovered during the competition.

Faking a JWT using the default keypair

The first vulnerability consits of the following parts:

  1. The RSA keypair deployed by default was the same for all teams and thus known to everyone.
  2. If a cook adds a new product or dish (via API or webinterface), it becomes appearent that the IDs of the objects created are just assigned incrementally. It is thus easy to enumerate all of them.
  3. Any cook can query any dish via the api (GET /api/dishes/<id>), even those created by another user. The response will not contain the recipe (which contains the flag), but the user ID of the other user (which is in the form of a UUID).

Exploitation

The path to exploiting this vulnerability is as follows:

  1. Register a new user of type cook.
  2. Add a dish and check the ID it gets assigned
  3. Access all dishes available on the system (/api/dishes/<id> for all IDs smaller then the one returned above). Collect the user ID for all dishes
  4. Edit the JWT your user got assigned by the system. Replace youe user ID with the ones collected in step 3. and resign it with the known private key
  5. Access the dishes again, this time sending the forged JWT for authorization.
  6. Search for flag in the recipe value

Unfortunately, we were not able to actually run the exploit, since already failed at step 1. The registration process was pretty complicated, since it involved running a full OIDC process before we can receive the JWT.

Fix

Fixing this was easy. We just had to create a new private key with openssl, rebuild the docker container and deploy it.

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -out pub.pem -pubout
docker-compose build
docker-compose up

Stealing the private key using XXE

The website offers an import functionality. We can upload a list of products serialized into XML format which gets parsed.

By looking into the decompiled source code of the corresponding class, we see that the XmlReaderSetting property is set to Parse instead of the default value of Prohibit. This enables a XML External Entity (XXE) vulnerability, which allows the inclusion of a file stored on the server into the XML document.

(NOTE: Another form of an XXE attack, the Billion Laughs Attack, which would have caused a DoS on the other servers, did not work, since .NETs deserializer has a builtin protection)

Exploitation

  1. Register an account of type cook
  2. Open the import function in the web interface and upload a prepared XML file which inludes the server's private key into some field of the new product.
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "/app/private.pem" >]>

<products>
    <product>
        <name>asfd</name>
        <manufacturer>&xxe;</manufacturer>
    </product>
    <product>
        <name>Drumstick leaves</name>
        <calories>64.0</calories>
        <protein>9.4</protein>
        <fat>1.4</fat>
        <carbohydrate>8.28</carbohydrate>
        <manufacturer>zoIFNpXnENYrFRD</manufacturer>
    </product>
    <product>
        <name>Bologna</name>
  1. Read the private key from the product
  2. Continue like above

We were not able to execute this exploit for the same reason as above

Fix

The correct fix would XmlReaderSettings in the import function to either prohibit or ignore. But while we were able to decompile the provided DLLs to a mostly readable source code, we failed at recompiling it into a patched version. Another try to manipulate the DLL directly using ILSpy did not work either.

We thus resorted to a dirty hack: A firewall rule was added which dropped all packages containing the words PRIVATE KEY and DOCTYPE.

Information Leakage in responses

The API is documented with swagger which is displayed if the server is run in development mode (which we failed to achieve). It allows to send different requests then those used by the frontend.

If a new dish is posted (POST /api/dish), it can contain a list of products the recipe uses. The backend then returns all recipes using one of the mentioned products, including all known information about the mentioned products (which contain flags).

Exploitation

  1. Register a new user of type cook.
  2. Add a product to learn the available product IDs
  3. Post a dish containing all available product IDs
  4. Parse the response for flags

An automation of this was not possible for us, again due to out problem with scripted registration. We tried to run this procedure manually, but ran out of time.

Fix

We were not able to fix this vulnerability, since we were still not able to deploy an altered version of the service.

It would have been neccesary to reduce the information in the answer to the POST request.

Miscaleneous

Here are some other things noteworthy:

  1. The PostgreSQL database had a standard password shared by all teams. We changed it as a defense-in-depth measure, even tough the database system was not exposed.
  2. When trying to exploit the information leakage vulnerability, we noted that an error message from the database was returned when we used a non-existing prodcut-ID: insert or update on table "Ingredient" violates foreign key constraint "FK_Ingredient_Products_ProductId"

Navigation