Introduction#

Most security professionals are very familiar with the pitfalls of default configurations. However, the larger Open Source Software community may not. Recently, I have been researching the nature of these configurations and specifically how it relates to the self-hosting community.

This journey started when I was looking for a replacement to the app I used to track my vehicle's fuel economy/expenses. I found a self-hosted solution that appeared to suit my needs. Given my security background it was important that I vet anything that I was considering putting on my server. The application in question is called "Hammond", and is available on Github. The backend is written in Go, a language I am not super familiar with, but know enough to be dangerous. The readme lists a few ways to start the server. The first option is to run the server using a docker command.

docker run -d -p 3000:3000 --name=hammond akhilrex/hammond

This command doesn't set a persistent volume or perform any additional configuration. I found this to be suspicious.

Upon starting the docker container and loading the webpage at localhost:3000, I completed the setup by registering a the first user.

Examining the Application#

After completing the setup, I began to explore the various API endpoints. As a side note, this app is a Vue based app, and is therefore heavily reliant on the API. I noted the presence of a JSON Web Token (JWT) in the requests.

JWT Primer#

For the uninitiated, JWT's are considered a type of bearer token. Typically, a user of a website or API will supply a username/password or other authentication material and receive a token in exchange. The JWT has three parts: header, payload, and signature.

The header defines the type of signature used. The purpose of the signature is to allow the server to validate the token has not been modified. The two most common types are RS256, and HS256.

RS256 is based on a public/private key pair. RS256 can be vulnerable to something called "key confusion" where the attacker supplies a token that is signed with the public key instead of the private key.

HS256 is a symetric key encryption. HS256 keys may be cracked if a weak secret value is chosen.

Unlike a normal cookie or session ID, JWT's are not tracked by the server. The validity of the token is controlled by a payload key/value pair (often exp) The token will also include some sort of user ID and other identifying characteristics (account type, user role).

The bottom line is that the attacker can forge tokens once they gain access to the signing key,

Searching for the key#

Since the JWT signing key isn't something I defined, the signing key has to be one of the following options:

  1. Defined at random upon startup (good)
  2. Static value defined in the docker images (bad)
  3. Static value defined in the application source code (bad)
  4. Empty/null value (very bad)

I should mention that there are additional configuration options. Users could optionally use the docker-compose file, or run directly on the host.

In the docker-compose file, there is a definition for the JWT signing key.

image

This indicates the base application expects to pull the signing key from the environment variable JWT_SECRET. My next logical step was to test the app and see which signing key is being used when run from the previously listed docker command.

The environment variable is not set when the basic docker command is used to build the container.

Obtain the Token#

The application issues the JWT after successfully authenticating. This token is then included in all subsequent requests to the API.

image

Here the JWT is shown, and the response in the following screenshot.

image

Decoding the JWT obtained reveals the following structure:

image

Attempting to crack the key using the recovered value from the docker-compose file failed to return a valid result.

image

This prompted me to investigate the docker container a bit to determine why that didn't work.

After attaching to the docker container, I checked the value of env.

image

Note the absence of the JWT_SECRET value. Grepping for such value around the container's file system returned no results. This led me to wonder if the signing key might be null/empty.

I confirmed this by using the following command:

echo "" > test_key
python jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTQ0MzIwNTIsImlkIjoiNzZiOGIxN2EtMjkwNS00MTRlLWFiODQtNzA3MzExNmM0ZDMwIiwicm9sZSI6MH0.eoGqM4di44SJ5NIKqfiho8UVLiZ9V5o_wJ8dIE2OnY4 -C -d test_key

image

The next step was to attempt to forge a cookie using the blank signature.

python jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MTQ0MzIwNTIsImlkIjoiNzZiOGIxN2EtMjkwNS00MTRlLWFiODQtNzA3MzExNmM0ZDMwIiwicm9sZSI6MH0.eoGqM4di44SJ5NIKqfiho8UVLiZ9V5o_wJ8dIE2OnY4 -T -p "" -S hs256

image

Submitting this tampered token to the API endpoint returns the list of users.

image

Risk and Impact#

For this specific application, the risk is what I would classify as medium. This is due to the relatively small user base. The Github repository shows approximately 500 stars. Based on data gathered from Shodan, there are roughly 10 instances of this application being hosted on the public internet with default configurations. I base that conclusion on the fact that all of these instances are running on the default port 3000. When the docker-compose file is used, the app is hosted by default on port, but with a more explicitly defined JWT key.

To guage the impact, I reviewed the data accessible in the application. The /api/users endpoint returns the registered user's email addresses. This information leakage is probably the most consequential. With these forged tokens, it is possible to escalate to gain the "admin" role. Under normal circumstances, this would likely be considered more impactful, but the data stored in this sytem is relatively benign. For example, the time and dates of fuelups, cost, etc. There is a risk of the data being modified or deleted, however, my feeling is that a threat actor is more likely to be interested in pivoting to some other service with more interesting data.

Root Cause#

I want to be clear that I don't consider this to be a vulnerability in the code itself, nor do I really blame the developer. I do feel that more could have been done to explicitly state the importance of choosing a secure value for the JWT signing key. The bottom line is that user's are likely to follow the instructions as written and fail to modifiy the JWT signing key. Ultimitely, it is the responsibility of the administrator to ensure they fully read and understand the documentation provided by developers prior to deployment.

Remediation#

For self-hosting enthusiasts:

  1. Carefully review documentation to ensure that the application is deployed using a secure configuration.

  2. ensure a cryptographically secure value for the JWT signing key is set in the environment variables or config files.

    openssl rand 60 | base64 -w 0
    
  3. Keep apps behind a VPN.

For developers:

  1. Ensure that software documentation explicitly states the importance for setting the JWT signing key.
  2. Don't set static values in config files. Always set dynamic/random values for cryptographic material

References#