Tôi và anh em đã để ý tới bug này kể từ khi nó xuất hiện trên mailing list của Apache, tuy nhiên vào thời điểm đó (20/07), chưa có quá nhiều thông tin để có thể reproduce được …
Bẵng đi một thời gian, cuối tuần vừa rồi đã có một bài blog phân tích không thể chi tiết hơn về bug cũng như cách xây dựng payload như thế nào. Thật tiếc là bài này được viết bằng tiếng Trung Quốc, khi translate sang tiếng Anh sẽ rất khó đọc, mình đã mất cả cuối tuần vừa rồi để dịch và đọc hiểu nội dung trong đó.
Link bài gốc tại đây: http://noahblog.360.cn/xalan-j-integer-truncation-reproduce-cve-2022-34169/
Do bài viết gốc đã quá chi tiết rồi, nên mục đích của bài viết này chỉ là dịch lại về ngữ nghĩa sang tiếng Việt, phần nào đó có thể giúp cho các bạn người Việt có thể dễ dàng đọc hiểu hơn. (Từ ngữ trong bài cũng đã được lựa chọn để Google Translate có thể dễ dàng dịch lại sang tiếng Anh mà không bị thay đổi ngữ nghĩa nhiều).
#Overview
XSLT (Extensible Stylesheet Language Transformations) là một ngôn ngữ được sử dụng để transform XML sang một định dạng khác, ví dụ như HTML …
Trên Java, thư viện Apache Xalan-J được sử dụng rộng rãi vào việc xử lý định dạng XSLT này,
Nếu bạn đọc đã từng làm qua các loại bug về Java Deserialization thì cũng có thể đã biết tới sự tồn tại của “TemplatesImpl.newTransformer()”, được sử dụng khá nhiều làm sink để thực thi lệnh.
Điều này xảy ra là do Xalan-J sử dụng một bộ JIT-compiler (Apache BCEL) để translate XSLT sang một class mới để phục vụ cho việc transform file XML.
Cấu trúc của một file Class sau khi được compile sẽ như sau:
Trong quá trình compile Class, các Constant ví dụ như “String” hoặc “Integer” (lưu ý là Integer phải đủ lớn) sẽ được push vào Constant Pool để phục vụ cho việc refer ở phía sau.
Ví dụ class:
Khi compile sang bytecode sẽ có dạng:
(Sử dụng công cụ ClassPy để xem)
Và điểm đáng lưu ý là mỗi Class file chỉ sử dụng 2 bytes cho việc xác định kích thước của Constant Pool, do đó giới hạn Constant Pool của một Class file sẽ là 0xFFFF = 65535
Nguyên nhân gốc rễ của lỗi này cũng xảy ra tại đây: trong quá trình compile bytecode, BCEL compiler không giới hạn kích thước của Constant Pool.
Như trong ví dụ dưới đây, ta có thể thấy số lượng Constant Pool là 67330, đã vượt quá giới hạn của Constant Pool.
Khi đó BCEL thực hiện gọi writeShort(), khiến cho số lượng Constant Pool bị truncated từ 67330 thành 1794
Tuy nhiên, BCEL vẫn tiếp tục ghi hết 67330 record của Constant Pool này vào Class file:
Khi đó, cấu trúc của một Class file đã bị phá hỏng, các cấu trúc nằm phía sau Constant Pool đều có thể bị ghi đè bởi nội dung của Constant Pool
#XSLT file
Code java phục vụ cho việc test:
Xem xét một file XSLT mẫu như sau:
Sau khi được compile, ta có thể thấy xml tag <AAA/> đã được thêm vào Constant Pool, ClassPy parse thành 2 trường có chuỗi AAA,
- Trong đó với trường có tag id = 1, ứng với Utf8_info, bytes = AAA
- Trường còn lại có tag id = 8, ứng với String_info, string_index = #92, chính là Utf8_info phía trên
Có thể tham khảo Constant Pool map tại đây
Như vậy, đối với mỗi một String (riêng biệt) sẽ chiếm 2 records trong Constant Pool, trường thứ nhất là Utf8_info dùng để khai báo nội dung của String, trường thứ 2 là String_info, dùng để index tới Utf8_info — nội dung của String.
Lưu ý rằng “AA” và “AAA” là 2 Constant hoàn toàn khác nhau, do vậy khi compile XSLT phía dưới, kích thước của Constant Pool sẽ được tăng lên 2 đơn vị:
Như vậy, ta chỉ cần tạo lần lượt >65535 element, số lượng Constant Pool sẽ bị truncated và gây tạo ra một file class gây crash quá trình parse XSLT:
Tuy nhiên, với cách thêm nhiều element như vậy cũng sẽ gây tốn rất nhiều thời gian trong quá trình compile, và để xử lý vấn đề này, ta có thể sử dụng cách add các attribute của xml tag thay vì add một element mới, ví dụ:
Gen payload crash sử dụng file sau: https://gist.github.com/testanull/474767aa2df6b0ebf9ebe175991993da
Payload bao gồm khoảng 65538 Constant, Class sau khi được compile bị truncated Constant Pool length, và số lượng Constant Pool bây giờ là 02, khiến cho chương trình bị crash :
Để thêm Constant dạng số vào Constant Pool (giá trị cần phải lớn hơn 32767), có thể sử dụng cách thêm Constant đó dưới dạng Argument của một Method call, ví dụ như muốn thêm một số `double` vào Constant Pool ta có thể sử dụng method Math.ceil(double):
Các Constant dạng số chỉ chiếm 1 slot trong Constant Pool thay vì 2 slot như String. Trong đó, với Constant dạng Double, chỉ byte đầu tiên là được sử dụng để xác định kiểu Constant, 8 bytes phía sau hoàn toàn có thể bị điều khiển và sử dụng vào mục đích khác (chi tiết hơn ở phần sau)
#Exploit payload construction
Như đã đề cập ở phần trên, Constant Pool length bị truncated và nội dung của Constant Pool vẫn được tiếp tục ghi đè qua các trường phía sau.
Dựa vào đó ta có thể:
- Chèn thêm một số lượng Constant nhất định để khi Constant Pool length bị truncate sẽ trở thành Constant Pool length mong muốn, ví dụ như: 67330 & 0xFFFF = 1794
- Khi đó tất cả số byte phía sau 1794 Constant Pool sẽ được reuse cho các trường access_flags, this_class, Code … đây cũng chính là số Constant mình đã overwrite phía trước
Ý tưởng khai thác có thể mô hình hóa như sau:
Class file trước khi bị sửa đổi:
Sau khi exploit, Constant Pool bị truncate và các Constant được reuse ứng với các trường access_flags, this_class …
Về ý tưởng là vậy, nhưng việc rebuild một Class file để khai thác không dễ dàng chút nào. Vùng dữ liệu của Class file đã bị overwrite vừa phải hợp lệ với các trường mới (access_flags, this_class, …) cũng vừa phải là một Constant hợp lệ nữa.
Một Class sau khi build sẽ có cấu trúc đầy đủ như sau: http://noahblog.360.cn/content/images/2022/09/04-ClassFile----.svg
+access_flags &this_class
Cấu trúc sơ bộ của một Class file như sau
Trong đó access_flags chiếm 2 bytes, this_class cũng chiếm 2 bytes
Như đã biết, access_flag & this_class nằm ngay sau Constant Pool và trước khi bị truncate thì khu vực này vẫn là các Constant hợp lệ
=> Do đó, byte đầu tiên của access_flags phải là một tag (dùng để phân biệt các loại Constant)
Và việc lựa chọn byte này cũng sẽ ảnh hưởng tới các bytes phía sau, vì mỗi tag sẽ quyết định có bao nhiêu bytes sau đó có thể bị control thoải mái.
Ví dụ như ở đây mình lựa chọn tag id = 3 (Constant Integer), theo định nghĩa của tag này, ngoài byte đầu tiên để xác định tag id thì 4 bytes phía sau được sử dụng để lưu trữ dữ liệu, như vậy đồng nghĩa với việc 4 bytes phía sau mình có thể làm gì cũng được, nó vẫn là một Constant hợp lệ!
Nội dung của Constant:
Đây cũng là một trong những nguyên tắc cốt lõi cần nhớ vì hầu như cả quá trình xây dựng payload phía sau đều dựa vào nó!
Ở đây, việc trigger code execution dựa vào việc sửa đổi Code của Constructor trước khi được gọi, do đó access_flags cần phải có modifier public, không phải là interface/abstract/annotation hoặc enum:
Việc xác định modifier của một Class được thực hiện như sau:
- access_flags & Modifier != 0
Ở đây ACC_PUBLIC=0x0001,
=> access_flags & 0x0001 != 0
Tuy nhiên do một số yêu cầu đặc thù phía sau, độ dài của Constant Pool phải lớn hơn 0x0600 (1536), nên access_flags không thể là 0x0001, access_flags cần phải là 0x0807. Điều này không ảnh hưởng tới giá trị modifier ta đã chọn vì
- 0x0807 & 0x0001 != 0
- 0x0807 & ACC_INTERFACE & ACC_ABSTRACT & ACC_ANNOTATION & ACC_ENUM = 0
Với tag id = 08 => String info
Tới đây ta đã có thể hoàn toàn control được access_flags và 1 byte của this_class:
Trường this_class chiếm 2 bytes và thực chất chỉ là index trỏ tới Constant Pool, xét ví dụ sau:
this_class trong ví dụ trên có giá trị 0x0004, đồng nghĩa với việc trỏ tới Constant #04, ở đây chính là Class “step1”,
Nội dung bên trong Constant Pool đã hoàn toàn bị kiểm soát nên ở đây this_class có thể thoải mái trỏ tới bất kỳ đâu.
Như đã nói ở phần đầu, ta có thể sử dụng tag id = 6 để thêm một số double vào Constant, và sẽ có tiếp 8 bytes có thể để thao túng!
Theo đó this_class sẽ có dạng:
- 0x??06
Để giảm thiểu dung lượng của Class, giá trị này cần phải bé nhất có thể, trong đó thì 0x0006 đã bị sử dụng cho Class khác
=> Giá trị có thể chọn là this_class = 0x0106 (index số #262 trong Constant Pool)
=> Giá trị Constant được sử dụng để tạo ra access_flags và một phần this_class là String_info: 0x0701
Với String_info 0x0701 nghĩa là trỏ tới Constant Utf8 thứ 0x0701 trong Constant Pool. Như vậy giá trị của access_flags cũng phần nào đó ảnh hưởng gián tiếp tới độ dài của Constant Pool hợp lệ.
Quay trở lại với this_class = 0x0106,
=> theo đó thì tại index 0x0106 của Constant Pool phải là một giá trị Class_info hợp lệ
Tuy nhiên giá trị này không thể là một Class đã tồn tại trong Constant Pool,
Với phương thức khai thác hiện tại, các bytecode sau khi compile sẽ được gọi tới defineClass() để khởi tạo Class, nếu như Class này đã tồn tại trước đó trong JVM thì sẽ bị conflict khi defineClass() và không thể tiếp tục.
Để giải quyết việc này, tác giả đã xử lý bằng cách load những class mà không được reference tới khi parse XSLT.
Do cơ chế lazy load Class của Java, nó sẽ chỉ load Class vào JVM khi được dùng tới, khi defineClass() các Class này chưa được gọi tới nên hoàn toàn có thể ghi đè mà không gây ra việc conflict!
Và ở đây Class được sử dụng là com.sun.org.apache.xalan.internal.lib.ExsltStrings
Bằng cách khởi tạo hoặc gọi tới method của Class này, nó sẽ được load vào Constant Pool, tuy nhiên cũng cần phải sắp xếp thứ tự các Constant trước đó cho hợp lý để có thể khớp vào đúng vị trí #262 trong Constant Pool
#super_class
Trường tiếp theo cần xem xét tới là super_class, trường này cũng cần trỏ tới một Constant Class_info.
Với cách khai thác hiện tại, mục đích khởi tạo của Class là để có thể được gọi newInstance() tại TemplatesImpl.getTransletInstance()
Để thỏa mãn điều này thì bắt buộc Class phải extends AbstractTranslet class:
Class này đã có sẵn trong Constant Pool nên super_class = 0x0006
Tiếp tục các field phía sau đó là interfaces_count, interfaces, fields_count, fields.
Các field này không cần thiết phải có giá trị nên chỉ cần 2 bytes để set interfaces_count = 0 và 2 bytes để set fields_count = 0
#methods_count
Như đã đề cập ở phần trước, cách thức khai thác hiện tại đang lợi dụng việc modify code Constructor của Class và sẽ được trigger khi gọi tới Class.newInstance().
Xét tới Constant ta đang lợi dụng:
0x06 0006 0000 0000 ????
Constant này xác định giá trị 1 nửa của this_class, và sẽ kết thúc tại methods_count
Nghĩa là trường phía ngay sau đó, methods[0].access_flags, sẽ có giá trị = 0x06?? (do hiện tại ta đang sử dụng Constant double để thao túng các giá trị)
Với access_flags = 0x06?? & ACC_ABSTRACT != 0,
Một abstract method không có body, do đó mà không thể được sử dụng để thêm vào Constructor mong muốn được.
- Side note:
Ở bytecode level, non-abstract Class không cần phải implement các abstract method
Cũng ở bytecode level, một abstract Method có thể tồn tại trong non-abstract Class
=> Do đó mà số lượng Method cần thiết = 2
=> Tới đây, lại một Constant nữa được hoàn thiện:
Constant_double = 0x06 0006 0000 0000 0002
Sử dụng script python sau (được cung cấp bởi tác giả) để generate ra số cần sử dụng:
Ở đây ta có giá trị của 0x06 0006 0000 0000 0002 sau khi generate:
#methods
Mỗi method sẽ được biểu diễn theo cấu trúc sau:
methods[0]
Như đã đề cập ở phía trước, với method đầu tiên thì access_flags = 0x06?? nên sẽ là một abstract method, nửa sau của access_flags thì không quá quan trọng nên sẽ chọn ACC_PUBLIC cho dễ xử lý.
=> access_flags = 0x0601
- methods[0].name_index: giá trị này có thể trỏ tới bất kỳ abstract Method nào của parent Class, trong exploit trên p0 cũng như trên noah, các tác giả đều sử dụng tới transferOutputSettings (thực ra là trỏ tới Utf8_info nào cũng đc) ở đây là #71 (0x0047)
- methods[0].descriptor_index: cũng như trên, để không gây ra rắc rối thì mình sử dụng luôn descriptor_index của transferOutputSettings tại index #72 (0x0048)
- methods[0].attributes_count: khai báo số lượng các attribute có trong method này
Một attribute có cấu trúc như sau:
Với attribute table:
Trong đó attribute Code là thứ ta cần quan tâm, attribute này có chứa các bytecode intructions của một method. Mỗi Code attribute có cấu trúc sau:
Lấy ví dụ với một đoạn code java đơn giản như sau:
Sau khi compile sang bytecode:
Với attribute_name_index = 10, tương ứng với Utf8_info = Code, để xác định attribute này là Code. code_length = 17 thì sẽ có tương ứng 17 bytecodes sau đó
Trong trường hợp attribute_name_index không trỏ tới một attribute name hợp lệ, dữ liệu phía sau đó sẽ được parse theo một định dạng chung nào đó (cái này cũng ko hiểu ông tác giả nói tới gì luôn?!)
Điều cần quan tâm ở đây là chúng ta có thể sử dụng “tính năng” này để làm hợp lệ các đoạn dữ liệu thừa phía sau (thứ mà sẽ cần tới để làm cho Constant Pool bị truncated),
- Tiếp tục với methods[0].attributes_count, vì method đầu tiên không được sử dụng vào mục đích gì nên sẽ chỉ cần một attribute để làm hợp lệ các dữ liệu phía sau đó.
- Field tiếp theo cần xem xét là attribute_name_index,
Với Constant Double đang được sử dụng là:
0x06 01 00 47 00 48 00 01 ??
Như vậy bắt buộc attribute_name_index phải có dạng: 0x??06 (hiện tại đang sử dụng Constant double để build Class)
Với 0x0106 đã được trỏ tới Class, do đó ở đây sẽ chọn giá trị thấp nhất có thể, đó là 0x0206
=> Constant double: 0x06 01 00 47 00 48 00 01 02
- Tiếp theo là attribute_length, dựa vào attribute_length có thể quyết định được bao nhiêu bytes phía sau sẽ được sử dụng
Constant double hiện tại:
0x06 ?? ?? ?? ?? ?? ?? ?? ??
Tạm để đó và xét tiếp tới method tiếp theo
methods[1]
Đây chính là Constructor mà ta cần inject malicious code, do đó mà access_flags bắt buộc phải = ACC_PUBLIC
=> access_flags = 0x0001
Như vậy, quay trở lại với methods[0].attributes[0].attribute_length cần phải đủ dài để “nuốt” được 1 byte chứa tag của Constant double phía sau.
Ở đây ta có thể sử dụng attribute_length = 0x00000005 (4 byte)
Và tiếp theo sau đó là các junk bytes là các attributes của methods[0].attributes[0].attribute,
Constant double khi đó sẽ trở thành:
0x06 00 00 00 05 AA BB CC DD
Với attribute_length = 5 như trên, chỉ có attribute AA, BB, CC, DD là tồn tại trong Constant double này,
Khi ta khai báo Constant double mới, với
0x06 ?? ?? ?? ?? ?? ?? ?? ??
Thì tag id 0x06 sẽ là attribute cuối cùng của methods[0].attributes[0].attribute, điều đó giải thích tại sao phía trên nó được gọi là “nuốt”
Tới đây, Class của chúng ta đã có dạng như sau:
Tiếp tục với methods[1], access_flags đã được set = 0x0001, Constant double hiện tại là:
0x06 00 01 ?? ?? ?? ?? ?? ??
- methods[1].name_index: trỏ tới Constant Utf8 có nội dung <init>, trong poc của thanat0s, tác giả đã tự khai báo Utf8 constant và refer tới:
Bắt buộc phải làm vậy là do Constructor được compile sau cùng, không thể sử dụng trong payload này!
- methods[1].descriptor_index: trỏ tới ()V
- methods[1].attributes_count = 0x0003, ở đây sử dụng tới 3 attribute để tạo thành method cuối cùng này
Với các attributes như sau:
- attributes[0]: được sử dụng để nuốt tag của Constant double
- attributes[1]: được sử dụng để chứa Code attribute
- attributes[2]: phần này sẽ được dùng để hợp pháp hóa tất cả các ký tự phía sau, bao gồm các thông tin của Class gốc cũng như các payload truncate
Tại đây Constant double tiếp theo đã được hoàn thiện:
0x06 00 01 00 8f 00 1e 00 03
Xét methods[1].attributes[0],
Trường attribute_name_index chiếm 2 bytes đầu tiên của struct này, và đây cũng là vị trí bắt đầu của một Constant double mới, từ đó bytes đầu tiên sẽ là 0x06.
Như vậy giá trị của attribute_name_index bắt buộc phải lớn hơn hoặc bằng 0x0600 (1536), giải thích cho việc tại sao kích thước của Constant Pool phải lớn như vậy.
Ở đây sẽ chọn giá trị attribute_name_index nhỏ nhất = 0x0600 trỏ tới một Utf8 Constant, vì attributes[0] được tạo ra để nuốt tag id double phía sau nên việc hoàn thiện cũng tương tự như methods[0].attributes[0]
Constant double hoàn thiện như sau:
0x06 00 00 00 00 04 AB CD EF
Tiếp tục với methods[1].attributes[1], do tag id của Constant double mới đã bị nuốt bởi attributes[0], tại đây ta có thể set attribute_name_index tới một vị trí tùy ý.
Và attributes[1] được coi là phần Code của chương trình nên attribute_name_index cần trỏ tới Utf8 Constant #77:
Khi đó, attribute Code cần có cấu trúc như sau:
Trong đó thì attribute_length và code_length cần tính sau khi hoàn thiện phần nội dung của code.
- max_stack = 0x00FF: max stack của các operand
Constant double tạm thời tại đây:
0x06 00 4d 00 00 00 2e 00 ff
(Tới đây thì có thể bạn đọc đã khá hiểu về cái ý tưởng sử dụng double Constant này rồi nên mình sẽ không tiếp tục nói về đoạn này nữa nhé)
- max_locals = 0x0600: kích thước cần thiết cho local variables, 0x0600 là hơi lớn so với thực tế, tuy nhiên vẫn cần 0x06 để làm tag id cho Constant double
- exception_table_length = 0x0000
- attributes_count = 0x0000: giá trị này chỉ dùng để làm Line Number cho code, không cần thiết phải sử dụng
Tiếp theo là tới phần methods[1].attributes[1].code:
Bytecode Instructions của java khá là đơn giản nên chỉ cần tạo một file source code java rồi compile và copy sang là được.
Ví dụ:
Sau khi compile sẽ được các bytecodes như sau:
Instructions và Opcode tương ứng:
- May mắn là 0x00 có thể được sử dụng làm nop instruction,
- Với tag id = 6 tương ứng với iconst_6, cần đi kèm với một lệnh istore để không bị chiếm mất stack, ở đây sẽ sử dụng tới istore_1.
- Sử dụng tới ldc_w thay cho ldc để tăng phạm vi load Constant Pool
- goto không cần thiết sử dụng tới vì không có exception table
Dựa vào đó có thể viết lại bytecodes như hình dưới để vẫn là các double Constant hợp lệ:
Để khai báo Methodref, sử dụng tới mẫu XSLT sau:
Tuy nhiên một vấn đề được đặt ra là làm sao để thêm Methodref của AbstractTranslet.<init>, vì khi khai báo constructor, bắt buộc sẽ phải gọi tới parent constructor, và trong quá trình compile, các Constructor luôn luôn được compile cuối cùng, cho nên vị trí Methodref của AbstractTranslet.<init> sẽ luôn ở cuối của Constant Pool. Gây ra khó khăn khi Refer do Constant Pool hiện tại đã bị truncated!
Trong bài viết của thanat0s, tác giả cũng đã tìm ra cách xử lý rất gọn, đơn giản bằng cách gọi tới Constructor của AbstractTranslet là xong:
Bởi vì AbstractTranslet là một abstract class, nên khi gọi tới “new()” để tạo instance mới thì nó bị mắc kẹt ở đây, chờ để tìm Methodref của AbstractTranslet.<init>.
Tuy nhiên trong FunctionCall.findConstructors(), Constructor của AbstractTranslet đã được lấy ra bằng Reflection API:
Trong FunctionCall.translate() thì lại không kiểm tra lại method call trong XSLT vừa rồi có phải là từ Abstract Class hay không
Từ đó mà vấn đề về Methodref tới Constructor của abstract class đã được xử lý. (Ko hiểu làm sao mà hắn nghĩ ra cái trò này được luôn)
Như vậy là đã xử lý xong được phần code, phần còn lại đó là methods[1].attributes[2], phần này được thêm vào để dọn dẹp các dữ liệu thừa phía sau.
Việc này có thể được thực hiện bằng cách điều chỉnh attribute_length sao cho phù hợp, được tính từ các junk attribute phía sau: 0x12, 0x34, 0x56, 0x78 … cho tới 10 bytes cuối cùng của file thì dừng lại, do các bytes này được sử dụng để cho một số attribute khác của ClassFile và SourceFile
.
.
PoC: https://gist.github.com/thanatoskira/07dd6124f7d8197b48bc9e2ce900937f
Tuy nhiên poc này chỉ hoạt động trên lab, cần thêm một số sửa đổi nữa thì mới có thể đem ra sử dụng vào các trường hợp cụ thể.
Big thanks to thanat0s, _fel1x for this very valuable research and writeup!
Ref:
- http://noahblog.360.cn/xalan-j-integer-truncation-reproduce-cve-2022-34169/
- https://bugs.chromium.org/p/project-zero/issues/detail?id=2290&continueFlag=5f0a104405cabc4e1e6027013da73bfc
__Jang__