Tản mạn về Struts2 OGNL Injection và một case XSS

Bài phân tích cách tìm một lỗ hổng Struts2 OGNL Injection bằng trace code, qua đó phát hiện lỗ hổng có thể dẫn đến tấn công XSS.

Trong bài viết về Struts2 OGNL Injection [1] trên blog của nhóm nghiên cứu bảo mật, tôi đã thực hiện nghiên cứu về bug CVE-2013-2251 nhằm để hiểu về luồng code lỗi bằng cách debug lỗi khi đã có public payload và một bài thông tin sơ bộ được mô tả trong CVE này từ Struts2. Có thể xem cách này là trace code từ entry point tới end point – với end point tôi cho ở đây là các sinks. Sinks ở đây hiểu nôm na là các đoạn code mà ta có thể nghi ngờ có khả năng mắc lỗi khi gọi tới (ví dụ OgnlUtil.getvalue() chẳng hạn). Sau quá trình debug, ta có thể biết được các sinks liên quan tới lỗ hổng OGNL Injection này.

Trong phần này, tôi sẽ thử thực hiện nghiên cứu lỗ hổng OGNL Injection bằng cách ngược lại với cách trước là thực hiện trace code từ end point tới entry point, cách làm này mới chính là cách để tìm ra một lố hổng bảo mật trong 1 project hay lib,…

Cái quan trọng để tìm ra bug chính là phải biết được sinks – những function nào có thể dẫn tới mắc lỗ hổng đó. Trong thời kỳ dịch nCovi có khái niệm về F0, F1, F2,… thì đối với tôi sinks nó cũng có tính chất tương tự như vậy. Chẳng hạn OgnlUtil.getvalue() là sink (tạm cho nó là F0) thì OgnlValueStack.getValue() nó sẽ là F1 vì trong OgnlValueStack.getValue() sẽ gọi tới OgnlUtil.getvalue(). Cứ thế ta sẽ có 1 cây các sink, ví dụ đó là TextParseUtil.translateVariables() (Fn – n là bao nhiêu thì tôi cũng ko nhớ 😊).

Việc trace code thì ta có thể code 1 công cụ nhỏ để tự động trace và hiển thị kết quả ( trước tôi có tham khảo ANTLR [2] trong việc hỗ trợ thực hiện phân tích cấu trúc file, không biết hiện tại có thư viện nào tốt hơn không, nếu có thì hi vọng bạn có thể comment để tôi biết). Tuy nhiên ở bài này thì tôi sẽ sử dụng luôn Netbean IDE để trace và tôi sẽ lấy ví dụ luồng trace với sink là TextParseUtil.translateVariables.

Struts2 OGNL Injection

Đầu tiên, tôi vẫn sẽ sử dụng project trong bài viết trước với phiên bản struts cũ là 2.3.12 và webserver là Tomcat 8.0.27.0. Nhảy luôn tới class TextParseUtil.java (lib xwork-core-2.3.12), function translateVariables(String expression, ValueStack stack). Thực hiện tìm các đoạn code có gọi tới function này sử dụng tính năng “Find Usages” của Netbeans

Kết quả trả về là cũng kha khá, và trong list này đảm bảo sẽ có một số CVE đã từng được công bố 😊

Công việc của chúng ta là cứ tay to trace code – lặp đi lặp lại cho đến khi ta thấy được điểm bắt đầu – entry point. Giống như việc trong mê cung và bạn phải tìm đường ra vậy. Đi đến cuối đường của ngã rẽ này mà ko thấy đường ra thì quay lại để đi vào ngã rẽ khác cho đến khi thấy cổng. 😐 (Cần chú ý xem các tham số truyền vào có thể control được không vì đơn giản control được thì mới OGNL Injection được).

Với danh sách trên, tôi tiếp tục trace với point là DefaultUrlHelper.java (lib Strut2-core-2.3.12), function cần tiếp tục trace là translateVariable(String input)

Có 2 kết quả gọi tới function này với tham số truyền vào là input

Tôi tiếp tục trace với translateAndEncode:

Để ý 2 tham số truyền vào là name và value. Tiếp tục …

Để ý tới luồng sinh giá trị tham số name được truyền vào function buildParameterSubstring()name <= entry <= iter <= params. Tham số này được truyền vào khi gọi function buildParametersString().

Tiếp tục trace code …

Function này là function buildUrl() – Tôi sẽ ghim đoạn này.

Tiếp tục với việc trace đối với buildUrl() function thì nó sẽ qua một số buildUrl function nữa, tham số truyền vào params vẫn đang có thể control:

Và sau đó là:

Trace tiếp với nhánh ServletUrlRender.java, tham số cần quan tâm là urlComponent.getParameters(). Để exploit khả thi thì các tham số phải control được.

 

Đến đây thì ta có thể thấy tham số cần quan tâm chính là parameters. Tham số này hoàn toàn có thể control, đơn giản là gửi tham số qua queryString trong request.

