Phân tích lỗ hổng ProxyLogon — Mail Exchange RCE (Sự kết hợp hoàn hảo CVE-2021–26855 + CVE-2021–27065)

Jang
11 min readMar 10, 2021

--

Tuần đầu tháng 3 vừa rồi có khá nhiều biến động trong giới bảo mật, 4 lỗ hổng 0day của Mail Exchange bị sử dụng trong thực tế để chiếm quyền điều khiển các server mail này. Thông tin chi tiết tại đây: https://www.volexity.com/blog/2021/03/02/active-exploitation-of-microsoft-exchange-zero-day-vulnerabilities/

Các lỗ hổng bị phát hiện trong đợt tấn công APT này là:

  • CVE-2021–26855: Mail Exchange Pre-Auth SSRF
  • CVE-2021–26857: Post-Auth Deserialization
  • CVE-2021–26858: Post-Auth arbitrary file write
  • CVE-2021–27065: Post-Auth arbitrary file write

Khi mới đọc tin này thì mình không có quan tâm gì mấy (nghĩ bụng: chắc nó trừ mình ra), chỉ khi mà người anh xã hội ở một tổ chức X đăng bài phân tích về ảnh hưởng của đợt tấn công này tới tổ chức đó thì mình mới chột dạ và bắt đầu đi tìm hiểu về các lỗ hổng này!

Mình biết là khi publish bài này sẽ gây ảnh hưởng tới nhiều đơn vị và tổ chức chưa (kịp) cập nhật bản vá vì lý do này hay vì lý do nọ,

Tuy nhiên, theo quan niệm của mình, đây cũng có thể là một cách để hối thúc họ vá lỗ hổng, thay vì để phơi ra internet và bị tổ chức APT nào đó cầm PoC thật chiếm quyền điều khiển server trước khi họ kịp nhận ra bị tấn công!

Để không mất thời gian thêm nữa thì mình bắt đầu bài phân tích luôn!

#DIFF PATCH

Như những gì chúng ta đã biết, các entrypoint được sử dụng trong lần này đều có dạng: “/ecp/x.js”. Thứ magic đi sau đó là làm sao mà chỉ với 1 request tới 1 file chưa từng tồn tại trên server lại có thể trigger được SSRF và gửi full request tới server backend.

Để làm rõ điều này hơn, cần phải thực hiện diff bản vá mà MS cung cấp để xem họ đã vá những gì trong lần này!

Trước đó mình chưa từng làm việc với dotNet bao giờ nên cũng mất khá nhiều thời gian để vào việc được.

Phiên bản mà mình chọn để nghiên cứu là Microsoft Exchange Server 2016 CU 18.

Patch cho CVE-2021–26855 có thể down tại đây: https://www.microsoft.com/en-us/download/details.aspx?id=102773

Phiên bản mà mình thực hiện để diff với patch mới nhất là bản patch được release ngay trước đó: 2020 Dec Patch — KB4593465

Bên trong các file update này có chứa các file binary/dll sẽ được patch, hoàn toàn có thể giải nén tất cả ra bằng công cụ nào đó, ở đây mình dùng 7zip:

Các file DLL được giải nén đa phần là được viết = dotNet, do đó ở đây mình sẽ sử dụng dnSpy để decompile TẤT CẢ file dll này ra dạng .cs, và sử dụng cho việc diff sau đó.

Để decompile tất cả file dll này thì chỉ đơn giản là kéo tất cả vào dnSpy cùng 1 lúc, sau đó bôi đen chọn tất cả và chọn “Export to Project” thôi:

Có một lưu ý nho nhỏ là trong setting của dnspy, phần Decompiler, bỏ tick mục “Show tokens, RVA …” để bỏ qua các comment về địa chỉ, RVA … gây khó khăn cho việc diff sau này!

Decompile mất tầm 30p sẽ xong cả 2 bản vá. Tuy nhiên chưa thể diff luôn được, do quá trình decompile sẽ có một số lỗi xảy ra hoặc là một số string mà developer để lại, cần phải dọn dẹp gọn gàng lại một lần nữa trước khi diff, ở đây là một ví dụ:

Quá trình diff cũng khá mất thời gian do có nhiều byte rác vẫn còn lại trong code,

#Spot the bugs (CVE-2021–26855)

Việc phát hiện lỗi bằng diff này dễ hơn nhiều so với các challenge #spotthebugs ở đâu đó trên mạng, sau khi diff một vòng mình phát hiện ra có một vài thay đổi quan trọng tại DLL: Microsoft.Exchange.FrontEndHttpProxy

Tại BEResourceRequestHandler, method ShouldBackendRequestBeAnonymous() được thêm mới:

BEResourceRequestHandler là class sẽ được dùng vào việc handle các request có dạng resource (các file có đuôi .js, .css, …)

Việc xác định class này có thể handler request được hay không dựa vào method BEResourceRequestHandler.CanHandle()

Đầu tiên là sẽ kiểm tra sự tồn tại của một cookie đặc biệt với method GetBEResouceCookie(). Method này sẽ lấy và trả về giá trị cookie “X-BEResource

Tiếp sau đó, BEResourceRequestHandler.IsResourceRequest() sẽ kiểm tra xem URL request có đuôi dạng các file resource hay không.

Giá trị “X-BEResource” sau đó tiếp tục được truyền vào BackEndServer.FromString() để xác định BackEnd Server cho request này!

BackEndServer.FromString() xử lý cookie X-BEResource như sau

Để thỏa mãn các điều kiện trên thì X-BEResource sẽ có dạng như sau:

X-BEResource=EXCHANGE2016~1942062522;

Giá trị này sẽ được dùng tới ngay sau đây,

Tại ProxyRequestHandler.BeginProxyRequest() sẽ gọi tới GetTargetBackEndServerUrl() để xác định uri sắp được forward tới (chức năng đúng như tên của Class luôn). Method GetTargetBackEndServerUrl() có nội dung như sau:

Đọc sơ qua có thể hiểu được nội dung của đoạn này có chức năng xây dựng lại url sẽ forward vào backend.

Trong đó Host của request được xác định bằng BackEndServer.Fqdn, kết hợp với BEResourceRequestHandler phía trên, giá trị BackEndServer.Fqdn này lại có thể control được bằng cookie X-BEResource.

Bằng sự kết hợp ngẫu nhiên đó, lỗ hổng SSRF CVE-2021–26855 ra đời — — — ( ͡° ͜ʖ ͡°).

Để khai thác, cần phải lươn lẹo và chỉnh sửa cookie này một chút, thành dạng như sau:

X-BEResource=EXCHANGE2016/owa/auth/logon.aspx?a=~1942062522;

Sau khi được xử lý qua ProxyRequestHandler, url cuối cùng được forward tới Backend có dạng như sau:

