Skip to content

Tổng quan về công cụ Java Deserialization Gadgetinspector

Bài viết dưới đây được dịch lại từ bản tiếng anh “Java Deserialization Tool Gadgetinspector First Glimpse” của tác giả Longofo@Knownsec 404 Team 

Nguồn:https://medium.com/@knownsec404team/java-deserialization-tool-gadgetinspector-first-glimpse-74e99e493649

Mở đầu

Lần đầu tiên tôi tìm hiểu về công cụ này thông qua @Badcode, được trình bày tại Black Hat 2018. Công cụ này thực hiện  static analysis các bytecodes bằng việc sử dụng các trick đã biết để tự động tìm deserialization chain từ Source đến Sink. Tôi đã xem video bài phát biểuPPT của tác giả trên Black Hat khá nhiều lần để biết thêm thông tin về nguyên tắc hoạt động của công cụ này, nhưng một số chỗ thực sự khó hiểu. Tác giả đã public mã nguồn mở của công cụ này, nhưng không cung cấp tài liệu chi tiết và có rất ít bài viết phân tích về công cụ này. Tôi đã đọc một bài viết về công cụ này của Ping An Group. Từ mô tả của bài viết cho thấy họ có một sự hiểu biết nhất định về công cụ này và đã có một số cải tiến, nhưng lại không giải thích quá nhiều chi tiết. Sau đó, tôi đã cố gắng debug gần như làm rõ nguyên tắc làm việc của nó. Sau đây là quá trình tôi phân tích, tìm hiểu công cụ này, cũng như ý tưởng về công việc và cải tiến trong tương lai của tôi

Giới thiệu về công cụ

  • Công cụ này không sử dụng để tìm lỗ hổng. Thay vào đó, nó sử dụng các trick source->…->sink  đã biết hoặc các tính năng tương tự của nó để tìm các chain hoặc một nhánh của chain
  • Công cụ này dùng để tìm kiếm call chain trong đường dẫn tới các lớp (classpath) của toàn bộ ứng dụng.
  • Công cụ này thực thi một số nguy hiểm chấp nhận được (stain judgment, taint transfer,…)
  • Công cụ này có thể tạo ra false positives và not false negatives
  • Công cụ này dựa trên việc bytecode analysis. Đối với các ứng dụng Java, nhiều lần chúng tôi không có mã nguồn, chỉ có gói War, gói Jar hoặc các class file.
  • Công cụ này không tạo ra Payload có thể được sử dụng luôn được, với các cấu trúc đặc biêt vẫn cần thực hiện một số thao tác thủ công

Serialization and Deserialization

Serialization là quá trình chuyển đổi trạng thái thông tin của một đối tượng thành một hình thức có thể được lưu trữ hoặc truyền đi. Các thông tin chuyển đổi có thể được lưu trữ trên một đĩa. Trong quá trình truyền qua mạng, nó có thể ở dạng byte, XML, JSON, v.v … Quá trình ngược lại của việc khôi phục thông tin theo byte, XML, JSON, v.v. thành các đối tượng được gọi là Deserialization.

Trong Java, object serialization và deserialization được sử dụng rộng rãi trong RMI và network transmission

Các thư viện Serialization và Deserialization trong Java

  • JDK(ObjectInputStream)
  • XStream(XML,JSON)
  • Jackson(XML,JSON)
  • Genson(JSON)
  • JSON-IO(JSON)
  • FlexSON(JSON)
  • Fastjson(JSON)

Mỗi một thư viện deserialization khác nhau sẽ có cách thức deserialize khác nhau. Các “magic methods” khác nhau sẽ được gọi, và các phương thức được tự động gọi này được sử dụng làm deserialization entry point (source). Nếu các phương thức được gọi tự động này lại gọi các phương thức con khác, thì phương thức con này cũng có thể được sử dụng làm source, giống như việc biết phần đầu của call chain, và bắt đầu bằng các phương thức con để đi tìm các nhánh. Các phương thức có thể gây nguy hiểm (sink) có thể được tìm thấy qua nhiều lớp gọi

  • ObjectInputStream

Ví dụ, một lớp thực hiện  implements Serializable interface, sau đó “ObjectInputStream.readobject” sẽ tự động tìm các phương thức “readObject”, “readResolve” và các phương thức khác của lớp khi deserialize.

Ví dụ, một lớp  implements Externalizable interface, sau đó “ObjectInputStream.readobject” sẽ tự động tìm các phương thức “readExternal” và các phương thức khác của lớp khi deserialize

  • Jackson

Khi “ObjectMapper.readValue” deserialize một lớp, nó sẽ tự động tìm ra hàm constructor không có đối số của lớp  deserialization, constructor có chứa tham số loại cơ sở, setter,getter của thuộc tính, …

Trong phân tích tiếp theo, tôi sử dụng ObjectInputStream của riêng JDK làm ví dụ.

Điều khiển kiểu dữ liệu => Điều khiển Code

Tác giả nói rằng trong lỗ hổng deserialization, nếu chúng ta kiểm soát kiểu dữ liệu, chúng ta sẽ kiểm soát hướng đi của code. Nó có nghĩa là gì? Theo sự hiểu biết của tôi, tôi đã viết ví dụ sau:

