A high percentage of applications we test are based around the Java technology stack - this includes core Java clients launched from the dekstop, applets, Java ME for mobile devices, J2EE web applications (spring, struts, java faces), regular JSP and all the backend components that make these technologies work (application servers, containers, databases etc).
When testing web applications or client side components that use the HTTP protocol it is trivial to intercept the traffic with a web proxy and modify / replay the requests. This type of testing is second nature to many application security consultants as it makes testing the security boundary between the client and the server easy, its efficient and the tools that support this style of testing are plentiful -
Burp Proxy is a great example of a suite of tools to support this type of security testing.
Although the testing methodology remains similar for the different technologies (for example we look at authentication, authorisation, session management, encryption, input validation) the tools and techniques to perform the assessment may differ. When testing a compiled Java client we often need to dip into the code to better understand what is going on under the hood. We may find that it does not use HTTP to communicate with the server which means tools like Burp, Paros, Webscarab are no longer what we need.
When developing a Java application the source code is compiled into
Java bytecode which is executed by the Java virtual machine. The bytecode contains extensive metadata which makes it possible to decompile the application and retrieve the original source code. During the assessment this is where the consultant may make modifications and changes to the application to achieve a desired result. For example, modifying the code to remove encryption, changing hardcoded parameters (URLs for endpoints, IP addresses, usernames, passwords, etc) and bypassing local security checks. In a number of cases it is not always possible to completely decompile the application, if heavy obfuscation is used during compile time, anonymous inner classes or maybe within the time frame of the assessment it is not feasible to "re-engineer" the Java application.
When it is not trivial to intercept the network traffic and you can not modify the source code of the Java application what is the next play? Thankfully
Aspect Security have done some great work in the area of Java instrumentation - the ability to hook into Java classes and inject "agents" during runtime. Their utility "JavaSnoop" allows you to instantly intercept method calls, class loading, inject custom Java code, modify parameters passed to methods and alter the return values - exactly what a security consultant will want to do during an assessment of a Java application.
So if you are reading this post because you searched for "hacking java applications" on google, then do not waste any more time, head over to Aspect Security, download JavaSnoop and get on with it - it should be all you need to get well under way. I am now going to quickly take a look at the two Java API's that make this possible - Attach and Instrumentation, and also a quick tutorial on how you can develop your own agents to perform custom tasks at runtime.
The Attach API allows you to attach to a Java Virtual Machine by specifying its process ID. The API provides functions to list all current running VM's and makes it trivial to attach to these. Once attached you use Java instrumentation to "inject" a Java agent into the JVM. Think of this process as hooking or intercepting method calls. What happens at this injection point is entirely up to you (or the agents developer), the instrumentation API provides a way to alter Java classes as they load.
Sample code for listing running Java VM's:
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : vms) {
System.out.println("[+] " + vmd.id() + " " + vmd.displayName());
}
Once the JVM of the process has been identified the following code can be used to attach:
vm = VirtualMachine.attach(virtualmachine);
vm.loadAgent("./RuntimeAgent.jar");
System.out.println("[+] Injected agent into process " + virtualmachine);
vm.detach();
RuntimeAgent.jar is the path to the instrumentation agent. This is where the actual modification of the Java clases occurs, the Attach API simply allows the injection into the JVM. It is important to note that it is also possible to perform instrumentation without attaching at runtime. Using the "javaagent' parameter when launching an application from the command line you can specific an agent to be injected and perform instrumentation:
java -javaagent:instrumentation/BootTimeAgent.jar -jar TargetApplication.jar
So now that we have two methods of injecting our agent into the JVM, lets take a look at how the agent is created and the subtle differences between a boot time agent and a run time agent.
Boot Time agents launched using the "javaagent" parameter has a "premain" method which initiates the class transformation:
package com.fixulate.instrument;
import java.lang.instrument.Instrumentation;
public class SimpleMain {
public static void premain(String agentArguments, Instrumentation instrumentation) {
try {
System.out.println("Running class transformer\n");
instrumentation.addTransformer(new SimpleTransformer());
} catch (Exception e) {
System.out.println("Exception: " + e);
}
}
}
SimpleTransformer is the class that makes the alterations to the compiled Java class. It takes and returns a byte array - this is where you modify the class based on your desired outcome and attack logic.
package com.fixulate.instrument;
//Core Java imports
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
// ASM bytecode modification import
import org.objectweb.asm.*;
public class SimpleTransformer implements ClassFileTransformer {
DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm");
public SimpleTransformer() {
super();
}
public byte[] transform(ClassLoader loader, String className, Class redefiningClass, ProtectionDomain domain, byte[] bytes) throws IllegalClassFormatException {
if (className.startsWith("java/") || className.startsWith("sun/") || className.startsWith("javax/") || className.startsWith("apple/") || className.startsWith("com/sun") || className.startsWith("com/apple")) {
System.out.println("Ignoring class: " + className);
return bytes;
}
else {
try {
System.out.println("[+] " + dateFormat.format(new Date()) + " - Loading class: " + className);
byte[] result = bytes;
// Create a reader for the existing bytes.
ClassReader reader = new ClassReader(bytes);
// Create a writer
ClassWriter writer = new ClassWriter(true);
// Create our class adapter, pointing to the class writer
// and then tell the reader to notify our visitor of all
// bytecode instructions
reader.accept(new PrintStatementClassAdapter(writer, className), true);
// get the result from the writer.
result = writer.toByteArray();
return result;
} catch (Exception e) {
System.out.println("Exception: " + e);
return bytes;
}
}
}
private class PrintStatementClassAdapter extends ClassAdapter {
private String className;
PrintStatementClassAdapter(ClassVisitor visitor, String theClass) {
super(visitor);
className = theClass;
}
@Override
public MethodVisitor visitMethod(int arg0, String name, String descriptor, String signature, String[] exceptions) {
try {
return new PrintStatementMethodAdapter(super.visitMethod(arg0, name, descriptor, signature, exceptions), className, name, descriptor);
} catch (Exception e) {
System.out.print("Exception: " + e);
return null;
}
}
}
private class PrintStatementMethodAdapter extends MethodAdapter {
private String methodName;
private String methodDescriptor;
private String className;
PrintStatementMethodAdapter(MethodVisitor visitor, String theClass, String name, String descriptor) {
super(visitor);
methodName = name;
methodDescriptor = descriptor;
className = theClass;
}
@Override
public void visitCode() {
try {
super.visitCode();
// load the system.out field into the stack
super.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// load the constant string we want to print into the stack
// this string is created by the values we get from ASM
super.visitLdcInsn("[+] " + dateFormat.format(new Date()) + " - Method called: " + className + "." + methodName + "\t Type: " + methodDescriptor);
// trigger the method instruction for 'println'
super.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
} catch (Exception e) {
System.out.println("Exception: " + e);
}
} //end of visitCode()
} //end of inner class
}
The technical details of how the bytecode modification works under the hood will be covered in a future blog post, in the mean time take a look at
this and
this post :-)
The Boot time agent must have a Premain-Class attribute in the Manifest file as shown below:
Manifest-Version: 1.0
Premain-Class: com.fixulate.instrument.SimpleMain
Boot-Class-Path: lib/asm-2.0.jar lib/asm-attrs-2.0.jar lib/asm-commons-2.0.jar
When using a runtime agent the premain method in the SimpleMain class should be replaced with "agentmain" and the Manifest file should contain "Agent-Class", "Boot-Class-Path" and "Can-Redefine-Classes" attribute, shown below:
Agent-Class: com.fixulate.instrument.SimpleMain
Boot-Class-Path: lib/asm-2.0.jar lib/asm-attrs-2.0.jar lib/asm-commons-2.0.jar
Can-Redefine-Classes: true
We will be shortly uploading some resources and java agents we use when conducting security assessments of Java applications. Although JavaSnoop from Aspect Security provides an excellent suite of tools there will always be the corner and edge case scenarios when you need to cut your own code to get a job done.
For further reading on the Attach API and Java Instrumentation see the following links: