Atlassian Confluence Pre-Auth RCE (CVE-2021–26084) và câu chuyện về “điểm mù” khi tìm bug
WFH khiến cho khái niệm về thời gian dần như mờ nhạt đi, lôi cuốn lịch ra và xem: “Chà! thế là đã quá nửa năm rồi!”.
Lôi blog ra thì bài gần nhất mới có hơn 1 tháng trước, sao mình lại thấy như đã cả nửa năm rồi không viết lách gì. Có lẽ cũng vì công việc tất bật, hay là khép mình bên trong 4 bức tưởng lâu quá đã làm ý chí, suy nghĩ và ý tưởng của con người ta mòn dần đi.
Những ngày cuối tháng 8, sự betak này dần được xua tan sau những dự án cũng như job khá nhẹ nhàng, và thêm đó là trên twitter xuất hiện một bug đáng để lưu tâm: CVE-2021–26084 Atlassian Confluence Pre-Auth RCE.
Đây là 1 cơ hội tốt để “Escape this fuking loop”, bởi đã khá lâu rồi chưa có 1 bug critical nào đáng được lưu tâm cả.
Atlassian Confluence được khá nhiều tổ chức lớn (và nhỏ) sử dụng làm nơi để lưu trữ tài liệu về Product, các tài liệu vận hành hệ thống, tài liệu mật …
( ͡° ͜ʖ ͡°) Do vậy đây cũng là một miếng bánh khá thơm bên cạnh những target nặng mùi như Exchange, Weblogic …
#SETUP
Phiên bản mình sử dụng để làm lab là 7.12.4
Việc setup và debug cũng khá đơn giản,
Bản setup có thể down tại đây: https://www.atlassian.com/software/confluence/download-archives
Cần phải register 1 acc để lấy license trial.
Quá trình setup debug mình đã nói quá nhiều trong các bài trước rồi, các bạn có thể tham khảo và tự setup cho môi trường này.
#BUG
Ngay khi release advisory, bên cạnh bản vá chính thức, Atlassian cũng cung cấp luôn một bản hot patch tại đây: https://confluence.atlassian.com/doc/files/1077906215/1077916296/2/1629936383093/cve-2021-26084-update.sh
Soi hot patch có thể thấy một số file sau được thay đổi:
Bên cạnh đó mình cũng down luôn bản 7.12.4 và 7.12.5 để diff, kết quả được như vậy:
Các bản vá chỉ thay đổi một số chi tiết rất nhỏ trong các file template như sau:
Original:
#tag ("Hidden" "name='queryString'" "value='$!queryString'")
Patched:
#tag ("Hidden" "name='queryString'" "value=queryString")
Dễ dàng có thể thấy ở đây, thay vì sử dụng nối biến thì đã chuyển qua dùng binding param.
Macro #tag trong Velocity được sử dụng để gen ra các “input” tag, ví dụ với đoạn template trên, output html sẽ là:
<input type="hidden" name="queryString" value="<value>"/>
Cách sử dụng của #tag nôm na là như vầy:
#tag (“attribute1” “attribute2” “attribute3”)
Các attribute này trong quá trình render sẽ được lấy ra tại AbstractTagDirective.applyAttributes():
Kết quả trong hình trên tương ứng với khai báo #tag như sau:
#tag ("Hidden" "name='queryString'" "value='ahihihi'")
Mò mẫm xem thêm những file được patch, vô tình thấy ở file “content-editor.vm” có dòng comment như sau:
Nghĩa là đội dev cũng đã biết được rằng macro #tag sẽ thực thi cái giá trị của tham số như 1 biểu thức OGNL. Đem so sánh lại với những thông tin đã public về bug này thì mình khá chắc lỗi nằm tại các attribute “value=” của macro #tag.
Tuy nhiên mình đã thử fuzz qua khá nhiều payload public về SSTI, nhưng đều không ăn thua. Cần phải có một sự khác biệt gì đó nữa mới có thể trigger được điểm G!
Tiếp tục follow quá trình parse macro #tag,
Sau khi các attribute được lấy ra bởi method AbstractTagDirective.applyAttributes(), nó tiếp tục được truyền vào AbstractUITag.doEndTag() -> AbstractUITag.evaluateParams():
Tại đây, giá trị của attribute “value” sẽ được truyền tiếp vào WebWorkTagSupport.findValue().
Giá trị này tiếp tục được truyền vào OgnlValueFinder.findValue() với dạng expression (biểu thức):
Qua tham khảo một số thông tin trên google về OGNL Injection, mình được biết rằng OgnlValueFinder.findValue() chính là điểm sink, điểm trigger và eval cái expression đó.
Vậy là ở đây đã đi đúng hướng ban đầu, tuy nhiên vẫn chưa đủ để trigger bug.
Tiếp tục debug vào SafeExpressionUtil.isSafeExpression()
Tại đây, expression đã truyền vào được compile, kiểm tra blacklist và đẩy vào cache.
Cần phải chú ý lại một chút, biểu thức chúng ta truyền vào là:
'ahihihi2' (có dấu ')
Qua bước compile, biểu thước được compile sang dạng ASTConst, nghĩa là chỉ constant không thôi,
Với dạng ASTConst này, khi được eval thì nó chỉ đơn thuần là return chuỗi đã được compile từ trước:
Đọc đến đây thì có thể nhiều người cũng mường tưởng ra cách để trigger bug này rồi chứ?
Suy nghĩ đơn giản như này: chỉ cần thêm 1 dấu ‘ vào expression để escape cái exp hiện tại rồi inject thêm đoạn RCE vào đúng ko ¯\_(ツ)_/¯.
Nhưng vấn đề ở đây là như thế này, đây là expression khi được thêm dấu ‘ từ input:
Dấu ‘ đã bị escape sang dạng html entity, điểm escape nó là tại HtmlAnnotationEscaper.annotatedValueInsert():
Hầu như tất cả các attribute đều phải đi qua đây và bị html encode,
Chính vì vậy mà làm nên cái điểm khác biệt cho bug này, mà đến bây giờ mới có người tìm thấy nó ╮(╯_╰)╭.
Sau khi thử đủ thể loại character trên đời thì mình vẫn stuck ở đây tận 3–4 ngày gì đó.
Cuối cùng thì vẫn phải nhờ đến sự trợ giúp “Gọi điện thoại cho người thân”, mọi betak đã được gỡ bỏ ( ( ͡° ͜ʖ ͡°) vứt hết liêm sỉ đi ăn xin).
Qua hint của một vài “hacker quốc tế”, mình biết được đoạn expression kia có thể escape được bằng unicode char ??? Một điều mà mình đã thử rất nhiều lần trước đó mà đều fail (ಥ _ ಥ), (hint như ko hint, một chút flashback lại cái thời còn chơi CTF …).
Lại tiếp tục 1 tgian mò mẫm nữa, và may mắn thay, payload lần này đã throw 1 exception khá thú vị:
EOF Exception được throw, điều này xảy ra đồng nghĩa với việc expression đã bị escape.
Trong đó payload mình sử dụng là “\u0027”.
Điều này khá là bất thường, do input nhận vào thông qua servlet rõ ràng là “\u0027”, cho tới khi truyền vào Ognl.parseExpression() thì nó vẫn là “\u0027”, vậy điểm nào ở đó trong quá trình parseExpression(), “\u0027” đã được parse sang dấu ‘.
(Đoạn này giải thích hơi tối nghĩa, có thể debug thực tế sẽ rõ ràng hơn điều này). Từ đây đã có thể viết được payload Ognl Injection rồi, nhưng mình sẽ viết chi tiết hơn để note lại chi tiết tại sao cái bug này lại đặc biệt! Ai quan tâm tới phần viết payload có thể ké tiếp xuống dưới.
- Ognl Unicode Parsing
Tiếp tục bới sâu vào bên trong quá trình phân tích ngữ nghĩa của Ognl, F7 debug liên tọi cho tới khi tới OgnlParserTokenManager.getNextToken(), stacktrace tại thời điểm đó như sau:
Các character được lấy vào bởi đoạn code “this.input_stream.readChar()”:
Trong quá trình debug, mình đã mặc định cho rằng “this.input_stream” là 1 InputStream default của Java, và step over nó trong quá trình debug.
(ಥ _ ಥ) Và nó cũng chính là điểm mù chết chóc trong quá trình phân tích lỗ hổng này.
Đi sâu vào nó nào,
Thoạt nhìn có thể thấy chức năng chung chung của nó là read 1 character rồi return, nhưng có một vài khác biệt trong quá trình xử lý.
Khi bắt gặp “\u”, JavaCharStream.readChar() sẽ convert nó sang dạng char.
Điều đó giải thích cho lý do tại sao “\u0027” lại có thể bị sử dụng để escape OGNL Expression tại đây ( 🤷♀️ vulnerable by design?)
- Viết Payload
Như vậy là đã có thể escape được expression tại attribute “value” của macro #tag bằng payload ‘\u0027".
Payload đại khái như thế này:
abcd\u0027+{7*7}+\u0027
Mặc dù đã có thể escape được rồi, nhưng vẫn vướng phải một số blacklist của OGNL như sau:
Do đó mà các payload default có trên mạng đều không thể ốp vào ngay được:
Để bypass đoạn này thì payload phải thỏa mãn các điều kiện sau:
- Không sử dụng các var nằm trong blacklist
- Không sử dụng khai báo biến
- Không access tới các property trực tiếp
- Các method không nằm trong blacklist
- …
Blacklist như trên cũng không khó để vượt qua lắm, mình với PeterJson mất đâu đó khoảng 5 phút để bypass to RCE và 2 tiếng để viết PoC có output tại http response ( ͡° ͜ʖ ͡°).
Tuy nhiên vào thời điểm hiện tại có khá nhiều server chưa được patch nên mình quyết định sẽ chưa public PoC, mà để làm 1 phần mở cho bạn đọc có thể nghiên cứu và tự viết lấy PoC của mình.
Như vậy sẽ thú vị hơn là đi hốt payload của người khác rồi dùng mà không biết trong đó có gì!
******BIG NOTE: ĐÂY LÀ LỖ HỔNG PRE-AUTHEN, KHÔNG CẦN USERNAME/PASSWORD GÌ HẾT
PATCH YOUR SERVER NOW!!!
PoC: https://youtu.be/VgAydg8A6QE
Thanks for reading!