public class TestDeserialization {   

interface Animal {
        public void eat();
    }    public static class Cat implements Animal,Serializable {
        @Override
        public void eat() {
            System.out.println("cat eat fish");
        }
    }   

public static class Dog implements Animal,Serializable {
        @Override
        public void eat() {
            try {
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {
static class Person implements Serializable {
        private Animal pet;        public Person(Animal pet){
            this.pet = pet;
        }        private void readObject(java.io.ObjectInputStream stream)
                throws IOException, ClassNotFoundException {
            pet = (Animal) stream.readObject();
            pet.eat();
        }
    }  

public static void GeneratePayload(Object instance, String file)
            throws Exception {
        //Serialize the constructed payload and write it to the file
        File f = new File(file);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
        out.flush();
        out.close();
    }   

public static void payloadTest(String file) throws Exception {
        //Read the written payload and deserialize it
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        Object obj = in.readObject();
        System.out.println(obj);
        in.close();
    }   

public static void main(String[] args) throws Exception {
        Animal animal = new Dog();
        Person person = new Person(animal);
        GeneratePayload(person,"test.ser");
        payloadTest("test.ser");//        Animal animal = new Cat();
//        Person person = new Person(animal);
//        GeneratePayload(person,"test.ser");
//        payloadTest("test.ser");
    }
}

Để thuận tiện, tôi viết tất cả các lớp trong một lớp để kiểm tra. Trong lớp Person, có một thuộc tính pet của lớp Animal, đó là interface giữa lớp Cat và lớp Dog. Trong serialization, chúng ta có thể kiểm soát xem thuộc tính pet của lớp Person là đối tượng Cat hay đối tượng Dog, do đó, trong quá trình deserialization, hướng đi của pet.eat() trong readObject là khác nhau. Nếu pet là một đối tượng lớp Cat, nó sẽ không gây ra nguy hiểm gì, nhưng nếu pet là một đối tượng lớp Dog, nó sẽ đi đến thực thi Runtime.getRuntime().Exec("calc");

Mặc dù đôi khi, thuộc tính của lớp đã được gán một đối tượng cụ thể khi khai báo, nó vẫn có thể được sửa đổi bằng  Reflection trong Java như sau:

public class TestDeserialization {  

interface Animal {
        public void eat();
    }    public static class Cat implements Animal, Serializable {
        @Override
        public void eat() {
            System.out.println("cat eat fish");
        }                           
    }   

 public static class Dog implements Animal, Serializable {
        @Override
        public void eat() {
            try {
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("dog eat bone");
        }
    }    

public static class Person implements Serializable {
        private Animal pet = new Cat();        private void readObject(java.io.ObjectInputStream stream)
                throws IOException, ClassNotFoundException {
            pet = (Animal) stream.readObject();
            pet.eat();
        }
    }   

 public static void GeneratePayload(Object instance, String file)
            throws Exception {
        //Serialize the constructed payload and write it to the file
        File f = new File(file);
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
        out.flush();
        out.close();
    }   

 public static void payloadTest(String file) throws Exception {
        //Read the written payload and deserialize it
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        Object obj = in.readObject();
        System.out.println(obj);
        in.close();
    }    

public static void main(String[] args) throws Exception {
        Animal animal = new Dog();
        Person person = new Person();        //Modify private properties by reflection
        Field field = person.getClass().getDeclaredField("pet");
        field.setAccessible(true);
        field.set(person, animal);        GeneratePayload(person, "test.ser");
        payloadTest("test.ser");
    }
}

Trong lớp Person, bạn có thể gán giá trị cho thuộc tính pet thông qua phương thức khởi tạo (constructor) hoặc phương thức setter hoặc các phương thức khác. Thuộc tính đã được định nghĩa là một đối tượng của lớp Cat khi được khai báo, nhưng sử dụng Reflection có thể sửa đổi thành đối tượng của lớp Dog, vì vậy khi deserialization, nó vẫn đi đến thực thi các đoạn code nguy hiểm.

Đây chỉ là quan điểm của riêng tôi đối với tác giả: “Nếu bạn kiểm soát  được dữ liệu, thì bạn điều khiển được code”. Trong lỗ hổng Java deserialization, nhiều trường hợp là sử dụng tính chất đa hình của Java để kiểm soát hướng đi của code và cuối cùng đạt được mục đích tấn công.

Magic Method

Trong ví dụ trên, chúng ta có thể thấy rằng phương thức readobject của lớp Person không được gọi thủ công khi deserialize. Nó được gọi tự động khi ObjectInputStream deserialize đối tượng. Tác giả gọi các phương thức tự động gọi này là “Magic method”.

Một số magic methods phổ biến khi deserializing với ObjectInputStream:

  • Object.readObject()
  • Object.readResolve()
  • Object.finalize()

Một số lớp serializable JDK  implement các phương thức trên và cũng tự động gọi các phương thức khác mà có thể được sử dụng làm entrypoints:

  • HashMap
  • Object.hashCode()
  • Object.equals()
  • PriorityQueue
  • Comparator.compare()
  • Comparable.CompareTo()

Sinks

  • Runtime.exec(), cách đơn giản nhất và rõ ràng để thực thi các lệnh trực tiếp tại môi trường cần tấn công
  • Method.invoke(), yêu cầu lựa chọn đúng các phương thức và tham số, thực thi các phương thức Java thông qua “Java Reflection”
  • RMI/JNDI/JRMP, v.v., hiệu quả một cách gián tiếp việc thực thi mã tùy ý bằng cách tham chiếu các đối tượng từ xa

Tác giả đưa ra một ví dụ từ Magic method (source) -> Gadget Chains -> Runtime.exe (sink):

Lớp HashMap ở trên  implements “Magic method” của readObject và gọi phương thức hashCode. Một số lớp implements phương thức equals để so sánh sự tương đương giữa các đối tượng (nói chung các phương thức equalshashCode được triển khai đồng thời). Có thể thấy từ hình vẽ rằng, AbstractTableModel$ff19274a thực hiện phương thức hashCode, gọi phương thức f.invoke, f là đối tượng của lớp IFn và f có thể được lấy bởi thuộc tính __clojureFnMap. IFn là một interface. Như đã đề cập ở trên, nếu kiểm soát được kiểu dữ liệu, thì đẽ điều khiển được hướng đi của code. Vì vậy, nếu chúng ta đặt một đối tượng của lớp FnCompose của interface IFn trong __clojureFnMap trong quá trình serialization, chúng ta có thể điều khiển phương thức f.invoke để kiểm soát phương thức FnCompose.invoke, sau đó điều khiển f1 và f2 trong FnCompose.invoke. FnConstant để đi tới FnEval.invoke (đối với f.invoke trong AbstractTableModel$ff19274a.hashcode, thì interface IFn nào sẽ được chọn? Qua thử nghiệm và phân tích nguyên tắc quyết định, mức độ ưu tiên sẽ là yếu tố quyết định. Đường dẫn ngắn là FnEval.invoke, đây là lý do tại sao cần sự can thiệp thủ công trong các phân tích ở những phần sau của bài viết).

Bây giờ, chúng ta chỉ cần tìm entrypoint đã triggered chain này. Payload sử dụng định dạng JSON như sau:

{
     "@class":"java.util.HashMap",
     "members":[
               2,
               {    
                    "@class":"AbstractTableModel$ff19274a",
                    "__clojureFnMap":{
                    "hashcode":{
                    "@class":"FnCompose",
                    "f1":{"@class","FnConstant",value:"calc"},
                    "f2":{"@class":"FnEval"}
                    }
               }
          }
     ]
}

Cách làm việc của Gadgetinspector

Như tác giả đã nói, chúng ta chỉ mất đúng năm bước:

// Enumerate all classes and all methods of the class
if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat"))
|| !Files.exists(Paths.get("inheritanceMap.dat"))) {
LOGGER.info("Running method discovery...");
MethodDiscovery methodDiscovery = new MethodDiscovery();
methodDiscovery.discover(classResourceEnumerator);
methodDiscovery.save();
}
//Generate passthrough data flow
if (!Files.exists(Paths.get("passthrough.dat"))) {
LOGGER.info("Analyzing methods for passthrough dataflow...");
PassthroughDiscovery passthroughDiscovery = new PassthroughDiscovery();
passthroughDiscovery.discover(classResourceEnumerator, config);
passthroughDiscovery.save();
}
//Generate passthrough call graph
if (!Files.exists(Paths.get("callgraph.dat"))) {
LOGGER.info("Analyzing methods in order to build a call graph...");
CallGraphDiscovery callGraphDiscovery = new CallGraphDiscovery();
callGraphDiscovery.discover(classResourceEnumerator, config);
callGraphDiscovery.save();
}
//Search for available sources
if (!Files.exists(Paths.get("sources.dat"))) {
LOGGER.info("Discovering gadget chain source methods...");
SourceDiscovery sourceDiscovery = config.getSourceDiscovery();
sourceDiscovery.discover();
sourceDiscovery.save();
}
//Search generation call chain
{
LOGGER.info("Searching call graph for gadget chains...");
GadgetChainDiscovery gadgetChainDiscovery = new GadgetChainDiscovery(config);
gadgetChainDiscovery.discover();
}

Bước 1: Liệt kê tất cả các lớp và tất cả các phương thức của mỗi lớp

Để thực hiện tìm kiếm call chain, trước tiên bạn phải có thông tin về tất cả các lớp và tất cả các phương thức lớp

public class MethodDiscovery {    
private static final Logger LOGGER = LoggerFactory.getLogger(MethodDiscovery.class);    
private final List<ClassReference> discoveredClasses = new ArrayList<>();//Save all class information
private final List<MethodReference> discoveredMethods = new ArrayList<>();//Save all methods information
    ...
    ...
    public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception {
        //classResourceEnumerator.getAllClasses() gets all the classes at runtime (JDK rt.jar) and all classes in the application to be searched
        for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
            try (InputStream in = classResource.getInputStream()) {
                ClassReader cr = new ClassReader(in);
                try {
                    cr.accept(new MethodDiscoveryClassVisitor(), ClassReader.EXPAND_FRAMES);//Save the method information to discoveredMethods by manipulating the bytecode through the ASM framework and saving the class information to this.discoveredClasses
                } catch (Exception e) {
                    LOGGER.error("Exception analyzing: " + classResource.getName(), e);
                }
            }
        }
    }
    ...
    ...
    public void save() throws IOException {
        DataLoader.saveData(Paths.get("classes.dat"), new ClassReference.Factory(), discoveredClasses);//Save class information to classes.dat
        DataLoader.saveData(Paths.get("methods.dat"), new MethodReference.Factory(), discoveredMethods);//Save method information to methods.dat        Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();
        for (ClassReference clazz : discoveredClasses) {
            classMap.put(clazz.getHandle(), clazz);
        }
        InheritanceDeriver.derive(classMap).save();//Find all inheritance relationships and save
    }

Hãy xem class.dat và method.dat có những gì:

classes.dat

class nameParent class nameAll interfacesIs interface?membercom/sun/deploy/jardiff/JarDiffPatcherjava/lang/Objectcom/sun/deploy/jardiff/JarDiffConstants,com/sun/deploy/jardiff/PatcherfalsenewBytes!2![Bcom/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImplcom/sun/corba/se/spi/orbutil/proxy/CompositeInvocationHandlerImplcom/sun/corba/se/spi/orbutil/proxy/LinkedInvocationHandler,java/io/Serializablefalsestub!130!com/sun/corba/se/spi/presentation/rmi/DynamicStub!this$0!4112!com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl

Lớp đầu tiên: com/sun/deploy/jardiff/JarDiffPatcher

Tương ứng với thông tin bảng ở trên, ta thấy rằng:

  • Tên lớp:com/sun/deploy/jardiff/JarDiffPatcher
  • Lớp cha: java/lang/Object,Nếu một lớp không kế thừa các lớp khác, thì mặc định sẽ hoàn toàn kế thừa java /lang/Object và Java không cho phép nhiều kế thừa, vì vậy mỗi lớp chỉ có một lớp cha.
  • Tất cả các interface:com/sun/deploy/jardiff/JarDiffConstants,

com/sun/deploy/jardiff/Patcher

  • Đây có là interface:false
  • Thành viên:newBytes!2![B, newBytes member, Byte type

Lớp thứ hai

com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl

Tương ứng với thông tin ở bảng trên, ta thấy rằng:

  • Tên lớp:

com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl$CustomCompositeInvocationHandlerImpl it is an inner class

  • Lớp cha:

com/sun/corba/se/spi/orbutil/proxy/CompositeInvocationHandlerImpl

  • Tất cả các interfaces:

com/sun/corba/se/spi/orbutil/proxy/LinkedInvocationHandler,java/io/Serializable

  • Đây có là một interface hay không:false
  • Member:

stub!130!com/sun/corba/se/spi/presentation/rmi/DynamicStub!this$0!4112!com/sun/corba/se/impl/presentation/rmi/InvocationHandlerFactoryImpl,!*! thể tạm thời được hiểu là một dấu phân tách, chúng có 1 member stub, kiểu

com/sun/corba/se/spi/presentation/rmi/DynamicStub

Vì là một inner class, nên có nhiều this member , this points hơn outer class

methods.dat

class namemethod namemethod infois static methodsun/nio/cs/ext/Big5newEncoder()Ljava/nio/charset/CharsetEncoder;falsesun/nio/cs/ext/Big5_HKSCS$Decoder\<init>(Ljava/nio/charset/Charset;Lsun/nio/cs/ext/Big5_HKSCS$1;)Vfalse

sun/nio/cs/ext/Big5#newEncoder:

  • Tên lớp: sun/nio/cs/ext/Big5
  • Tên phương thức: newEncoder
  • Thông tin phương thức: ()Ljava/nio/charset/CharsetEncoder; no params,return java/nio/charset/CharsetEncoder object
  • Đây có là phương thức tĩnh: false

sun/nio/cs/ext/Big5_HKSCS$Decoder#\<init>:

  • Tên lớp:sun/nio/cs/ext/Big5_HKSCS$Decoder
  • Tên phương thức:\<init>
  • Thông tin phương thức:

(Ljava/nio/charset/Charset;Lsun/nio/cs/ext/Big5_HKSCS$1;)V Tham số 1 là java/nio/charset/Charset type, Tham số 2 là sun/nio/cs/ext/Big5_HKSCS$1, kiểu và giá trị trả về là void.

  • Đây có là phương thức tĩnh: false

Tạo mối quan hệ kế thừa

Mối quan hệ thừa kế được sử dụng sau này để xác định xem một lớp có thể được serialized bởi một thư viện và tìm kiếm các phương thức lớp con hay không.

public class InheritanceDeriver {
private static final Logger LOGGER = LoggerFactory.getLogger(InheritanceDeriver.class);    
public static InheritanceMap derive(Map<ClassReference.Handle, ClassReference> classMap) {
        LOGGER.debug("Calculating inheritance for " + (classMap.size()) + " classes...");
        Map<ClassReference.Handle, Set<ClassReference.Handle>> implicitInheritance = new HashMap<>();
        for (ClassReference classReference : classMap.values()) {
            if (implicitInheritance.containsKey(classReference.getHandle())) {
                throw new IllegalStateException("Already derived implicit classes for " + classReference.getName());
            }
            Set<ClassReference.Handle> allParents = new HashSet<>();            getAllParents(classReference, classMap, allParents);//Get all the parent classes of the current class            implicitInheritance.put(classReference.getHandle(), allParents);
        }
        return new InheritanceMap(implicitInheritance);
    }
    ...
    ...
    private static void getAllParents(ClassReference classReference, Map<ClassReference.Handle, ClassReference> classMap, Set<ClassReference.Handle> allParents) {
        Set<ClassReference.Handle> parents = new HashSet<>();
        if (classReference.getSuperClass() != null) {
            parents.add(new ClassReference.Handle(classReference.getSuperClass()));//父类
        }
        for (String iface : classReference.getInterfaces()) {
            parents.add(new ClassReference.Handle(iface));//Interface class
        }        for (ClassReference.Handle immediateParent : parents) {
            //Get the indirect parent class, and recursively get the parent class of the indirect parent class
            ClassReference parentClassReference = classMap.get(immediateParent);
            if (parentClassReference == null) {
                LOGGER.debug("No class id for " + immediateParent.getName());
                continue;
            }
            allParents.add(parentClassReference.getHandle());
            getAllParents(parentClassReference, classMap, allParents);
        }
    }
    ...
    ...
}

Kết quả của bước này được lưu vào inheritanceMap.dat:

classDirect parent class + indirect parent classcom/sun/javaws/OperaPreferencesPreferenceSectionPreferenceSectionPreferenceEntryIteratorjava/lang/Object, java/util/Iteratorcom/sun/java/swing/plaf/windows/WindowsLookAndFeel$XPValuejava/lang/Object、javax/swing/UIDefaults$ActiveValue

Bước 2: Tạo luồng dữ liệu “Passthrough”

Luồng dữ liệu Passthrough ở đây là mối quan hệ giữa kết quả trả về của từng phương thức và các tham số của phương thức. Dữ liệu được tạo trong bước này sẽ được sử dụng khi tạo passthrough call graph.

Lấy bản demo được đưa ra bởi tác giả làm ví dụ, đầu tiên đánh giá từ cấp vĩ mô:

Mối quan hệ giữa giá trị trả về FnConstant.invoke và tham số this (Tham số 0, vì tất cả các thành viên của lớp có thể được kiểm soát trong quá trình serialization, vì vậy tất cả các biến thành viên được coi là đối số 0)

  • Mối quan hệ với this param: trả về this.value, có liên quan đến 0
  • Mối quan hệ với arg param: giá trị trả về không có mối quan hệ với arg, nghĩa là nó không có mối quan hệ với 1 tham số.
  • Kết luận là FnConstant.invoke có liên quan đến tham số 0 và được biểu diễn dưới dạng FnConstant.invoke () -> 0

Mối quan hệ giữa giá trị trả về Fndefault.invoke và các tham số this (tham số 0), arg (tham số 1): – mối quan hệ với param này: Nhánh thứ hai của điều kiện trả về có mối quan hệ với this.f, nghĩa là nó có mối quan hệ với 0. – mối quan hệ với arg param: Nhánh đầu tiên của điều kiện trả về có mối quan hệ với arg, nghĩa là nó có mối quan hệ với 1 đối số – Kết luận là FnConstant.invoke có mối quan hệ với 0 tham số và 1 tham số, được biểu thị dưới dạng Fndefault.invoke () -> 0, Fndefault.invoke () -> 1

Trong bước này, công cụ Gadinspector sử dụng ASM để phân tích phương thức bytecode. Logic chính là trong các lớp PassthroughDiscovery và TaintTrackingMethodVisitor. Cụ thể, TaintTrackingMethodVisitor, theo dõi ngăn xếp và localvar của máy ảo JVM khi thực thi các phương thức

Core implementation code

public class PassthroughDiscovery {    
private static final Logger LOGGER = LoggerFactory.getLogger(PassthroughDiscovery.class);    
private final Map<MethodReference.Handle, Set<MethodReference.Handle>> methodCalls = new HashMap<>();
private Map<MethodReference.Handle, Set<Integer>> passthroughDataflow;    
public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();//load Methods.dat
        Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();//load classes.dat
        InheritanceMap inheritanceMap = InheritanceMap.load();//load inheritanceMap.dat        Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);//Find the submethod contained in a method
        List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();//Perform inverse topology sorting on graphs composed of all methods
        passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods,
                config.getSerializableDecider(methodMap, inheritanceMap));//Compute and generate passthrough data flow, involving bytecode analysis
    }
    ...
    ...
    private List<MethodReference.Handle> topologicallySortMethodCalls() {
        Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences = new HashMap<>();
        for (Map.Entry<MethodReference.Handle, Set<MethodReference.Handle>> entry : methodCalls.entrySet()) {
            MethodReference.Handle method = entry.getKey();
            outgoingReferences.put(method, new HashSet<>(entry.getValue()));
        }        // Perform inverse topology sorting on graphs composed of all methods
        LOGGER.debug("Performing topological sort...");
        Set<MethodReference.Handle> dfsStack = new HashSet<>();
        Set<MethodReference.Handle> visitedNodes = new HashSet<>();
        List<MethodReference.Handle> sortedMethods = new ArrayList<>(outgoingReferences.size());
        for (MethodReference.Handle root : outgoingReferences.keySet()) {
            dfsTsort(outgoingReferences, sortedMethods, visitedNodes, dfsStack, root);
        }
        LOGGER.debug(String.format("Outgoing references %d, sortedMethods %d", outgoingReferences.size(), sortedMethods.size()));        return sortedMethods;
    }
    ...
    ...
    private static void dfsTsort(Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences,
                                    List<MethodReference.Handle> sortedMethods, Set<MethodReference.Handle> visitedNodes,
                                    Set<MethodReference.Handle> stack, MethodReference.Handle node) {        if (stack.contains(node)) {//Prevent entry into the loop in a method call chain of dfs
            return;
        }
        if (visitedNodes.contains(node)) {//Prevent reordering of a method and submethod
            return;
        }
        Set<MethodReference.Handle> outgoingRefs = outgoingReferences.get(node);
        if (outgoingRefs == null) {
            return;
        }        stack.add(node);
        for (MethodReference.Handle child : outgoingRefs) {
            dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, child);
        }
        stack.remove(node);
        visitedNodes.add(node);
        sortedMethods.add(node);
    }
}

Sắp xếp Topo

Sắp xếp Topo chỉ thực hiện được với các đồ thị chu trình có hướng (DAG) và không dành cho các đồ thị chu trình không có hướng. Khi đồ thị chu trình có hướng đáp ứng các điều kiện sau: – mọi đỉnh xuất hiện và chỉ xuất hiện một lần – Nếu A ở phía trước B trong chuỗi, không có đường dẫn từ B đến A như trong hình.

Một đồ thị như vậy là một đồ thị theo thứ tự tôpô. Cấu trúc cây cũng có thể được chuyển đổi thành sắp xếp topo, nhưng sắp xếp topo không nhất thiết phải chuyển thành cấu trúc cây

Lấy sơ đồ sắp xếp topo ở trên làm ví dụ, sử dụng kiểu dữ liệu từ điển để biểu diễn cấu trúc đồ thị.

graph = {
"a": ["b","d"],
"b": ["c"],
"d": ["e","c"],
"e": ["c"],
"c": [],
}

Implementation code:

graph = {
    "a": ["b","d"],
    "b": ["c"],
    "d": ["e","c"],
    "e": ["c"],
    "c": [],
}def TopologicalSort(graph):
  degrees = dict((u, 0) for u in graph)
  for u in graph:
      for v in graph[u]:
          degrees[v] += 1
  #Insert queue with zero degree of entry
  queue = [u for u in graph if degrees[u] == 0]
  res = []
  while queue:
      u = queue.pop()
      res.append(u)
      for v in graph[u]:
          # Remove the edge, the intrinsic degree of the current element related element -1
          degrees[v] -= 1
          if degrees[v] == 0:
              queue.append(v)
  return resprint(TopologicalSort(graph)) # ['a', 'd', 'e', 'b', 'c']

Nhưng trong lệnh gọi phương thức, chúng ta mong rằng kết quả cuối cùng là c, b, e, d, a, bước này yêu cầu sắp xếp topo ngược, sắp xếp chuyển tiếp bằng thuật toán BFS, sau đó kết quả ngược lại có thể sử dụng thuật toán DFS. Tại sao chúng ta cần sử dụng sắp xếp cấu trúc liên kết nghịch đảo trong các cuộc gọi phương thức, điều này có liên quan đến việc tạo ra các luồng dữ liệu “passthrough”. Nhìn vào ví dụ sau:

...
public String parentMethod(String arg){
String vul = Obj.childMethod(arg);
return vul;
}
...

Vì vậy, có bất kỳ mối quan hệ giữa arg và giá trị trả về? Giả sử Obj.childMethod là:

...
public String childMethod(String carg){
return carg.toString();
}
...

Vì giá trị trả về của childMethod có liên quan đến carg, nên thể xác định giá trị trả về của ParentMethod có liên quan đến tham số arg. Vì vậy, nếu có một lệnh gọi các phương thức con và chuyển đối số phương thức cha sang phương thức con, trước tiên bạn cần xác định mối quan hệ giữa giá trị trả về phương thức con và đối số của phương thức con. Do đó, sự phân tích các phương thức con cần phải được làm trước, đó là lý do tại sao việc sắp xếp cấu trúc liên kết ngược được thực hiện.

Như bạn có thể thấy trong hình bên dưới, cấu trúc dữ liệu của  outgoingReferences là:

{
method1:(method2,method3,method4), method5:(method1,method6),
...
}

Và cấu trúc này là phù hợp để sắp xếp topo nghịch đảo

Nhưng ở trên đã nói rằng cấu trúc liên kết không thể tạo thành một vòng khi sắp xếp, nhưng lại phải có một vòng trong việc gọi phương thức. Làm thế nào là tác giả xử lý việc này?

Trong  dfsTsort implementation code ở trên, bạn có thể thấy stack và visitedNodes được sử dụng. Stack đảm bảo rằng các vòng lặp không được hình thành khi thực hiện sắp xếp cấu trúc liên kết ngược và visitNodes tránh sắp xếp lặp lại. Call graph sau cho ta thấy rõ quá trình này hơn

Từ hình vẽ, chúng ta có thể thấy rằng có các vòng med1-> med2-> med6-> med1 và có các lời gọi lặp lại đến med3. Nói một cách chính xác, nó không thể được sắp xếp topo nghịch đảo, nhưng nó có thể được nhận ra bằng phương pháp xếp chồng và các bản ghi đã truy cập (stack and visited records). Để thuận tiện cho việc giải thích, sơ đồ trên được biểu thị bằng một cây:

  • Thực hiện sắp xếp topo nghịch đảo (chế độ DFS) trên ảnh trên:

Bắt đầu từ med1, đầu tiên thêm med1 vào stack. Tại thời điểm này, trạng thái của stack, visited, và sortedmethods như sau:

Vậy đã có phương thức con cho med1 chưa? Có, tiếp tục duyệt cây theo chiều sâu. Đặt med2 vào stack, trạng thái tại thời điểm này:

Liệu med2 có phương thức con? Có, tiếp tục duyệt cây theo chiều sâu. Cho med3 vào stack, trạng thái tại thời điểm này:

Liệu med3 có submethods? Có, tiếp tục duyệt cây theo chiều sâu. Đặt med7 vào stack, trạng thái tại thời điểm này:

Liệu med7 có submethods? Không, lấy med7 từ stack và thêm vào visited, sortedmethods, trạng thái tại thời điểm này:

Quay trở lại mức trước, có bất kỳ phương thức con nào khác cho med3 không? Có, med8, đặt med8 vào stack, trạng thái tại thời điểm này:

Có bất kỳ phương thức con cho med8? Không, lấy giá trị đỉnh của stack, và thêm vào visited, sortedmethods, trạng thái tại thời điểm này:

Quay trở lại mức trước, có bất kỳ phương thức con nào khác cho med3 không? Không, lấy giá trị đỉnh của stack, và thêm vào visited, sortedmethods, trạng thái tại thời điểm này:

Quay trở lại mức trước, có phương thức con nào khác cho med2 không? Có med6, thêm med6 vào stack, trạng thái tại thời điểm này:

Có phương thức con nào cho med6 không? Có med1, vậy thêm med1 trong stack? Nếu thêm ta sẽ được trạng thái như bước trước nên ta bỏ qua bước này.

Quay trở lại mức trước, có phương thức con nào cho med6 không? Không, lấy giá trị đỉnh của stack, và thêm vào visited, sortedmethods, trạng thái tại thời điểm này:

Quay trở lại mức trước, Có phương thức con nào khác cho med2 không? Không, lấy giá trị đỉnh của stack, và thêm vào visited, sortedmethods, trạng thái tại thời điểm này:

Quay trở lại mức trước, có phương thức con nào khác cho med1 không? Có med3, nhưng med3 trong visited? Ta bỏ bước này.

Quay trở lại mức trước, abandon khác cho med1 không? Có med4, thêm med4 vào stack, trạng thái tại thời điểm này:

Có phương thức con nào khác cho med4? Không, lấy giá trị đỉnh của stack, và thêm vào visited, sortedmethods, trạng thái tại thời điểm này:

Quay trở lại mức trước, có phương thức con nào khác cho med1 không? Không, lấy giá trị đỉnh của stack, và thêm vào visited, sortedmethods, trạng thái tại thời điểm này (tức là trạng thái cuối cùng):

Vì vậy, kết quả phân loại topo ngược cuối cùng là: med7, med8, med3, med6, med2, med4, med1.

Tạo luồng dữ liệu “passthrough”

Các  sortedmethods được duyệt tại calculatePassthroughDataflow, và thông qua bytecode analysis, luồng dữ liệu passthrough của giá trị trả về của phương thức và mối quan hệ tham số đã được tạo. Lưu ý bộ xác định serialization sau đây, tác giả đã xây dựng bộ ba: JDK, Jackson, Xstream, theo bộ xác định serialization cụ thể để xác định xem một lớp khi đang trong quy trình quyết định có đáp ứng các yêu cầu deserialization của thư viện tương ứng hay không, nếu nó không khớp với:

  • Đối với JDK (ObjectInputStream), lớp kế thừa Serializable interface.
  • Đối với Jackson, lớp có hàm constructor không có tham số?
  • Đối với Xstream, tên lớp có thể là thẻ XML hợp lệ không?

Tạo luồng dữ liệu “passthrough”

...
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 HashMap<>();
for (MethodReference.Handle method : sortedMethods) {//The sortedmethods are traversed in turn, and the submethod of each method is always evaluated before this method, which is achieved by the above inverse topological sorting.。
if (method.getName().equals("<clinit>")) {
continue;
}
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.ASM6, method);
cr.accept(cv, ClassReader.EXPAND_FRAMES);//Determine the relationship between the return value of the current method and the parameter by combining the classMap, the inheritanceMap, the determined passthroughDataflow result, and the serialization determiner information.
passthroughDataflow.put(method, cv.getReturnTaint());//Add the determined method and related pollution points to passthroughDataflow
} catch (Exception e) {
LOGGER.error("Exception analyzing " + method.getClassReference().getName(), e);
}
} catch (IOException e) {
LOGGER.error("Unable to analyze " + method.getClassReference().getName(), e);
}
}
return passthroughDataflow;
}
...

 

