Những ngày tháng yên ả của tháng 5 và tháng 6, twitter không có gì nổi bật lắm ngoài những vụ drama về Ransomware, supply chain này nọ …

Tới những ngày cuối tháng thì sóng gió ập tới, trong đó có PrintNightmare và lỗ hổng trên.

Vì mức độ nghiêm trọng và tích cấp thiết, mình đi vào xem PrintNightmare trước, hiện tại sự việc đã xong xuôi thì mới có time quay lại để ngâm bug này đây.

Bài viết gốc của tác giả tại: https://portswigger.net/research/pre-auth-rce-in-forgerock-openam-cve-2021-35464

Bài này của mình cũng là phân tích về lỗ hổng trên, nhưng theo một cách nhìn, hướng tiếp cận khác so với bài gốc của tác giả!

.

ForgeRock AM/OpenAM là giải pháp cho phép quản lý việc đăng nhập, phân quyền các thứ …

Mà đây lại là thứ đâu tiên attacker gặp được và muốn đánh khi target vào một hệ thống nào đó. Nó là thứ mà ai cũng truy cập được, và ham muốn có một tài khoản hay cách nào đó để chiếm lấy.

Do đó mà trên chợ đen, những bug này thường được trả với giá rất hậu hĩnh.

Và cũng như 80% của những bài viết khác trong blog của mình, lỗ hổng này cũng là về Java Deserialization ( ͡° ͜ʖ ͡°).

.

.

#SETUP

Việc setup cho target này khá là đơn giản, download phiên bản có lỗ hổng tại: https://backstage.forgerock.com/downloads/get/familyId:am/productId:am/minorVersion:6.5/version:6.5.0/releaseType:full/distribution:war

Cài tomcat, ném file war vào folder webapps và … xong ( ͡° ͜ʖ ͡°)

#THE BUG

Theo bài viết của artsploit, entrypoint của lỗ hổng này là :

/ccversion/Version

Như khai báo trong web.xml, class đang handler entrypoint này là com.sun.identity.console.version.VersionServlet:

Đây là một servlet của thư viện JATO, thư viện này đã được 20 tuổi, và dường như hiện tại đã không còn được duy trì và phát triển nữa.

VersionServlet có mô hình thừa kế như sau:

Class VersionServlet không override các method service(), doPost(), doGET() mà trực tiếp sử dụng các method này của class cha ApplicationServletBase:

Trong bài viết gốc của tác giả, phần sink của lỗ hổng này là tại ViewBeanBase.deserializePageAttributes():

Param “jato.pageSession” sẽ được decode base64 và truyền vào Encoder.deserialize() để deserialize và khởi tạo lại object, trong đó content của Encoder.deserialize() như sau:

Với ApplicationObjectInputStream như sau:

Thật may mắn, class này đơn giản chỉ là thừa kế ObjectInputStream, và override method resolveClass() để cho phép mở rộng việc resolve các class được deserialize, chứ không hề có đoạn nào để hạn chế deserialize object tùy ý cả.

Không biết là do vô tình hay cố ý, mọi thứ đều sắp đặt để tạo ra một lỗ hổng Insecure Object Deserialization lead to RCE! ¯\_(ツ)_/¯

Khi bắt đầu phân tích lỗ hổng này, mình vẫn thắc mắc ở chỗ là làm sao từ VersionServlet -> ViewBeanBase.deserializePageAttributes().

Để tìm hiểu điều này, set 1 breakpoint tại ViewBeanBase.deserializePageAttributes() và gửi payload, mình có được call stack như sau:

Tại ApplicationServletBase.dispatchRequest() sẽ xác định “viewBean ”nào sẽ được tiếp tục gọi, trong trường hợp này “viewBean ”= com.sun.identity.console.version.VersionViewBean

com.sun.identity.console.version.VersionViewBean thừa kế TagsViewBeanBase

TagsViewBeanBase lại thừa kế ViewBeanBase

VersionViewBean TagsViewBeanBase không override các method invokeRequestHandler(), deserializePageAttributes(), do đó ở đây viewBean.invokeRequestHandler() sẽ gọi tới ViewBeanBase.invokeRequestHandler(), tương tự như ViewBeanBase.deserializePageAttributes().

Tiếp tục trace ngược call stack về method ApplicationServletBase.processRequest(), đây chính là vị trí xác định giá trị của “viewBean ” phía trên thông qua method getViewBeanInstance().

ApplicationServletBase.getViewBeanInstance() tiếp tục gọi getLocalViewBean() với tham số “pageName”

