Elastic · Report #1636382
Synthetics Recorder: Code injection when recording website with malicious content
How it was found
Summary
Hello team! Synthetics recorder has a quote function to escape user-controlled input, but in one particular scenario the escaping isn't enough and a malicious website can inject arbitrary code in the recorder session.
Description
The waitForNavigation event calls quote within the context of a multi-line comment (/* ... */) so we can break out of that without using the escaped characters (reference)
In a normal situation the code generated looks like this for a navigation event to https://example.com
page.waitForNavigation(/*{ url: 'https://example.com' }*/),
but it's possible to escape out of the comment without using single quotes (which would be escaped) with a specially crafted URL like https://example.com?q=*/require(child_process).exec(touch$IFS/tmp/haxx)/*
page.waitForNavigation(/*{ url: 'https://example.com?q=*/require(`child_process`).exec(`touch$IFS/tmp/dee-see`)/*' }*/),
The syntax highlighting here on HackerOne helps visualizing how that works. $IFS is used because spaces get encoded to %20.
It's possible to have code execution when the victim uses the test feature inside of the synthetic recorder but the code we're allowed to use is fairly limited because the require function isn't available. The maximum impact is when the user saves the recorded session as a project and executes it using the synthetic runner.
Steps To Reproduce
Preparation
Install the synthetics recorder (See https://github.com/elastic/synthetics-recorder/, I'm following the instructions to run it in development mode (nvm install; nvm use; npm install; npm run dev) but you could also download the binary on the releases page)
Reproduction
-
Start Synthetics Recorder and enter
http://deesee.xyz:4567in the text box where it says "Enter a Starting URL" -
Click "Start Recording"
-
A browser has opened, this website is a modified clone of my blog. Click the GitLab icon in the top right
{F1820934}
-
Close the browser window
In a normal Synthetics Recorder session there would be much more steps to record but here we only did what's necessary to trigger the issue.
-
Click the "Export" button and you'll see this code
step('Go to http://deesee.xyz:4567/', async () => { await page.goto('http://deesee.xyz:4567/'); await Promise.all([ page.waitForNavigation(/*{ url: 'https://gitlab.com/dee-see?query=*/require(`child_process`).exec(`touch$IFS/tmp/dee-see`)/*' }*/), page.click('[aria-label="GitLab"] svg') ]); });We can see the payload is in place. It's fairly obvious because we only recorded one step, but in a long recording session it would be buried deeper.
-
Click the "Export" button and save the file in a directory
-
In that directory run
npm init -y; npm install @elastic/synthetics; npx @elastic/synthetics . -
When the tests finished running observe that the
touch /tmp/dee-seecommand ran
Those last steps seem contrived, but that's how a synthetics test suite is setup and how a developer would make sure the session they just recorded would be integrated into their builds and whatnot.
Supporting Material/References:
{F1820942}
CVSS
Confidentiality and Integrity impact are High because of the arbitrary command execution. I also included Availability impact because those commands can shut down the system. I will concede though that Attack Complexity could be "very high" if that existed on the Attack Complexity scale. :)
Impact
Someone with control over the website's content can run arbitrary code where ever the synthetics recorded session will be re-executed.
Developer computers and CI systems come to mind. The most realistic attack scenario would be privilege escalation from within a company.