CVE-2022-39311 (Java, published 2022-10-14T20:15Z)
Description: value: GoCD is a continuous delivery server. GoCD helps you automate and streamline the build-test-release cycle for continuous delivery of your product. GoCD versions prior to 21.1.0 are vulnerable to remote code execution on the server from a malicious or compromised agent. The Spring RemoteInvocation endpoint exposed agent communication and allowed deserialization of arbitrary java objects, as well as subsequent remote code execution. Exploitation requires agent-level authentication, thus an attacker would need to either compromise an existing agent, its network communication or register a new agent to practically exploit this vulnerability. This issue is fixed in GoCD version 21.1.0. There are currently no known workarounds.
Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-502 \u2014 Deserialization of Untrusted Data
The product deserializes untrusted data without sufficiently verifying that the resulting data will be valid.
Repository: https://github.com/gocd/gocd
Files changed:
server/src/main/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporter.java
server/src/test-fast/java/com/thoughtworks/go/remote/AgentRemoteInvokerServiceExporterTest.java
Commit message: Disable RMI endpoint by default

This endpoint can be enabled by including `gocd.agent.remoting.legacy=true` in the
`-Dgocd.agent.extra.properties` property for the server process.


Vulnerable code:
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}), target);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("nonexistent", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}), target);
    private static AgentRemoteInvokerServiceExporter deserializingWith(final RemoteInvocation invocation, final BuildRepositoryMessageProducer proxy) {
        final AgentRemoteInvokerServiceExporter invoker = new AgentRemoteInvokerServiceExporter() {
                return proxy;

Fixed code:
import com.thoughtworks.go.util.SystemEnvironment;
import static com.thoughtworks.go.util.SystemEnvironment.AGENT_EXTRA_PROPERTIES;
import static javax.servlet.http.HttpServletResponse.*;
    private final SystemEnvironment env;

    public AgentRemoteInvokerServiceExporter() {
        this(new SystemEnvironment());
    }

    public AgentRemoteInvokerServiceExporter(SystemEnvironment env) {
        this.env = env;
    }

        if (rejectRMI()) {
            // yes, ideally, this should be short-circuited in the agent auth filter, but keeping this logic here has
            // some advantages:
            //   - it keeps all deprecated RMI logic in one place so it's easier to remove (just remove this class)
            //   - it's 100% reliable by virtue of its proximity to the RMI invocation code and can't be thwarted by
            //     some clever URI encoding to circumvent the uri path test that we would need to write at the filter
            //     level in order to selectively apply this logic to the RMI endpoint and not the JSON API endpoint
            reject(response, SC_GONE, "This RMI endpoint is disabled.");
            return;
        }

    private boolean rejectRMI() {
        final String props = env.get(AGENT_EXTRA_PROPERTIES).toLowerCase();
        return !Arrays.asList(props.split("\\s+")).contains("gocd.agent.remoting.legacy=true");
    }

import com.thoughtworks.go.util.SystemEnvironment;
import static com.thoughtworks.go.util.SystemEnvironment.AGENT_EXTRA_PROPERTIES;
    @Mock
    private SystemEnvironment env;

        when(env.get(AGENT_EXTRA_PROPERTIES)).thenReturn("gocd.agent.remoting.legacy=true");
    @Test
    void rejectsWhenLegacyDisabled() throws Exception {
        when(env.get(AGENT_EXTRA_PROPERTIES)).thenReturn("");
        final AgentRuntimeInfo agent = runtimeInfo(AGENT_UUID);
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        invoker.handleRequest(req, res);
        verify(target, never()).ping(agent);
        assertEquals(SC_GONE, res.getStatus());
    }

        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("isIgnored", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class}, new Object[]{agent, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("ping", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getWork", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("getCookie", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCurrentStatus", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobState.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleting", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("reportCompleted", new Class[]{AgentRuntimeInfo.class, JobIdentifier.class, JobResult.class}, new Object[]{agent, null, null}));
        final AgentRemoteInvokerServiceExporter invoker = deserializingWith(new RemoteInvocation("nonexistent", new Class[]{AgentRuntimeInfo.class}, new Object[]{agent}));
    private AgentRemoteInvokerServiceExporter deserializingWith(final RemoteInvocation invocation) {
        final AgentRemoteInvokerServiceExporter invoker = new AgentRemoteInvokerServiceExporter(env) {
                return target;