Trong đó “pageName” được lấy từ url path của request

“pageName” tiếp tục được concat với package Name

Sau khi concat sẽ trở thành dạng như sau:

com.sun.identity.console.version.VersionViewBean

Như vậy đã lý giải nguyên nhân tại sao lại từ VersionServlet.doGet() có thể trigger tới ViewBeanBase.deserializePageAttributes()

Tóm tắt về flow của bug này như sau:

#THE VARIANT

Dựa vào cách xử lý để lấy viewBean phía trên cùng với các servlet được khai báo trong web.xml, mình tìm ra rất nhiều trường hợp tương tự như sau:

Lấy một ví dụ FileUploaderViewBean,

Với mô hình thừa kế như sau:

Trong đó PageSessionStoreViewBase ConsoleViewBeanBase có override lại method deserializePageAttributes() với nội dung như sau:

Và trong ConsoleViewBeanBase không gọi tới super.deserializePageAttributes().

Do đó mà sink ViewBeanBase.deserializePageAttributes() không thể reach với các viewBean này.

Xem kỹ hơn method ConsoleViewBeanBase.deserializePageAttributes():

Tại đây, input data cũng được decode base64 và đưa vào IOUtils.deserialise() để khởi tạo lại object. Nội dung của method này như sau:

Tại đây, WhitelistObjectInputStream được sử dụng cho việc deserialize, nó có nhiệm vụ kiểm tra lại các class sẽ được khởi tạo lại, dựa vào một whitelist có sẵn:

Whitelist:

private static final List<String> FALLBACK_CLASS_WHITELIST = Arrays.asList("com.iplanet.dpro.session.DNOrIPAddressListTokenRestriction", "com.sun.identity.console.base.model.SMSubConfig", "com.sun.identity.console.service.model.SMDescriptionData", "com.sun.identity.console.service.model.SMDiscoEntryData", "com.sun.identity.console.session.model.SMSessionData", "com.sun.identity.shared.datastruct.OrderedSet", "com.sun.xml.bind.util.ListImpl", "com.sun.xml.bind.util.ProxyListImpl", "java.lang.Boolean", "java.lang.Integer", "java.lang.Number", "java.lang.String", "java.net.InetAddress", "java.util.ArrayList", "java.util.Collections$EmptyMap", "java.util.Collections$SingletonList", "java.util.HashMap", "java.util.HashSet", "org.forgerock.openam.dpro.session.NoOpTokenRestriction", "org.forgerock.openam.dpro.session.ProofOfPossessionTokenRestriction");

Do đó, khi gửi payload deserialize lên sẽ bị Reject và ko thể lợi dụng cho Insecure Deserialization:

Và thật đáng buồn là hầu như trong tất cả các biến thể ViewBean mình tìm được, đều thừa kếConsoleViewBeanBase, do vậy mà không tìm được thêm entrypoint nào khác để khai thác lỗ hổng này ╮(╯_╰)╭.

Tóm gọn lại chỉ có những entrypoint sau có thể sử dụng:

  • /ccversion/ButtonFrame
  • /ccversion/Masthead
  • /ccversion/Version

#BONUS PART

Trong bài viết ban đầu của tác giả, mình có thấy kêu là ko có outbound và phải write shell để exec command.

Tuy nhiên trong thực tế thì việc lead từ Deserialize -> Response Echo đã được làm khá nhiều, và trường hợp này cũng không khó lắm để thực hiện!

Ở đây mình sử dụng kỹ thuật được xây dựng bởi threedr3am (https://threedr3am.github.io/2020/03/20/%E5%9F%BA%E4%BA%8Etomcat%E7%9A%84%E5%86%85%E5%AD%98Webshell%E6%97%A0%E6%96%87%E4%BB%B6%E6%94%BB%E5%87%BB%E6%8A%80%E6%9C%AF/)

Và gadgetchain thì vẫn dùng y nguyên gadgetchain của tác giả (https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/Click1.java)

Chỉ có thay đổi một chút tại Click1.java#L72, thay vì sử dụng Gadgets.createTemplatesImpl(command) mình dùng Gadgets.createTemplatesImpl(TomcatEchoInject.class);

Với TomcatEchoInject được sử dụng tại đây

Và kết quả được như sau:

.

.

.

Như trên là một số thông tin mình học hỏi được trong quá trình phân tích lỗ hổng này, khuyến nghị bạn đọc nên setup và debug để hiểu hơn về lỗ hổng này.

Cảm ơn các bạn đã đón đọc!

__Jang__

675 lượt xem