Cuối cùng tạo passthrough.dat:

类名方法名方法描述污点java/util/Collections$CheckedNavigableSettailSet(Ljava/lang/Object;)Ljava/util/NavigableSet;0,1java/awt/RenderingHintsput(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;0,1,2

Bước 3: Liệt kê Passthrough Call Graph

Bước này tương tự như bước trước. Công cụ Gadgetinspector quét lại tất cả các phương thức Java, nhưng ta không còn thấy mối quan hệ giữa các tham số và kết quả được trả về, mà là mối quan hệ giữa các tham số của phương thức và phương thức con mà nó gọi, liệu các tham số của phương thức con có thể bị ảnh hưởng bởi các tham số của phương thức cha. Vậy tại sao chúng ta cần tạo luồng dữ liệu thông qua từ bước trước? Do sự phân tích của bước này cũng nằm trong bytecode analysis, nên ở đây chúng ta chỉ có thể đưa ra một số dự đoán, như trong ví dụ sau

...
private MyObject obj; public void parentMethod(Object arg){
...
TestObject obj1 = new TestObject();
Object obj2 = obj1.childMethod1(arg);
this.obj.childMethod(obj2);
...
}
...

Nếu luồng dữ liệu “passthrough” không được thực thi thì không thể đưa ra kết luận liệu giá trị trả về của TestObject.childMethod1 có bị ảnh hưởng bởi tham số 1 hay không và không thể tiếp tục phân tích mối quan hệ truyền tham số giữa tham số arg phương thức cha và con phương pháp MyObject.childmethod.

Tác giả đưa ra một ví dụ:

AbstractTableModel$ff19274a.hashcode và phương thức con IFn.invoke:

  • this parameter(0 parameter) của AbstractTableModel$ff19274a.hashcode được truyền cho tham số 1 của IFn.invoke, được thể hiện như sau

0 ->IFn.invoke()@1

  • Bởi vì f được lấy bởi this.__clojureFnMap(0 parameter), và f là this (0 parameter) của  IFn.invoke(),  tham số 0 của AbstractTableModel$ff19274a.hashcode được truyền cho tham số 0 của IFn.invoke. Expressed as 0->IFn.invoke()@0

FnCompose.invoke và phương thức con IFn.invoke:

  • arg (1 đối số) của FnCompose.invoked được truyền qua một đối số của IFn.invoke, biểu diễn như sau 1->IFn.invoke()@1
  • f1 là thuộc tính của FnCompose (this, 0 argument), được truyền như this của IFn.invoke, biểu diễn như sau 0->IFn.invoke()@1
  • f1.invoke(arg) được truyền như một tham số tới IFn.invoke. Vì f1 được serialized, chúng ta có thể kiểm soát lớp nào implement IFn. F1.invoke (arg) có thể được coi là tham số 0 được truyền cho tham số 1 của IFn.invoke (đây chỉ là một phỏng đoán đơn giản, việc triển khai cụ thể trong bytecode analysis), được biểu diễn như sau 0-> IFn.invoke()@1

Trong bước này, công cụ Gadinspector cũng sử dụng ASM để phân tích bytecode. Logic chính là trong các lớp CallGraphDiscovery và ModelGeneratorClassVisitor. ModelGeneratorClassVisitor theo dõi ngăn xếp và localvar của máy ảo JVM khi thực thi các phương thức và cuối cùng có được mối quan hệ truyền tham số giữa các tham số Phương thức và các đối tượng con mà nó gọi.

Tạo passthrough call graph code  (tạm thời bỏ qua việc triển khai ModelGeneratorClassVisitor):

public class CallGraphDiscovery {
private static final Logger LOGGER = LoggerFactory.getLogger(CallGraphDiscovery.class);    
private final Set<GraphCall> discoveredCalls = new HashSet<>();    public void discover(final ClassResourceEnumerator classResourceEnumerator, GIConfig config) throws IOException {
        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();//load all methods
        Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();//load all classes
        InheritanceMap inheritanceMap = InheritanceMap.load();//load inheritance graph
        Map<MethodReference.Handle, Set<Integer>> passthroughDataflow = PassthroughDiscovery.load();//load passthrough data flow        SerializableDecider serializableDecider = config.getSerializableDecider(methodMap, inheritanceMap);//Serialization decider        
for (ClassResourceEnumerator.ClassResource classResource : classResourceEnumerator.getAllClasses()) {
            try (InputStream in = classResource.getInputStream()) {
                ClassReader cr = new ClassReader(in);
                try {
                    cr.accept(new ModelGeneratorClassVisitor(classMap, inheritanceMap, passthroughDataflow, serializableDecider, Opcodes.ASM6),
                            ClassReader.EXPAND_FRAMES);//Determine the current method parameter and sub-method transfer call relationship by combining classMap, inheritanceMap, passthroughDataflow result, and serialization determiner information.
                } catch (Exception e) {
                    LOGGER.error("Error analyzing: " + classResource.getName(), e);
                }
            }
        }
    }

Cuối cùng đã tạo passthrough.dat:

parent class nameparent methodparent method infochild method’s class namechild methodchild method infoparent method parameter indexwhich field of the parameter object is passedchild method parameter indexjava/io/PrintStreamwrite(Ljava/lang/String;)Vjava/io/OutputStreamflush()V0out0javafx/scene/shape/ShapesetSmooth(Z)Vjavafx/scene/shape/ShapesmoothProperty()Ljavafx/beans/property/BooleanProperty;00

Bước 4: Tìm kiếm Sources có sẵn

Bước này kiểm tra tất cả các phương thức có thể triggered được lỗ hổng dựa vào các entry để tìm lỗ hổng deserialization. Ví dụ: khi sử dụng proxy trong một chain, bất kỳ phương thức được gọi nào có thể được serialized và là một lớp con của java/lang/Reflect/InvocationHandler và đều có thể được coi là Source. Nó cũng xác định liệu các lớp đó có thể được serialize dựa trên một thư viện deserialization cụ thể hay không.

Tìm kiếm các Sources có sẵn:

public class SimpleSourceDiscovery extends SourceDiscovery {    
@Override
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 the class implements readObject, the ObjectInputStream is considered to be trainted
        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));
                }
            }
        }        // Any classes that extend serializable and InvocationHandler are trainted when using proxy techniques.
        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() is an accessible entry point for standard techniques for putting 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 the comparator proxy, you can jump to any groovy Closure call()/doCall() method, all args are contaminated
        //

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));
                }
            }
        }
    }