(đã hiểu lý do tại sao các PoC lại có dấu “?” hoặc “#” ở cuối url chưa?)

Như vậy là đã có thể gửi request thoải mái tới server Backend mà không bị giới hạn gì cả,

Tuy nhiên chúng ta vẫn còn bỏ quên chưa nhắc tới về vấn đề quan trọng nữa, SSRF thành công mà request này lại còn được Authen với quyền của system nữa chứ! ( ͡° ͜ʖ ͡°).

Việc quyết định request qua proxy có cần authenticate hay không được quyết định tại ProxyRequestHandler.PrepareServerRequest():

Với trường hợp các request được xử lý bởi class BEResourceRequestHandler, luồng chương trình sẽ thực thi vào nhánh else cuối cùng, nghĩa là cho phép Authenticated.

Chính điều đó đã làm cho lỗ hổng SSRF này trở nên đặc biệt hơn, M$ đã sửa sai bằng cách thêm method ShouldBackendRequestBeAnonymous() vào class BEResourceRequestHandler để không cho phép các request này được gửi cred vào backend!

Nói thì hơi lan man, luồng đi của 1 request trên Exchange server có dạng như sau:

Tiếp theo là tới lỗ hổng cho phép ghi file tùy ý CVE-2021–27065

Nghiên cứu đống attack log của các bài phân tích (recommend bài của crowdstrike, ref phía dưới), có thể thấy Attacker sử dụng tính năng ResetOAB trong Exchange admin center để write file:

Để khai thác lỗ hổng này, đầu tiên attacker sẽ cần một tài khoản Admin mail exchange, sau đó sửa param External URL trong Exchange admin > Servers > Virtual Directories

Sau khi đã set tham số “External URL”, việc cuối cùng cần làm là sử dụng tính năng ResetOABVirtualDirectories và nhập path của file cần ghi:

Ngay sau khi thực hiện, file đã được ghi vào đường dẫn đã định sẵn, với nội dung có thể điều khiển tùy ý

Như vậy là đã có thể write shell tùy ý, lỗ hổng này không có gì khó khăn lắm để phát hiện và thực thi.

#Sự kết hợp hoàn hảo

Hiện tại chúng ta đã có

  • 1 lỗ hổng SSRF với quyền system
  • 1 lỗ hổng ghi file tùy ý (cần quyền admin mail)

Có thể nhiều bạn cũng nghĩ ra, là sẽ kết hợp lỗ hổng SSRF này để truy cập vào entrypoint cho phép ghi shell tùy ý,

.

.

.

Tuy nhiên đời không như mơ,

Không hiểu vì lý do gì mà khi sử dụng bug SSRF để vào vào các entrypoint của Exchange admin thì đều bị xử lý như 1 request Un-Authenticated!

Đoạn này nghĩ thì đơn giản, nhưng mình và đồng nghiệp bị stuck tại đây suốt mấy ngày liền, đành phải đi tìm thêm trợ giúp từ ông anh xã hội — người đã viết bài điều tra về case kia.

Sau một hồi trao đổi chiêu thức qua lại và xem log thì đúng là có vấn đề gì với entrypoint /ecp/proxyLogon.ecp thật!

Với mỗi lần attack, đều có một request tới “/ecp/proxyLogon.ecp”, và sau đó các request tới ECP đều được thực hiện với quyền của user “Administrator@mailbox”.

(Tình cờ một chút, cũng vào ngày hôm đó, anh cam có đăng thông báo về tên của lỗ hổng này là “proxylogon” với website proxylogon.com.)

Dựa vào web.config trong folder “C:/Program Files/Microsoft/Exchange Server/V15/ClientAccess/ecp”, có thể xác định được class “Microsoft.Exchange.Management.ControlPanel.ProxyLogonHandler” đang handle các request tới entrypoint /ecp/proxyLogon.ecp:

Nội dung của class này khá đơn giản, có lẽ đây ko phải nơi xử lý chính.

Sau khi “Ctrl + Shift + F” một hồi, phát hiện ra được request được xử lý tại “Microsoft.Exchange.Management.ControlPanel .RbacSettings()“

Đoạn xử lý này có thể hiểu đơn giản như thế này:

B1: Server sẽ lấy phần body của request tạo thành SerializedAccessToken(), sau một hồi ngâm cứu các thứ thì mình đã tạo được phần body thỏa mãn có dạng như sau:

B2: Dựa vào serialized token vừa tạo được, server tiếp tục dùng nó để tạo thành định danh cho request hiện tại:

Và ngoài ra cần thỏa mãn thêm một số tham số đầu vào nữa, nếu thuận lợi thì server sẽ trả về cookie: ASP.NET_SessionIdmsExchEcpCanary, dùng để xác thực cho user vừa request!

Có thể hiểu đơn giản hơn như sau:

Đó chính là thứ đặc biệt làm nên dấu ấn của chain attack “proxyLogon” này.

Do bị hạn chế bởi câu chữ nên có thể chưa bộc tả đc hết từng góc cạnh, từng cái hay của chain này.

Bạn đọc nên bắt tay vào debug lại để hiểu rõ hơn cái hay của nó!

.

.

Vậy là đã có được session id và canary, việc cần làm tiếp theo là thay vào request SSRF write file ban đầu để thực thi thôi ¯\_(ツ)_/¯

Tuy nhiên vẫn đề vẫn tiếp tục diễn ra:

Với SID mình sử dụng để proxylogon - S-1–5–21–1525789613–2932220202–353317642–3102, đây là SID của một user bình thường, không có quyền gì với Exchange admin cả.

Sau khi quan sát một hồi, mình nhận ra là SID của admin với SID của người dùng thông thường không khác nhau nhiều lắm, chỉ duy nhất có phần ID cuối cùng là có sự khác biệt:

- SID của user john: S-1–5–21–1525789613–2932220202–353317642–3102
- SID của admin: S-1-5-21-1525789613-2932220202-353317642-500

Như vậy hoàn toàn có thể sử dụng một SID bất kỳ trong hệ thống để tìm được tài khoản administrator và chiếm quyền qua proxylogon!

Với việc chiếm quyền administrator thành công thì việc write shell tiếp theo không còn là vấn đề gì nữa, do đó mình sẽ không nói thêm nữa về nó:

.

.

Quay trở lại bên trên một chút, ta cần một SID hợp lệ của user bất kỳ trong hệ thống để leo quyền, vậy làm sao để lấy được SID này khi chỉ biết mỗi username?

Để giải quyết vấn đề này, cần phải sử dụng tới các entrypoint: /autodiscover/autodiscover.xml và /mapi/emsmdb

Tính năng tại entrypoint /mapi sẽ trả về SID khi xảy ra lỗi:

Phần input body của request có thể lấy bằng cách sử dụng entrypoint /Autodiscover/autodiscover.xml:

Vậy là đã hoàn thành mảnh ghép cuối cùng cho chain RCE của proxylogon, tóm gọn lại trong một hình như sau:

.

.

PoC video:

PoC payload:

https://gist.github.com/testanull/fabd8eeb46f120c4b15f8793617ca7d1

Trong quá trình debug mới phát hiện ra quá nhiều thứ tinh túy/hay ho tạo nên chain attack này. Bạn đọc nên bắt tay vào làm và debug trực tiếp để thấy cái hay trong đó!

Có thể vì lý do câu chữ nên không thể mô tả lại được hết sự tinh túy/sự trùng hợp may mắn/sự kết hợp hoàn hảo của các mảnh ghép nhỏ để tạo nên chain attack tuyệt vời như vậy!

Respect to “anh Cam” and DevCore!

Cảm ơn đồng nghiệp & người anh xã hội đã giúp mình trong quá trình nghiên cứu!

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

__Jang of VNPT ISC__

Ref:

--

--