Trong quá trình lập trình giao diện thì Struts2 sẽ có những Component để hỗ trợ việc viết code:

Như vậy với luồng trace như trên thì khi nhảy tới class URL.java là chúng ta đã thấy được “cổng vào” rồi. Debug với URL tag sử dụng đoạn code sau:

<s:url includeParams=”get”>

</s:url>

Đặt breakpoint tại function end() của URL class và gửi HTTP request “http://localhost:8084/struts/?payload=OGNL_Here

Ta có thể thấy giá trị của parameters đã nhận đúng như ta mong muốn. Tiếp tục debug theo luồng code phân tích ở trên. Đến function translateVariables(), giá trị expression truyền vào là “OGNL_Here”.

Bây giờ sửa một chút phần request, tham số payload truyền vào giá trị là ${7*7}

“http://localhost:8084/struts/?payload=${7*7}”

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

Vậy là có thể OGNL injection rồi, thử “call calc” với request:

http://localhost:8084/struts/?payload=${(#_memberAccess=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(@java.lang.Runtime@getRuntime().exec(‘calc.exe’))}

Host header injection leads to XSS

Vậy là RCE đã xong, đây là bug cũ và đã được khắc phục. Trong quá trình trace thì tôi cũng thấy lại một số các bug cũ khác nhưng tôi chọn phân tích bug trên vì có lẽ cũng chưa có bài phân tích.

Trở lại với function buildUrl() mà tôi ghim ở trên, hãy xem phần code của nó:

Và:

Khi sử dụng URL tag với các attribute forceAddSchemaHostAndPort hoặc schema, giá trị link sẽ append với request.getServerNameRequest.getServerName sẽ lấy giá trị host header trong HTTP request => ta có thể control được. Tiếp tục trace xem result này sẽ được xử lý như thế nào tiếp theo:

Như vậy là result sẽ được truyền vào function write() của class writer (Java.io.Writer), đơn giản là dùng để write ra một string – đoạn code trên sẽ write ra url. Nếu ai có kinh nghiệm về secure coding thì sẽ biết sử dụng function này sẽ có nguy cơ mắc lỗ hổng XSS. Để kiểm chứng tôi sẽ gửi 1 request với Host header là XSS payload. Sửa lại một chút code test:

<s:url includeParams=”get” forceAddSchemeHostAndPort=”true”>

</s:url>

 

Thực hiện gửi HTTP request:

Vậy là XSS thành công. Với lỗ hổng này, Struts team đã phản hồi với tôi rằng đây không phải là lỗ hổng phía Struts mà là lỗ hổng của Servlet Container – cụ thể ở đây là Apache Tomcat. Các phiên bản gần đây của tomcat đã bổ sung code validate giá trị host header, sẽ trả về response error-code 400 trong trường hợp giá trị host header không hợp lệ.

Tuy nhiên với những ứng dụng sử dụng Struts2 cùng với Tomcat phiên bản chưa bổ sung validation vẫn có thể mắc lỗi XSS trong trường hợp sử dụng URL tag như trên. Và cho dù Tomcat đã bổ sung validation giá trị host header thì vẫn có khả năng bị khai thác host header injection để chèn một malicious domain/IP để tấn công người dùng (Ví dụ Host header injection tại các chức năng như quên/reset mật khẩu cho phép takeover tài khoản người dùng,…). Phía Struts team phản hồi sẽ xem xét có thể khắc phục dưới hình thức bổ sung security hardening chứ không coi đây là lỗ hổng của Struts.

Theo quan điểm của tôi thì host header, cookies, querystring, postdata hay bất kỳ header khác thì được xem là entrypoint. Lập trình viên khi lấy giá trị bất kỳ entrypoint nào gửi từ client đều cần phải thực hiện validate đảm bảo dữ liệu hợp lệ. Do vậy việc khắc phục lỗi nên thực hiện ở chính đoạn code mắc lỗi (trong struts code) chứ ko phải là ở servlet container. Ngoài ra tôi có kiểm tra với một Servlet Container khác là Glassfish phiên bản mới nhất thì vẫn khai thác XSS thành công, nghĩa là Glassfish không thực hiện validation giá trị host header này => Vậy liệu đây là bug của Glassfish hay Struts?

Hi vọng bài phân tích sẽ mang đến cho bạn đọc kiến thức bổ ích và thú vị, hiểu rõ thêm về quá trình trace code để tìm ra lỗ hổng bảo mật. Hẹn gặp lại ở bài phân tích tiếp theo.

P/S: GTSC Redteam và Blueteam đang tuyển dụng các nhân tài quan tâm và yêu thích lĩnh vực bảo mật. Nếu bạn có hứng thú, vui lòng liên hệ với chúng tôi qua email van.do@gteltsc.vn

Tác giả: @buxu

Reference:

[1]: https://medium.com/nightst0rm/ph%C3%A2n-t%C3%ADch-struts2-ognl-injection-cve-2013-2251-10cdfea9d47b

[2]: https://www.antlr.org/