...

Kết quả của bước này sẽ được lưu trong file sources.dat:

classmethodmethodinfotrainted

argjava/awt/color/ICC_Profilefinalize()V0java/lang/EnumreadObject (Ljava/io/ObjectInputStream;) V1

Bước 5: Tìm kiếm Generation Call Chain

Bước này đi qua tất cả các  Source và tìm kiếm đệ quy tất cả các lệnh gọi phương thức con mà có thể truyền Taint parameter trong callgraph.dat cho đến khi nó gặp phương thức trong phần Sink.

Tìm kiếm Generation Call Chain:

public class GadgetChainDiscovery {    
private static final Logger LOGGER = LoggerFactory.getLogger(GadgetChainDiscovery.class);   
private final GIConfig config;    
public GadgetChainDiscovery(GIConfig config) {
        this.config = config;
    }    
public void discover() throws Exception {
        Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
        InheritanceMap inheritanceMap = InheritanceMap.load();
        Map<MethodReference.Handle, Set<MethodReference.Handle>> methodImplMap = InheritanceDeriver.getAllMethodImplementations(
                inheritanceMap, methodMap);//Get all subclass method implementations of methods (methods rewritten by subclasses)        
final ImplementationFinder implementationFinder = config.getImplementationFinder(
                methodMap, methodImplMap, inheritanceMap);//Save all subclass method implementations of the method to methodimpl.dat
        try (Writer writer = Files.newBufferedWriter(Paths.get("methodimpl.dat"))) {
            for (Map.Entry<MethodReference.Handle, Set<MethodReference.Handle>> entry : methodImplMap.entrySet()) {
                writer.write(entry.getKey().getClassReference().getName());
                writer.write("\t");
                writer.write(entry.getKey().getName());
                writer.write("\t");
                writer.write(entry.getKey().getDesc());
                writer.write("\n");
                for (MethodReference.Handle method : entry.getValue()) {
                    writer.write("\t");
                    writer.write(method.getClassReference().getName());
                    writer.write("\t");
                    writer.write(method.getName());
                    writer.write("\t");
                    writer.write(method.getDesc());
                    writer.write("\n");
                }
            }
        }        //The method calls map, the key is the parent method, and the value is the sub-method and the parent method parameter.
        Map<MethodReference.Handle, Set<GraphCall>> graphCallMap = new HashMap<>();
        for (GraphCall graphCall : DataLoader.loadData(Paths.get("callgraph.dat"), new GraphCall.Factory())) {
            MethodReference.Handle caller = graphCall.getCallerMethod();
            if (!graphCallMap.containsKey(caller)) {
                Set<GraphCall> graphCalls = new HashSet<>();
                graphCalls.add(graphCall);
                graphCallMap.put(caller, graphCalls);
            } else {
                graphCallMap.get(caller).add(graphCall);
            }
        }        //exploredMethods saves the method node that the call chain has visited since the lookup process, and methodsToExplore saves the call chain
        Set<GadgetChainLink> exploredMethods = new HashSet<>();
        LinkedList<GadgetChain> methodsToExplore = new LinkedList<>();
        //Load all sources and use each source as the first node of each chain
        for (Source source : DataLoader.loadData(Paths.get("sources.dat"), new Source.Factory())) {
            GadgetChainLink srcLink = new GadgetChainLink(source.getSourceMethod(), source.getTaintedArgIndex());
            if (exploredMethods.contains(srcLink)) {
                continue;
            }
            methodsToExplore.add(new GadgetChain(Arrays.asList(srcLink)));
            exploredMethods.add(srcLink);
        }        long iteration = 0;
        Set<GadgetChain> discoveredGadgets = new HashSet<>();
        //Use BFS to search all call chains from source to sink
        while (methodsToExplore.size() > 0) {
            if ((iteration % 1000) == 0) {
                LOGGER.info("Iteration " + iteration + ", Search space: " + methodsToExplore.size());
            }
            iteration += 1;            GadgetChain chain = methodsToExplore.pop();//Pop a chain from the head of the team
            GadgetChainLink lastLink = chain.links.get(chain.links.size()-1);//Take the last node of this chain            Set<GraphCall> methodCalls = graphCallMap.get(lastLink.method);//Get the transfer relationship between all submethods of the current node method and the current node method parameters
            if (methodCalls != null) {
                for (GraphCall graphCall : methodCalls) {
                    if (graphCall.getCallerArgIndex() != lastLink.taintedArgIndex) {
                        //Skip if the pollution parameter of the current node method is inconsistent with the index of the current submethod that is affected by the parent method parameter
                        continue;
                    }                    Set<MethodReference.Handle> allImpls = implementationFinder.getImplementations(graphCall.getTargetMethod());//Get all subclass rewriting methods of the class in which the submethod is located                    for (MethodReference.Handle methodImpl : allImpls) {
                        GadgetChainLink newLink = new GadgetChainLink(methodImpl, graphCall.getTargetArgIndex());//New method node
                        if (exploredMethods.contains(newLink)) {
                            //If the new method has been accessed recently, skip it, which reduces overhead. But this step skip will cause other chains/branch chains to pass through this node. Since this node has already been accessed, the chain will be broken here. So if this condition is removed, can you find all the chains? Removed here will encounter ring problems, resulting in an infinite increase in the path...
                            continue;
                        }                        
GadgetChain newChain = new GadgetChain(chain, newLink);//The new node and the previous chain form a new chain
                        if (isSink(methodImpl, graphCall.getTargetArgIndex(), inheritanceMap)) {//If the sink is reached, add the discoveredGadgets
                            discoveredGadgets.add(newChain);
                        } else {
                            //New chain join queue
                            methodsToExplore.add(newChain);
                            //New node joins the accessed collection
                            exploredMethods.add(newLink);
                        }
                    }
                }
            }
        }        //Save the searched exploit chain to gadget-chains.txt
        try (OutputStream outputStream = Files.newOutputStream(Paths.get("gadget-chains.txt"));
             Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)) {
            for (GadgetChain chain : discoveredGadgets) {
                printGadgetChain(writer, chain);
            }
        }        LOGGER.info("Found {} gadget chains.", discoveredGadgets.size());
    }
