Gadget Inspector – a tool to find Java gadget chains for exploitation
Tại hội nghị Black Hat 2018, Ian Haken (JackOfMostTrades) đã giới thiệu một tool cho phép automation việc tìm gadget chain trong các thư viện hoặc classpath mà một Java application sử dụng. Trong blog này thì mình sẽ đi tìm hiểu cách hoạt động của tool nó như nào :P
Setup
Env: WSL Ubuntu 22.04.5 LTS, OpenJDK 11.0.24
Analysis method: Static code analysis
(Lưu ý: các bạn nên sử dụng VM hoặc cài dual boot hơn là dùng WSL)
Bản gốc của tool thì có thể được tải về ở link này, tuy nhiên bằng một cách nào đó thì lúc clone về setup thì máy mình báo lỗi như ảnh dưới, vì thế nên mình có cài một bản thay thế ở đây:
Đầu tiên clone về, cd vào và chạy lệnh sau là xong:
./gradlew shadowJar
Review tool
Để chạy tool thì khá dễ, câu lệnh mà ta dùng ở đây là:
java -jar build/libs/gadget-inspector-all.jar <args>
Trong đó, args cho phép ta chọn 1 file .war hoặc nhiều file .jar để inspect. Thử chạy tool trên với commons-collections 3.1:
List dir hiện tại ra thì thấy có thêm một số file mới được tạo ra, trong đó có gadget-chains.txt. Nội dung của file này chứa 4 gadget chain như trong log ở console:
Ở mỗi chain, entrypoint của gadget chính là class.method được ghi ở dòng trên cùng, còn dòng dưới cùng là sink. Với Commons-Collections 3.1 thì chain ở trên cùng trong result chính là chain mà frohoff có sử dụng trong ysoserial (link). Vậy là xong phần test tool.
Analyze tool
Một số đặc điểm của tool:
Tool không tìm vulnerabilities, nó chỉ tìm ra các chain đã biết hoặc các chain có điểm tương đồng với các chain đã biết, từ đó hỗ trợ exploit hoặc tìm ra chain mới
Vì có áp dụng một vài giả định (assumption) để đánh giá các class nên tool có thể trả về false positives
Tool phân tích các classpath bằng bytecode analysis, hữu ích trong trường hợp ta chỉ có file .jar, .war hoặc .class
Chain của tool generate ra khá tổng quát nên không dùng được trực tiếp, mà cần phải tự phân tích kỹ hơn
Đến với phần source code, main class của tool là class GadgetInspector. Dựa vào việc phân tích method main của class này, ta có thể nhận ra flow hoạt động của tool như sau:
Đầu tiên tool check xem các file data cũ có tồn tại không, nếu có thì xóa luôn. Vì thế khi dùng tool thì nên save data cũ để tránh phiền phức, hoặc là dùng thêm tham số là --resume
Sau đó là đến phần chính, gồm các bước như sau:
Đầu tiên thực hiện quá trình Method Discovery bằng cách spawn một instance của class MethodDiscovery rồi gọi lên method discover() để list ra toàn bộ method của class:
Kế đó thực hiện quá trình phân tích dataflow của từng class (tức là bước này kiểm tra mối quan hệ giữa parameters truyền vào và return value). Ở đây người ta dùng method discover() của class PassthroughDiscovery:
Nếu thử trace kỹ hơn thì config được dùng ở đây là "jserial", lấy ra từ một list các config có sẵn:
Tạo call graph. Ở bước này thì tool tạo ra biểu đồ biểu thị cách mà các method trong class gọi nhau và gọi các method từ các parameter là class instance như thế nào. Khá là giống bước 2:
Xác định các method được xem là possible source bằng cách tự serialize rồi deserialize, thông qua call discover() từ interface GIConfig. Riêng method này có đến 3 implementations nên mình sẽ phân tích sâu hơn ở phần phía dưới blog:
Duyệt qua các possible source cho đến khi gặp sink, bằng cách dựng đồ thị và thực hiện BFS trên đồ thị đến khi gặp sink (là leaf)
Và đó là những gì mà Gadget Inspector làm. Tiếp theo mình cùng phân tích kỹ hơn từng step trong workflow của tool.
Debug
Method Discovery
Điều đầu tiên cần lầm khi tìm chain là phải biết tất cả các method của class(es). Ở phần này, tool spawn ra một instance của class MethodDiscovery. Phân tích code của class này:
public class MethodDiscovery {
private static final Logger LOGGER = LoggerFactory.getLogger(MethodDiscovery.class);
private final List<ClassReference> discoveredClasses = new ArrayList<>();
private final List<MethodReference> discoveredMethods = new ArrayList<>();
public void save() throws IOException {
...
}
public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception {
for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
try (InputStream in = classResource.getInputStream()) {
ClassReader cr = new ClassReader(in);
try {
cr.accept(new MethodDiscoveryClassVisitor(), ClassReader.EXPAND_FRAMES);
} catch (Exception e) {
LOGGER.error("Exception analyzing: " + classResource.getName(), e);
}
}
}
}
private class MethodDiscoveryClassVisitor extends ClassVisitor {
...
}
public static void main(String[] args) throws Exception {
...
}
}
Ở method discover(), nó nhận parameter là một ClassResourceEnumerator. Class này chứa một ClassLoader, khi gọi đến method getAllClass() thì nó trả về một Collection chứa các ClassResource:
public class ClassResourceEnumerator {
private final ClassLoader classLoader;
public ClassResourceEnumerator(ClassLoader classLoader) throws IOException {
this.classLoader = classLoader;
}
public Collection<ClassResource> getAllClasses() throws IOException {
Collection<ClassResource> result = new ArrayList<>(getRuntimeClasses());
for (ClassPath.ClassInfo classInfo : ClassPath.from(classLoader).getAllClasses()) {
result.add(new ClassLoaderClassResource(classLoader, classInfo.getResourceName()));
}
return result;
}
...
Ở tại vòng for của method discover() phía trên, nó thực hiện duyệt qua tất cả các ClassResource được trả về từ getAllClasses() của ClassResourceEnumerator, sau đó thực hiện gọi getInputStream() để tạo một input stream đến ClassResource.
public class ClassResourceEnumerator {
private final ClassLoader classLoader;
public ClassResourceEnumerator(ClassLoader classLoader) throws IOException {
this.classLoader = classLoader;
}
public Collection<ClassResource> getAllClasses() throws IOException {
...
}
private Collection<ClassResource> getRuntimeClasses() throws IOException {
...
}
public static interface ClassResource {
public InputStream getInputStream() throws IOException;
public String getName();
}
private static class PathClassResource implements ClassResource {
private final Path path;
private PathClassResource(Path path) {
this.path = path;
}
@Override
public InputStream getInputStream() throws IOException {
return Files.newInputStream(path);
}
@Override
public String getName() {
return path.toString();
}
}
private static class ClassLoaderClassResource implements ClassResource {
private final ClassLoader classLoader;
private final String resourceName;
private ClassLoaderClassResource(ClassLoader classLoader, String resourceName) {
this.classLoader = classLoader;
this.resourceName = resourceName;
}
@Override
public InputStream getInputStream() throws IOException {
return classLoader.getResourceAsStream(resourceName);
}
@Override
public String getName() {
return resourceName;
}
}
}
Với mỗi input stream được tạo ra với mỗi ClassResource, discover() thực hiện đưa vào ClassReader để parse bytecode, sau đó thực hiện call tới MethodDiscoveryClassVisitor(). Method này thực chất được extends từ org.objectweb.asm.ClassVisitor (dùng để phân tích bytecode của Java class, xem ở ClassVisitor (ASM 9.7.1)), có override một số method của chính ClassVisitor:
private class MethodDiscoveryClassVisitor extends ClassVisitor {
private String name;
private String superName;
private String[] interfaces;
boolean isInterface;
private List<ClassReference.Member> members;
private ClassReference.Handle classHandle;
private MethodDiscoveryClassVisitor() {
super(Opcodes.ASM9);
}
@Override
public void visit ( int version, int access, String name, String signature, String superName, String[]interfaces)
{
this.name = name;
this.superName = superName;
this.interfaces = interfaces;
this.isInterface = (access & Opcodes.ACC_INTERFACE) != 0;
this.members = new ArrayList<>();
this.classHandle = new ClassReference.Handle(name);
super.visit(version, access, name, signature, superName, interfaces);
}
public FieldVisitor visitField(int access, String name, String desc,
String signature, Object value) {
if ((access & Opcodes.ACC_STATIC) == 0) {
Type type = Type.getType(desc);
String typeName;
if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY) {
typeName = type.getInternalName();
} else {
typeName = type.getDescriptor();
}
members.add(new ClassReference.Member(name, access, new ClassReference.Handle(typeName)));
}
return super.visitField(access, name, desc, signature, value);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
boolean isStatic = (access & Opcodes.ACC_STATIC) != 0;
discoveredMethods.add(new MethodReference(
classHandle,
name,
desc,
isStatic));
return super.visitMethod(access, name, desc, signature, exceptions);
}
@Override
public void visitEnd() {
ClassReference classReference = new ClassReference(
name,
superName,
interfaces,
isInterface,
members.toArray(new ClassReference.Member[members.size()]));
discoveredClasses.add(classReference);
super.visitEnd();
}
}
Sau khi thực hiện xong việc visit đến methods của các class, kết quả được lưu vào các file classes.dat và methods.dat. Cuối cùng, tool xác định mối quan hệ kế thừa giữa các class thông qua method MethodDiscovery().save():
public void save() throws IOException {
DataLoader.saveData(Paths.get("classes.dat"), new ClassReference.Factory(), discoveredClasses);
DataLoader.saveData(Paths.get("methods.dat"), new MethodReference.Factory(), discoveredMethods);
Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();
for (ClassReference clazz : discoveredClasses) {
classMap.put(clazz.getHandle(), clazz);
}
InheritanceDeriver.derive(classMap).save();
}
Ở đây, tool thực hiện lưu từng class vào một HashMap instance. Key là một ClassReference.Handle object, có nhiệm vụ truy xuất tên, hashcode và kiểm tra sự bằng nhau của hai object handle (thông qua override method equal()), value là một ClassReference với chức năng lưu trữ mọi thông tin về class đó. Cuối cùng, InheritanceDeriver.derive(classMap) được gọi lên để truy xuất thông tin về tất cả các parent của các class bên trong HashMap instance ở trên.
Cùng quay trở lại với ví dụ với commons-collections-3.1 ở đầu blog, ta cùng đi vào phân tích hai file này.
Ở classes.dat, mình chọn ra dòng của file:
com/sun/crypto/provider/AESCrypt com/sun/crypto/provider/SymmetricCipher com/sun/crypto/provider/AESConstants false ROUNDS_12!2!Z!ROUNDS_14!2!Z!sessionK!2![[I!K!2![I!lastKey!2![B!limit!2!I
Với entrypoint này, ta có thể diễn giải thành như sau:
Class name | Class cha | Interface | Có là interface không? | Các member trong class |
com/sun/crypto/provider/AESCrypt | com/sun/crypto/provider/SymmetricCipher | com/sun/crypto/provider/AESConstants | false | ROUNDS_12!2!Z!ROUNDS_14!2!Z!sessionK!2![[I!K!2![I!lastKey!2![B!limit!2!I |
Class name: com/sun/crypto/provider/AESCrypt
Class cha: com/sun/crypto/provider/SymmetricCipher, Java chỉ cho phép đơn kế thừa nên mỗi class con chỉ có 1 class cha duy nhất
Interface: com/sun/crypto/provider/AESConstants
Có phải là interface hay không: false (rõ ràng là AESCrypt không phải interface)
Class members: ROUNDS_12, ROUNDS_14, sessionK, K, lastKey, limit
Ở methods.dat:
com/sun/crypto/provider/AESCipher engineGetParameters ()Ljava/security/AlgorithmParameters; false
com/sun/crypto/provider/AESCipher <init> (I)V false
Class name | Method name | (Parameters)Return type | Static method? |
com/sun/crypto/provider/AESCipher | engineGetParameters | ()Ljava/security/AlgorithmParameters; | false |
com/sun/crypto/provider/AESCipher | <init> (tức construtor) | (I)V | false |
Class name: com/sun/crypto/provider/AESCipher
Method name:
- engineGetParameters
- <init>
(Parameters)Return type:
- ()Ljava/security/AlgorithmParameters; (ở đây () là no param, “L“ có nghĩa là object, return type là java/security/AlgorithmParameters)
- (I)V (ở đây (I) là integer, return type là void
(Vì sao “L” lại kí hiệu cho object thì là do serialization rule, các bạn có thể đọc thêm ở đây: Java Object Serialization Specification: 6 - Object Serialization Stream Protocol)
- Static method: false (tất nhiên là không phải static method)
Dựa vào 2 file result trên thì tool tạo lập ra mối quan hệ thừa kế và lưu vào inheritanceMap.dat. Xem thử 1 dòng trong file này:
java/rmi/ConnectIOException java/lang/Exception java/lang/Object java/io/Serializable java/io/IOException java/lang/Throwable
Class name | Các class/interface mà nó kế thừa |
java/rmi/ConnectIOException | java/lang/Exception, java/lang/Object, java/io/Serializable java/io/IOException, java/lang/Throwable |
Lưu ý rằng, “Các class/interface mà nó kế thừa“ ở đây không mang ý nghĩa java/rmi/ConnectIOException extends trực tiếp vào các class được liệt kê trên. Như đã nói, mỗi class trong Java chỉ thừa kế trực tiếp từ 1 class cha, ở đây ConnectIOException extends từ RemoteException, RemoteException lại thừa kế từ IOException, IOException thừa kế từ Exception, … Vì thế, các class liệt kê ở trên mang ý nghĩa là mỗi class là class con của 1 class khác, và ConnectIOException là class con thừa kế cuối cùng trong chuỗi thừa kế.
Ngoài ra, để lí giải cho việc tại sao tool không liệt kê RemoteException, thì mình (đoán) là do các class này chỉ có nhiệm vụ gọi lên constructor của class IOException mà nó kế thừa chứ không có thêm chức năng gì khác biệt:
Passthrough discovery
Ở bước này, tool thực hiện kiểm tra mối quan hệ giữa parameters đầu vào và return value của mỗi method. Result ở bước này sẽ được dùng vào việc tạo call graph.
Khi thực hiện discover, đầu tiên tool thực hiện việc load methods, classes và inheritance map từ các file .dat tương ứng. Sau khi load xong, method discoverMethodCalls() được gọi lên:
public class PassthroughDiscovery {
...
public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();
InheritanceMap inheritanceMap = InheritanceMap.load();
Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);
...
}
Tại discoverMethodCalls(), các class lại được đưa vào các input stream thông qua classResource.getInputStream(). Sau khi read class từ các input stream này, method thực hiện init instance MethodCallDiscoveryClassVisitor(). Sau khi xong việc, một Map có các key là class name và value là các class resource được trả về.
private Map<String, ClassResourceEnumerator.ClassResource> discoverMethodCalls(final ClassResourceEnumerator classResourceEnumerator) throws IOException {
Map<String, ClassResourceEnumerator.ClassResource> classResourcesByName = new HashMap<>();
for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
try (InputStream in = classResource.getInputStream()) {
ClassReader cr = new ClassReader(in);
try {
MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM9);
cr.accept(visitor, ClassReader.EXPAND_FRAMES);
classResourcesByName.put(visitor.getName(), classResource);
} catch (Exception e) {
LOGGER.error("Error analyzing: " + classResource.getName(), e);
}
}
}
return classResourcesByName;
}
Tại dòng cr.accept(visitor, ClassReader.EXPAND_FRAMES); ở phía trên, method này nhận visitor làm tham số, trace kỹ hơn thì accept() thực hiện call đến this.readMethod:
readMethod thực hiện call đến visitMethod:
Vì visitMethod() đã bị override ở khai báo của class MethodCallDiscoveryClassVisitor nên nó thực hiện instantiate một MethodCallDiscoveryMethodVisitor object:
Trong method này lại thực hiện việc put các method tìm được vào trong object methodCalls (là một Map, lát sau mình sẽ chỉ ra cách nó được sử dụng):
Sau khi xong bước tìm ra các method call, topologicallySortMethodCalls() được gọi lên:
public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
...
List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();
passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods,
config.getSerializableDecider(methodMap, inheritanceMap));
}
Ở hàm này thì nó có thực hiện một số giải thuật trên đồ thị. Qua phân tích code và tìm hiểu trên mạng thì nó thực hiện giải thuật topologically sort, thứ chỉ xuất hiện ở các đồ thị dạng Directed Acyclic Graph (DAG). Đồ thị này là đồ thị có hướng, tuy nhiên không có vòng (nếu A có đường đi đến B thì không được có đường từ B đến A), và mỗi đỉnh chỉ xuất hiện 1 lần. Ví dụ:
Mình sẽ không đi chi tiết vào cách mà giải thuật đồ thị hoạt động ở đây, tại vì nó khá là khủng khiếp 😅 Tuy nhiên có 1 số ý chính cần nắm:
Danh sách methodCalls (được tạo ra lúc call discoverMethodCalls() như đã giải thích bên trên) sẽ được đem đi sort để tạo thành 1 đồ thị có hướng, biểu thị cho method call chains.
Nếu method call chain là A → B → C → D thì return của giải thuật đồ thị ở đây sẽ là D → C → B → A. Tại vì ta đang cần phân tích mối quan hệ giữa parameter truyền vào method với return value của nó, thì với ví dụ như sau:
public class lmao { public boolean funcB(Integer a) { Integer b = 100; return a == b; } public boolean funcA(Integer test) { return funcB(test); } }
Để đánh giá được parameter “test” có ảnh hưởng thế nào đến return value của funcA() thì ta cần phân tích funcB() trước. Vì vậy, giải thuật sắp xếp method call bên trên cần phải trả về funcB → funcA.
Có một điểm cần chú ý là, đồ thị DAG không được phép có vòng, tuy nhiên method call thì tất nhiên sẽ có vòng lặp, kiểu đệ quy chẳng hạn, nếu không xử lí thì sẽ xảy ra dead loop. Vậy làm sao để giải quyết vấn đề này? Thì tại method dfsTsort() có sử dụng 1 Set chứa các node đã được visit, nếu node đã visit thì dừng duyệt:
Lúc này lại có vấn đề khác xảy ra: giả sử có 2 chain khác nhau là A → B → C và A → B → D, thì do B đã được đánh là visited nên chain A → B → D sẽ không được kiểm tra. Khi này ta có thể bị miss một số gadget chain hữu ích. Đây chính là điểm hạn chế của tool.
Cuối cùng, calculatePassthroughDataflow() được call lên để duyệt qua danh sách sortedMethods và kiểm tra data flow của các method, thông qua phân tích bytecode với API từ org.objectweb.asm:
public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
...
passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods,
config.getSerializableDecider(methodMap, inheritanceMap));
}
private static Map<MethodReference.Handle, Set<Integer>> calculatePassthroughDataflow(Map<String, ClassResourceEnumerator.ClassResource> classResourceByName,
Map<ClassReference.Handle, ClassReference> classMap,
InheritanceMap inheritanceMap,
List<MethodReference.Handle> sortedMethods,
SerializableDecider serializableDecider) throws IOException {
final Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = new ConcurrentHashMap<>();
ExecutorService ex = Executors.newFixedThreadPool(4);
for (MethodReference.Handle method : sortedMethods) {
if (method.getName().equals("<clinit>")) {
continue;
}
ex.execute(() -> {
ClassResourceEnumerator.ClassResource classResource = classResourceByName.get(method.getClassReference().getName());
try (InputStream inputStream = classResource.getInputStream()) {
ClassReader cr = new ClassReader(inputStream);
try {
PassthroughDataflowClassVisitor cv = new PassthroughDataflowClassVisitor(classMap, inheritanceMap,
passthroughDataflow, serializableDecider, Opcodes.ASM9, method);
cr.accept(cv, ClassReader.EXPAND_FRAMES);
passthroughDataflow.put(method, cv.getReturnTaint());
} catch (Exception e) {
LOGGER.error("Exception analyzing " + method.getClassReference().getName(), e);
}
} catch (IOException e) {
LOGGER.error("Unable to analyze " + method.getClassReference().getName(), e);
}
});
}
ex.shutdown();
try {
if (!ex.awaitTermination(300, TimeUnit.SECONDS)) {
ex.shutdownNow();
}
} catch (InterruptedException e) {
ex.shutdownNow();
}
return passthroughDataflow;
}
Ở đây, serializableDecider được lấy từ config (init ở đầu chương trình). Interface này có 3 implementation:
Đối với mỗi config:
Nếu là JavaDeserializationConfig: chọn ra các class là subclass của java/io/Serializable
Nếu là JacksonDeserializationConfig: bỏ qua các constructor hoặc no parameter method:
Nếu là XstreamDeserializationConfig: bỏ qua các class có tên không thể dùng để làm một XML tag:
Giờ cùng thử phân tích một entry trong passthrough.dat:
java/util/concurrent/ConcurrentSkipListMap$SubMap tailMap (Ljava/lang/Object;)Ljava/util/concurrent/ConcurrentSkipListMap$SubMap; 0,1,
Ta có thể diễn giải các field thành như sau:
Class name | Method name | (Parameters)Return value | ? |
java/util/concurrent/ConcurrentSkipListMap$SubMap | tailMap | (Ljava/lang/Object;)Ljava/util/concurrent/ConcurrentSkipListMap$SubMap; | 0,1, |
3 field đầu thì mình không phân tích gì thêm nữa, nhưng field cuối thì khá lạ nên mình chỉ biết để là ?. Sau khi tham khảo một số nơi và slide gốc ở Black Hat 2018 thì mình nhận ra là số 0, 1 có ý nghĩa như sau:
Nếu return value sử dụng biến cục bộ của func thì đánh số là 0
Nếu return value sử dụng tham số đầu vào thì return index của tham số đó
Phân tích entry trên:
Vì newSubMap() là local method của class SubMap, và trong return value của tailMap() có sử dụng param “fromKey” (tại vị trí 1 của tailMap()) nên entry này được đánh số là 0, 1.
Tạo callgraph
Bước này cũng tương tự bước trước đó, nhưng thay vì phân tích mối quan hệ giữa param và return value trong 1 method thì tool lại phân tích mối quan hệ giữa param và child method call được thực hiện từ bên trong method đó. Ví dụ 1 entry từ callgraph.dat:
java/lang/invoke/VarHandleBytes$FieldInstanceReadWrite weakCompareAndSetPlain (Ljava/lang/invoke/VarHandleBytes$FieldInstanceReadWrite;Ljava/lang/Object;BB)Z java/lang/Class cast (Ljava/lang/Object;)Ljava/lang/Object; 1 1
Parent class name | Parent method | Parent method (Parameters)Return value | Child method’s class | Child method | Child method (Parameters)Return value | ? | ?? |
java/lang/invoke/VarHandleBytes$FieldInstanceReadWrite | weakCompareAndSetPlain | (Ljava/lang/invoke/VarHandleBytes$FieldInstanceReadWrite;Ljava/lang/Object;BB)Z | java/lang/Class | cast | (Ljava/lang/Object;)Ljava/lang/Object; | 1 | 1 |
Method này call đến handle.receiverType.cast() với handle là param thứ 1 của weakCompareAndSetPlain(). Ở method cast() thì holder được truyền vào param cho cast(), và nó cũng là param thứ 1 của chính method này:
Vì thế nên entry này được đánh 1 1 trong callgraph.
Tìm các method có thể dùng làm source của chain
Bước này tìm ra các source dựa trên các sink đã biết. Ở abstract method discover() có implement hai cách để tìm source là Jackson và Simple method:
Jackson method tìm source bằng cách tìm constructor hoặc getter/setter method:
Đối với Simple method thì thực hiện tìm các source phổ biến như là finalize, readObject, invoke, hashCode, equals, call. Có thể thấy số lượng các source khá hạn chế:
public void discover(Map<ClassReference.Handle, ClassReference> classMap,
Map<MethodReference.Handle, MethodReference> methodMap,
InheritanceMap inheritanceMap) {
final SerializableDecider serializableDecider = new SimpleSerializableDecider(inheritanceMap);
for (MethodReference.Handle method : methodMap.keySet()) {
if (Boolean.TRUE.equals(serializableDecider.apply(method.getClassReference()))) {
if (method.getName().equals("finalize") && method.getDesc().equals("()V")) {
addDiscoveredSource(new Source(method, 0));
}
}
}
// If a class implements readObject, the ObjectInputStream passed in is considered tainted
for (MethodReference.Handle method : methodMap.keySet()) {
if (Boolean.TRUE.equals(serializableDecider.apply(method.getClassReference()))) {
if (method.getName().equals("readObject") && method.getDesc().equals("(Ljava/io/ObjectInputStream;)V")) {
addDiscoveredSource(new Source(method, 1));
}
}
}
// Using the proxy trick, anything extending serializable and invocation handler is tainted.
for (ClassReference.Handle clazz : classMap.keySet()) {
if (Boolean.TRUE.equals(serializableDecider.apply(clazz))
&& inheritanceMap.isSubclassOf(clazz, new ClassReference.Handle("java/lang/reflect/InvocationHandler"))) {
MethodReference.Handle method = new MethodReference.Handle(
clazz, "invoke", "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;");
addDiscoveredSource(new Source(method, 0));
}
}
// hashCode() or equals() are accessible entry points using standard tricks of putting those objects
// into a HashMap.
for (MethodReference.Handle method : methodMap.keySet()) {
if (Boolean.TRUE.equals(serializableDecider.apply(method.getClassReference()))) {
if (method.getName().equals("hashCode") && method.getDesc().equals("()I")) {
addDiscoveredSource(new Source(method, 0));
}
if (method.getName().equals("equals") && method.getDesc().equals("(Ljava/lang/Object;)Z")) {
addDiscoveredSource(new Source(method, 0));
addDiscoveredSource(new Source(method, 1));
}
}
}
// Using a comparator proxy, we can jump into the call() / doCall() method of any groovy Closure and all the
// args are tainted.
// https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Groovy1.java
for (MethodReference.Handle method : methodMap.keySet()) {
if (Boolean.TRUE.equals(serializableDecider.apply(method.getClassReference()))
&& inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("groovy/lang/Closure"))
&& (method.getName().equals("call") || method.getName().equals("doCall"))) {
addDiscoveredSource(new Source(method, 0));
Type[] methodArgs = Type.getArgumentTypes(method.getDesc());
for (int i = 0; i < methodArgs.length; i++) {
addDiscoveredSource(new Source(method, i + 1));
}
}
}
}
Cùng xem 1 entry trong sources.dat:
javax/security/auth/kerberos/KeyImpl readObject (Ljava/io/ObjectInputStream;)V 1
Entry này có thể được phân tích thành:
Class name | Method name | (Parameters)Return type | Index of used param |
javax/security/auth/kerberos/KeyImpl | readObject | (Ljava/io/ObjectInputStream;)V | 1 |
Dựng gadget chain hoàn chỉnh
Ở bước này, đầu tiên tool load toàn bộ sources và callgraph đã dựng được ở các bước trước đó. exploredMethods là một Set object được tạo ra để track các method đã được kiểm tra nhằm tránh duyệt lặp (và cũng khiến cho nhiều chain bị bỏ sót):
Sau khi load xong thì thực hiện duyệt toàn bộ sources. Theo như slide của tác giả thì ở đây tool thực hiện duyệt BFS trên call graph để hình thành nên chain. Vậy thì nó BFS như nào? Trước tiên, biến lastLink được tạo để đánh dấu method hiện đang được kiểm tra, đồng thời method đó được pop khỏi chain:
Tiếp theo, lấy ra method được call bởi lastLink bằng graphCallMap.get() (nói cách khác, tìm các method mà laskLink gọi lên). Nếu như vị trí param của caller khác với vị trí param của lastLink thì bỏ qua. Tại sao? Thì lấy ví dụ đơn giản như sau:
public class lmao {
public boolean funcC(Integer x, Integer y) {
return x.equals(y);
}
public boolean funcB(Integer a) {
Integer b = 100;
return a == b;
}
public boolean funcA(Integer a) {
return funcC(5, a);
}
}
Giả sử ta đang phân tích chain funcA → funcB. Ở funcA ta thấy có call funcC với param a nằm ở vị trí số 2. Tuy nhiên khi phân tích sang funcB thì lại thấy funcB chỉ có param duy nhất ở vị trí số 1. Vì hai index khác nhau nên chain funcA → funcB là không thể xảy ra.
Kế đó, mỗi method lại có thể có nhiều implementation, nên cần lấy ra tất cả các impl ấy (potential next link):
Cuối cùng, kiểm tra xem method có phải sink không. Bên trong method isSink() có sẵn khá nhiều sink phổ biến, tuy nhiên danh sách này có vẻ chưa đủ (có thể add thêm custom sink):
private Optional<GadgetChain.Type> isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) {
if (method.getClassReference().getName().equals("java/io/FileInputStream")
&& method.getName().equals("<init>")) {
return Optional.of(GadgetChain.Type.FILE);
}
if (method.getClassReference().getName().equals("java/io/FileOutputStream")
&& method.getName().equals("<init>")) {
return Optional.of(GadgetChain.Type.FILE);
}
if (method.getClassReference().getName().equals("java/nio/file/Files")
&& (method.getName().equals("newInputStream")
|| method.getName().equals("newOutputStream")
|| method.getName().equals("newBufferedReader")
|| method.getName().equals("newBufferedWriter"))) {
return Optional.of(GadgetChain.Type.FILE);
}
if (method.getClassReference().getName().equals("java/lang/Runtime")
&& method.getName().equals("exec")) {
return Optional.of(GadgetChain.Type.RCE);
}
/*
if (method.getClassReference().getName().equals("java/lang/Class")
&& method.getName().equals("forName")) {
return Optional.of(GadgetChain.Type.UNKNOWN);
}
if (method.getClassReference().getName().equals("java/lang/Class")
&& method.getName().equals("getMethod")) {
return Optional.of(GadgetChain.Type.UNKNOWN);
}
*/
// If we can invoke an arbitrary method, that's probably interesting (though this doesn't assert that we
// can control its arguments). Conversely, if we can control the arguments to an invocation but not what
// method is being invoked, we don't mark that as interesting.
if (method.getClassReference().getName().equals("java/lang/reflect/Method")
&& method.getName().equals("invoke") && argIndex == 0) {
return Optional.of(GadgetChain.Type.INVOKE);
}
if (method.getClassReference().getName().equals("java/net/URLClassLoader")
&& method.getName().equals("newInstance")) {
return Optional.of(GadgetChain.Type.NET);
}
if (method.getClassReference().getName().equals("java/lang/System")
&& method.getName().equals("exit")) {
return Optional.of(GadgetChain.Type.UNKNOWN);
}
if (method.getClassReference().getName().equals("java/lang/Shutdown")
&& method.getName().equals("exit")) {
return Optional.of(GadgetChain.Type.UNKNOWN);
}
if (method.getClassReference().getName().equals("java/lang/Runtime")
&& method.getName().equals("exit")) {
return Optional.of(GadgetChain.Type.UNKNOWN);
}
if (method.getClassReference().getName().equals("java/nio/file/Files")
&& method.getName().equals("newOutputStream")) {
return Optional.of(GadgetChain.Type.FILE);
}
if (method.getClassReference().getName().equals("java/lang/ProcessBuilder")
&& method.getName().equals("<init>") && argIndex > 0) {
return Optional.of(GadgetChain.Type.RCE);
}
if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("java/lang/ClassLoader"))
&& method.getName().equals("<init>")) {
return Optional.of(GadgetChain.Type.INVOKE);
}
if (method.getClassReference().getName().equals("java/net/URL") && method.getName().equals("openStream")) {
return Optional.of(GadgetChain.Type.NET);
}
// Some groovy-specific sinks
if (method.getClassReference().getName().equals("org/codehaus/groovy/runtime/InvokerHelper")
&& method.getName().equals("invokeMethod") && argIndex == 1) {
return Optional.of(GadgetChain.Type.INVOKE);
}
if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("groovy/lang/MetaClass"))
&& Arrays.asList("invokeMethod", "invokeConstructor", "invokeStaticMethod").contains(method.getName())) {
return Optional.of(GadgetChain.Type.INVOKE);
}
// This jython-specific sink effectively results in RCE
if (method.getClassReference().getName().equals("org/python/core/PyCode") && method.getName().equals("call")) {
return Optional.of(GadgetChain.Type.RCE);
}
return Optional.ofNullable(null);
}
Sau khi xác nhận được rằng method là sink thì add chain vào discoveredGadgets.
Một gadget chain đã được tìm ra sẽ có dạng như sau:
End
Và đó là tất cả những gì mình hiểu về gadgetinspector. Có thể sẽ có sai sót nên nếu các bạn tìm ra hãy contact mình nhé ^^ Discord: NoSpaceAvailable
Subscribe to my newsletter
Read articles from Le Quoc Cuong directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by