...

Phương thức Sink đưa ra bởi tác giả:

private boolean isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) {
if(method.getClassReference().getName().equals("java/io/FileInputStream")&&method.getName().equals("<init>")) {
      return true;
}
if (method.getClassReference().getName().equals("java/io/FileOutputStream")&&method.getName().equals("<init>")) {
     return true;
}
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 true;
}

if (method.getClassReference().getName().equals("java/lang/Runtime")&&method.getName().equals("exec")) {
     return true;
}
/*
if(method.getClassReference().getName().equals("java/lang/Class")&&method.getName().equals("forName")) {
      return true;
}
if(method.getClassReference().getName().equals("java/lang/Class")&&method.getName().equals("getMethod")) {
       return true;
}
*/
// 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 true;
}
if (method.getClassReference().getName().equals("java/net/URLClassLoader")
&& method.getName().equals("newInstance")) {
return true;
}
if (method.getClassReference().getName().equals("java/lang/System")
&& method.getName().equals("exit")) {
return true;
}
if (method.getClassReference().getName().equals("java/lang/Shutdown")
&& method.getName().equals("exit")) {
return true;
}
if (method.getClassReference().getName().equals("java/lang/Runtime")
&& method.getName().equals("exit")) {
return true;
}
if (method.getClassReference().getName().equals("java/nio/file/Files")
&& method.getName().equals("newOutputStream")) {
return true;
}
if (method.getClassReference().getName().equals("java/lang/ProcessBuilder")
&& method.getName().equals("<init>") && argIndex > 0) {
return true;
}
if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("java/lang/ClassLoader"))
&& method.getName().equals("<init>")) {
return true;
}
if (method.getClassReference().getName().equals("java/net/URL") && method.getName().equals("openStream")) {
return true;
}
// Some groovy-specific sinks
if (method.getClassReference().getName().equals("org/codehaus/groovy/runtime/InvokerHelper")
&& method.getName().equals("invokeMethod") && argIndex == 1) {
return true;
}
if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("groovy/lang/MetaClass"))
&& Arrays.asList("invokeMethod", "invokeConstructor", "invokeStaticMethod").contains(method.getName())) {
return true;
}
return false;
}

Đối với entrypoint và tất cả các lệnh gọi phương thức con , các lệnh gọi phương thức cháu, tạo thành một cây đệ quy. Những gì các bước trước đã làm giống với việc tạo cây và bước này làm là tìm đường dẫn đến nút lá từ nút gốc, sao cho nút lá chính xác là phương thức sink mà chúng ta mong đợi. Công cụ gadgetinspector sử dụng thuật toán breadth-first để duyệt toàn bộ cây và bỏ qua các nút đã được kiểm tra, điều này làm giảm chi phí hoạt động và tránh các vòng lặp, nhưng lại bỏ qua nhiều chain.

Quá trình này như sau:

Lưu ý: targ chỉ ra chỉ số của tham số được đánh dấu, 0-> 1 chỉ ra rằng tham số 0 của phương thức cha được truyền cho tham số 1 của phương thức con.

Sample Analysis

Chúng ta sẽ viết một ví dụ demo cụ thể dựa trên ví dụ của tác giả để thử các bước trên.

Bản demo như sau:

IFn.java:
package com.demo.ifn;    
import java.io.IOException;    
public interface IFn {
        public Object invokeCall(Object arg) throws IOException;
    }

FnEval.java:
package com.demo.ifn;    
import java.io.IOException;
import java.io.Serializable;    public class FnEval implements IFn, Serializable {
        public FnEval() {
        }        public Object invokeCall(Object arg) throws IOException {
            return Runtime.getRuntime().exec((String) arg);
        }
    }

FnConstant.java:
package com.demo.ifn;    
import java.io.Serializable;    
public class FnConstant implements IFn , Serializable {
private Object value;        
       public FnConstant(Object value) {
            this.value = value;
        }        public Object invokeCall(Object arg) {
            return value;
        }
    }

FnCompose.java:
package com.demo.ifn;    
import java.io.IOException;
import java.io.Serializable;    
public class FnCompose implements IFn, Serializable {
        private IFn f1, f2;        
        public FnCompose(IFn f1, IFn f2) {
            this.f1 = f1;
            this.f2 = f2;
        }        
        public Object invokeCall(Object arg) throws IOException {
            return f2.invokeCall(f1.invokeCall(arg));
        }
    }

TestDemo.java:
package com.demo.ifn;    
public class TestDemo {
        //测试拓扑排序的正确性
        private String test;        
        public String pMethod(String arg){
            String vul = cMethod(arg);
            return vul;
        }  
      
        public String cMethod(String arg){
            return arg.toUpperCase();
        }
    }

AbstractTableModel.java:
package com.demo.model;    
import com.demo.ifn.IFn;    
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;    
public class AbstractTableModel implements Serializable {
        private HashMap<String, IFn> __clojureFnMap;        
        public AbstractTableModel(HashMap<String, IFn> clojureFnMap) {
            this.__clojureFnMap = clojureFnMap;
        }        
        public int hashCode() {
            IFn f = __clojureFnMap.get("hashCode");
            try {
                f.invokeCall(this);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return this.__clojureFnMap.hashCode() + 1;
        }
    }

Lưu ý: Thứ tự của dữ liệu trong ảnh bên dưới được thay đổi và chỉ cung cấp dữ liệu trong com / demo

Bước 1: Liệt kê tất cả các lớp và tất cả các phương thức của mỗi lớp

classes.dat:

Methods.dat:

Bước 2: Tạo luồng dữ liệu Passthrough

passthrough.dat:

https://miro.medium.com/max/855/0*ZDbpmFuQUARRuB4w

Có thể thấy rằng chỉ có invokeCall FnConstant trong lớp con của IFn nằm trong luồng dữ liệu “passthrough”, bởi vì một số stactic analysis khác không thể xác định mối quan hệ giữa giá trị trả về và tham số. Đồng thời, TestMethod sườn cMethod và pMethod nằm trong luồng dữ liệu thông qua, điều này cũng giải thích sự cần thiết và chính xác của bước 2.

Bước 3: Liệt kê Passthrough Call Graph

callgraph.dat:

Bước 4: Tìm kiếm các Source có sẵn

sources.dat:

https://miro.medium.com/max/548/0*etPxeo4OvEKf0L-C

Bước 5: Tìm kiếm generation call chain

Chain dưới đây được tìm thấy trong gadget-chains.txt:

com/demo/model/AbstractTableModel.hashCode()I (0)
com/demo/ifn/FnEval.invokeCall(Ljava/lang/Object;)Ljava/lang/Object; (1)
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)

Có thể thấy rằng ta cần tìm ra đường dẫn ngắn nhất và không đi qua đường dẫn FnCompose, FnConstant.

Loop Caused Path Explosion

Trong bước thứ năm của quá trình phân tích, điều gì xảy ra nếu bạn loại bỏ sự phân tích của nút được đã đi qua, bạn có thể tạo chuỗi lời gọi thông qua FnCompose và FnConstant không?

In the explosion state, không gian tìm kiếm được tăng vô hạn và phải có một vòng lặp. Chiến lược được sử dụng bởi tác giả là không truy cập các nút các nút đã đi qua, do đó giải quyết vấn đề vòng lặp, nhưng mất các chuỗi khác.

Ví dụ: lớp FnCompose ở trên:

public class Fncompose implements IFn{
     private IFn f1,f2;
     public Object invoke(Object arg){
         return f2.invoke(f1.invoke(arg));
     }
}

Vì IFn là một  interface, nó sẽ tìm một lớp con của nó trong call chain generation. Nếu F1 và f2 là các đối tượng của lớp FnCompose, thì điều này tạo thành một vòng lặp.

Lời gọi ngầm

Kiểm tra lời gọi ngầm để xem liệu công cụ có thể tìm và thực hiện một số thay đổi với  FnEval.java

FnEval.java:

package com.demo.ifn;    
import java.io.IOException;
import java.io.Serializable;    
public class FnEval implements IFn, Serializable {
        private String cmd;        
        public FnEval() {
        }        @Override
        public String toString() {
            try {
                Runtime.getRuntime().exec(this.cmd);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return "FnEval{}";
        }        public Object invokeCall(Object arg) throws IOException {
            this.cmd = (String) arg;
            return this + " test";
        }
    }

Kết quả:

com/demo/model/AbstractTableModel.hashCode()I (0)
com/demo/ifn/FnEval.invokeCall(Ljava/lang/Object;)Ljava/lang/Object; (0)
java/lang/StringBuilder.append(Ljava/lang/Object;)Ljava/lang/StringBuilder; (1)
java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String; (0)
com/demo/ifn/FnEval.toString()Ljava/lang/String; (0)
java/lang/Runtime.exec(Ljava/lang/String;)Ljava/lang/Process; (1)

Phương thức toString được gọi ngầm, chỉ ra rằng bước này được thực hiện trong phân tích bytecode.

Không đi theo lời gọi “Java Reflection”

Trong phần mô tả công cụ trên github, tác giả cũng đã đề cập đến điểm mù của công cụ này trong static analysis, ví dụ như FnEval.Class.getMethod("exec", String.Class).invoke(null, arg) không tuân theo reflection call, thử sửa FnEval.java

FnEval.java:

package com.demo.ifn;import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;public class FnEval implements IFn, Serializable {    
public FnEval() {
    }    public static void exec(String arg) throws IOException {
        Runtime.getRuntime().exec(arg);
    }    public Object invokeCall(Object arg) throws IOException {
        try {
            return FnEval.class.getMethod("exec", String.class).invoke(null, arg);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }
}

Sau khi test thử, nó không được tìm thấy. Nhưng khi đổi kiểu code từ FnEval.class.getMethod("exec", String.class).invoke(null, arg) sang this.getClass().getMethod("exec", String.class).invoke(null,arg) thì lại có thể tìm thấy

Các cú pháp đặc biệt

Thử viết mốt số cú pháp đặc biệt ví dụ như lambda và thay đổi FnEval.java:

FnEval.java:
package com.demo.ifn;    
import java.io.IOException;
import java.io.Serializable;   
public class FnEval implements IFn, Serializable {       
    public FnEval() {
        }        interface ExecCmd {
            public Object exec(String cmd) throws IOException;
        }        public Object invokeCall(Object arg) throws IOException {
            ExecCmd execCmd = cmd -> {
                return Runtime.getRuntime().exec(cmd);
            };
            return execCmd.exec((String) arg);
        }
    }

Sau khi test thử, chain không được tìm thấy. Nhưng thay đổi FnEval. Class.getMethod ("exec", String. Class).invoke (null, arg) thành this.getClass(). GetMethod (" exec ", String. Class).invoke (null, arg) thì có thể tìm thấy

Ngữ pháp đặc biệt

Thử một số cú pháp đặc biệt, chẳng hạn như cú pháp lambda, thực hiện một số thay đổi đối với FnEval.java:

FnEval.java:

package com.demo.ifn;    
import java.io.IOException;
import java.io.Serializable;    
public class FnEval implements IFn, Serializable {        
     public FnEval() {
        }        interface ExecCmd {
            public Object exec(String cmd) throws IOException;
        }        public Object invokeCall(Object arg) throws IOException {
            ExecCmd execCmd = cmd -> {
                return Runtime.getRuntime().exec(cmd);
            };
            return execCmd.exec((String) arg);
        }
    }

Sau khi test thử, chain này đã không được phát hiện. Điều này cho thấy rằng phần phân tích cú pháp hiện tại chưa phân tích các cú pháp đặc biệt

Anonymous Inner Class

Kiểm tra các Anonymous Inner Class và thực hiện một số thay đổi đối với FnEval.java:

FnEval.java:

package com.demo.ifn;    
import java.io.IOException;
import java.io.Serializable;    
public class FnEval implements IFn, Serializable {        
     public FnEval() {
        }        
     interface ExecCmd {
            
     public Object exec(String cmd) throws IOException;
        }        
     public Object callExec(ExecCmd execCmd, String cmd) throws IOException {
            return execCmd.exec(cmd);
        }       
     public Object invokeCall(Object arg) throws IOException {
            return callExec(new ExecCmd() {
                
@Override
                
     public Object exec(String cmd) throws IOException {
                    return Runtime.getRuntime().exec(cmd);
                }
            }, (String) arg);
        }
    }

Sau khi thử nghiệm, chain này không được phát hiện, chứng tỏ rằng chưa có cú phân tích về các lớp Anonymous Inner Class

Sink->Source?

Chúng ta có thể đi từ source->sink, vậy điều ngược lại có thể xảy ra không? Vì  Source và Sink được biết khi tìm kiếm Source -> Sink, nếu Sink và Source được biết khi tìm kiếm Sink -> Source, thì việc đi từ Source -> Sink và Sink -> Source không khác nhau. Nếu chúng ta có coi Source là một tính năng có thể kiểm soát tham số, thì phương thức Source -> Sink là một cách rất tốt để khai thác lỗ hổng deserialization, và trong các lỗ hổng khác (như templates injection, v.v.). Nhưng vẫn còn một số vấn đề ở đây. Ví dụ, deserialization coi this và các thuộc tính của lớp là đối số 0, bởi vì chúng có thể được kiểm soát trong quá trình khử deserialization, nhưng trong các lỗ hổng khác, điều này là không cần thiết

Khuyết điểm

Hiện tại, tôi chưa thực hiện nhiều thử nghiệm, tôi chỉ phân tích nguyên tắc chung của công cụ này từ cấp độ vĩ mô. Kết hợp với bài viết phân tích của Ping An Group và các test case trên thì có thể tóm tắt một số thiếu sót sau:

  • Callgraph chưa hoàn thành
  • Kết quả tìm kiếm call chain không đầy đủ do cách tìm kiếm
  • Một số cú pháp đặc biệt, các inner classes ẩn danh chưa được hỗ trợ

Cải thiện

  • Cải thiện những khiếm khuyết trên
  • Thử nghiệm liên tục kết hợp với các chains đã biết (ví dụ: ysoserial, v.v.)
  • Liệt kê tất cả các chuỗi càng nhiều càng tốt và kết hợp chúng với sàng lọc thủ công. Chiến lược được tác giả là miễn là có một chuỗi đi qua một nút, thì các chuỗi khác sẽ không tiếp tục tìm kiếm đi qua nút này.
  • DFS + giới hạn độ sâu tối đa
  • Tiếp tục sử dụng thuật toán BFS, kiểm tra thủ công call chain đã tạo, xóa callgraph không hợp lệ và lặp lại quá trình chạy
  • Gọi các chain cache (phương pháp này tôi chưa thực sự hiểu rõ)

Ý tưởng của tôi là duy trì một danh sách đen trong mỗi  chain, kiểm tra các vòng lặp mỗi lần. Nếu một vòng lặp xảy ra trong  chain này, các nút gây ra vòng lặp sẽ bị liệt vào danh sách đen và tiếp tục để nó đi, và cần thêm giới hạn độ dài đường dẫn.

  • Thử thực hiện Sink->Source

Tìm kiếm đồng thời đa luồng cho nhiều chains sử dụng để tăng tốc

Lời kết

Trong việc phân tích nguyên trên, các chi tiết của việc bytecode analysis được bỏ qua. Một số chỗ chỉ là kết quả của việc phán đoán và kiểm tra tạm thời, vì vậy có thể có một số lỗi. Bytecode analysis là một phần rất quan trọng. Nó đóng một vai trò quan trọng trong việc phân tích the stain và chuyển the stain. Nếu có vấn đề trong các phần này, toàn bộ quá trình tìm kiếm sẽ có vấn đề. Do ASM framework có yêu cầu cao đối với người dùng, nên cần nắm vững kiến ​​thức về JVM để sử dụng tốt hơn khung ASM framework, vì vậy bước tiếp theo là bắt đầu học những thứ liên quan đến JVM. Bài viết này chỉ phân tích nguyên tắc của công cụ này từ cấp độ vĩ mô, nhưng cũng tăng thêm sự tự tin cho chính bạn. Vì ít nhất bạn hiểu rằng công cụ này không thể hiểu được và không thể được cải thiện. Đồng thời, tôi sẽ được tách ra khỏi công cụ sau đó. Nó cũng thuận tiện và những người khác có thể tham khảo nó nếu họ quan tâm đến công cụ này. Sau khi tôi quen và có thể thao tác Java bytecode, tôi sẽ quay lại và cập nhật bài viết này và sửa các lỗi có thể xảy ra.

Nếu những ý tưởng và cải tiến này thực sự được thực hiện và xác minh, thì công cụ này thực sự là một trợ giúp tốt. Nhưng vẫn còn một chặng đường dài để đi trước khi những điều này có thể được xảy ra. Tôi đã tưởng tượng rất nhiều vấn đề trước khi tôi bắt đầu thực hiện chúng. Tôi sẽ gặp nhiều vấn đề hơn khi tôi nhận ra chúng. Nhưng may mắn thay, có một hướng chung, và bước tiếp theo là giải quyết từng vấn đề nhỏ một

123 lượt xem