VYPR
Unrated severityNVD Advisory· Published May 28, 2026

CVE-2026-30761

CVE-2026-30761

Description

An arbitrary file upload vulnerability in the pages/admin.uploadmapimg.php component of SourceBans Material Admin v1.1.6 allows attackers to execute arbitrary code via uploading a crafted image file.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

SourceBans Material Admin ≤1.1.6 has an arbitrary file upload in pages/admin.uploadmapimg.php, allowing authenticated attackers with a specific permission to achieve RCE.

Vulnerability

The vulnerability resides in the pages/admin.uploadmapimg.php component of SourceBans Material Admin versions prior to the fix commit fb18342 (i.e., version <1.1.6@fb18342) [2]. The upload handler only checks the client-supplied Content-Type header and PHP upload error code, trivially bypassed by an attacker [2]. By crafting a request with Content-Type: image/jpeg and uploading a .php file (or a .htaccess to allow execution), an attacker can place arbitrary files into the web-accessible images/maps/ directory without any extension validation or content sanitization [2].

Exploitation

An attacker must be authenticated and possess the ADMIN_ADD_SERVER flag [2]. The attacker sends a POST request to admin.uploadmapimg.php with a file whose Content-Type is set to image/jpeg but contains PHP code [2]. After uploading, a second upload overwrites or creates a .htaccess file that enables PHP execution in the upload directory [2]. No user interaction beyond authentication is required, and the attack is carried out over the network [2].

Impact

Successful exploitation leads to remote code execution as the web server user, enabling full site compromise, unauthorized access to the database and RCON credentials, modification of application files, and potential lateral movement to connected game servers or other hosts [2]. The CVSS v3.1 score is 8.8 (High) with vector AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H [2].

Mitigation

The vulnerability was patched in commit fb18342 of the vendor repository, and the fix was reported as available by October 29, 2025 [2]. Users should update to the latest version of SourceBans Material Admin (≥1.1.6@fb18342). No workarounds have been documented for unpatched versions [2]. The project's security advisory recommends contacting the maintainer via email for any vulnerability reports [3].

AI Insight generated on May 28, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

2
fb18342410e6

Fix insecure map image upload, add PNG/WebP support

https://github.com/sb-materialadmin/webA1mDevFeb 25, 2026via body-scan
3 files changed · +199 74
  • images/maps/.htaccess+6 0 added
    @@ -0,0 +1,6 @@
    +php_flag engine off
    +RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .phps
    +<FilesMatch "\.(php|phtml|php3|php4|php5|php7|phps|cgi|pl|asp|aspx|py|rb|sh)$">
    +    Order Deny,Allow
    +    Deny from all
    +</FilesMatch>
    \ No newline at end of file
    
  • includes/system-functions.php+35 26 modified
    @@ -719,35 +719,44 @@ function PageDie()
     	die();
     }
     
    -function GetMapImage($map, $game=false)
    +function GetMapImage(string $map, ?string $game = null, array $extensions = ['jpg', 'png', 'webp']) : string
     {
    -	if($game){
    -		if(@file_exists(SB_MAP_LOCATION . "/".$game."/" . $map . ".jpg"))
    -			$map_icon = "images/maps/".$game."/" . $map . ".jpg";
    -		else{
    -			if(@file_exists(SB_MAP_LOCATION . "/" . $map . ".jpg"))
    -				$map_icon = "images/maps/" . $map . ".jpg";
    -			else
    -				$map_icon = "images/maps/nomap.jpg";
    -		}
    -	}else{
    -		if(@file_exists(SB_MAP_LOCATION . "/" . $map . ".jpg"))
    -			$map_icon = "images/maps/" . $map . ".jpg";
    -		else
    -			$map_icon = "images/maps/nomap.jpg";
    -	}
    -	return $map_icon;
    -}
    +    // Sanitize inputs: remove any characters that are not letters, numbers, underscore, dash, dot
    +    // This prevents directory traversal and other injection.
    +    $safe_map = preg_replace('/[^a-zA-Z0-9._-]/', '', $map);
    +    if ($game !== null) {
    +        $safe_game = preg_replace('/[^a-zA-Z0-9._-]/', '', $game);
    +    } else {
    +        $safe_game = '';
    +    }
     
    -/*
    -function GetMapImage($map)
    -{
    -	if(@file_exists(SB_MAP_LOCATION . "/" . $map . ".jpg"))
    -		return "images/maps/" . $map . ".jpg";
    -	else
    -		return "images/maps/nomap.jpg";
    +    // Base directory for map images (filesystem path)
    +    $base_dir = rtrim(SB_MAP_LOCATION, '/') . '/';
    +
    +    // Relative URL prefix (adjust if your images are served from a different base)
    +    $url_prefix = rtrim(str_replace($_SERVER['DOCUMENT_ROOT'], '', SB_MAP_LOCATION), '/') . '/';
    +
    +    // Search for existing file
    +    foreach ($extensions as $ext) {
    +        // First try in game subfolder if applicable
    +        if ($safe_game) {
    +            $fs_path = $base_dir . $safe_game . '/' . $safe_map . '.' . $ext;
    +            if (file_exists($fs_path)) {
    +                return $url_prefix . $safe_game . '/' . $safe_map . '.' . $ext;
    +            }
    +        }
    +
    +        // Then try in root maps folder
    +        $fs_path = $base_dir . $safe_map . '.' . $ext;
    +        if (file_exists($fs_path)) {
    +            return $url_prefix . $safe_map . '.' . $ext;
    +        }
    +    }
    +
    +    // Fallback to nomap.jpg (make sure nomap.jpg exists in the maps folder)
    +    return $url_prefix . 'nomap.jpg';
     }
    -*/
    +
     function CheckExt($filename, $ext)
     {
     	if (is_array($ext)) {
    
  • pages/admin.uploadmapimg.php+158 48 modified
    @@ -1,67 +1,177 @@
     <?php
     // *************************************************************************
     //  This file is part of SourceBans++.
    -//
    -//  Copyright (C) 2014-2016 Sarabveer Singh <me@sarabveer.me>
    -//
    -//  SourceBans++ is free software: you can redistribute it and/or modify
    -//  it under the terms of the GNU General Public License as published by
    -//  the Free Software Foundation, per version 3 of the License.
    -//
    -//  SourceBans++ is distributed in the hope that it will be useful,
    -//  but WITHOUT ANY WARRANTY; without even the implied warranty of
    -//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    -//  GNU General Public License for more details.
    -//
    -//  You should have received a copy of the GNU General Public License
    -//  along with SourceBans++. If not, see <http://www.gnu.org/licenses/>.
    -//
    -//  This file is based off work covered by the following copyright(s):  
    -//
    -//   SourceBans 1.4.11
    -//   Copyright (C) 2007-2015 SourceBans Team - Part of GameConnect
    -//   Licensed under GNU GPL version 3, or later.
    -//   Page: <http://www.sourcebans.net/> - <https://github.com/GameConnect/sourcebansv1>
    -//
    +//  (C) reserved author rights as in original.
     // *************************************************************************
    +
     header("Content-Type: text/html; charset=utf-8");
    +
     include_once("../init.php");
     include_once("../includes/system-functions.php");
    +
     global $theme, $userbank;
     
    -if (!$userbank->HasAccess(ADMIN_OWNER|ADMIN_ADD_SERVER))
    -{
    -    $log = new CSystemLog("w", "Попытка взлома", $userbank->GetProperty('user') . " пытался загрузить изображение карты, не имея на это прав.");
    -	echo 'У вас нет доступа к этому!';
    -	die();
    +// Check access rights: only owners and server adders may upload
    +if (empty($userbank) || !$userbank->HasAccess(ADMIN_OWNER|ADMIN_ADD_SERVER)) {
    +    $userName = (empty($userbank)) ? 'Unknown' : $userbank->GetProperty('user');
    +
    +    // Log suspicious attempt
    +    $log = new CSystemLog("w", "Попытка взлома", "{$userName} пытался загрузить изображение карты, не имея на это прав.");
    +
    +    die('У вас нет доступа к этому!');
     }
     
    -$message = sprintf("<br /><strong>Обратите внимание!</strong><br />Максимальный размер файла: %s<br />Максимальное кол-во файлов для загрузки: %s<br /><br />", ini_get('upload_max_filesize'), ini_get('max_file_uploads'));
    -if(isset($_POST['upload']))
    -{
    -	$fls = normalize_files_array($_FILES);
    -
    -	$message = '<script>alert("';
    -	$fcount = count($fls['mapimg_file']);
    -	foreach ($fls['mapimg_file'] as $curfile) {
    -		if ($curfile['error'] != 0 || $curfile['type'] != "image/jpeg")
    -			$message .= sprintf("Не удалось загрузить файл %s. Причина: %s.", $curfile['name'], getReasonByCode(($curfile['type'] != "image/jpeg")?100500:$curfile['error'], "JPG"));
    -		else {
    -			$filename = explode('.', $curfile)[0] . '.jpg';
    -			move_uploaded_file($curfile['tmp_name'], SB_MAP_LOCATION."/".$filename);
    -			$log = new CSystemLog("m", "Изображение карты загружено", "Новое изображение карты загружено: ".htmlspecialchars($filename));
    -			$message .= sprintf("Файл %s загружен.", $filename); // $curfile['name']
    -		}
    -		$message .= "\\n";
    -	}
    -	$message .= '"); self.close();</script>';
    +$message = sprintf("
    +    <br><strong>Обратите внимание!</strong>
    +    <br>Максимальный размер файла: %s
    +    <br>Максимальное кол-во файлов для загрузки: %s<br><br>",
    +    ini_get('upload_max_filesize'), ini_get('max_file_uploads')
    +);
    +
    +if (isset($_POST['upload'])) {
    +    $fls = normalize_files_array($_FILES);
    +    $message = '<script>alert("';
    +
    +    foreach ($fls['mapimg_file'] as $curfile) {
    +        // 1. Check for upload errors
    +        if ($curfile['error'] != 0) {
    +            $message .= sprintf("Не удалось загрузить файл %s. Причина: %s.",
    +                $curfile['name'], getReasonByCode($curfile['error'], "JPG"));
    +            $message .= "\\n";
    +            continue;
    +        }
    +
    +        // 2. Verify that the file is a genuine image (JPEG, PNG, or WebP) via content check
    +        $check = @getimagesize($curfile['tmp_name']);
    +        if ($check === false) {
    +            $message .= sprintf("Файл %s не является допустимым изображением.", $curfile['name']);
    +            $message .= "\\n";
    +            $log = new CSystemLog("w", "Подозрительная загрузка", "Не удалось определить тип файла: " . $curfile['name']);
    +            continue;
    +        }
    +
    +        $allowed_types = [IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_WEBP];
    +        if (!in_array($check[2], $allowed_types)) {
    +            $message .= sprintf("Файл %s не является изображением в формате JPEG, PNG или WebP.", $curfile['name']);
    +            $message .= "\\n";
    +            $log = new CSystemLog("w", "Подозрительная загрузка", "Попытка загрузить неподдерживаемый тип: " . $curfile['name']);
    +            continue;
    +        }
    +
    +        // 3. Determine the correct extension based on the image type
    +        $image_type = $check[2];
    +        switch ($image_type) {
    +            case IMAGETYPE_JPEG: $ext = 'jpg'; break;
    +            case IMAGETYPE_PNG:  $ext = 'png'; break;
    +            case IMAGETYPE_WEBP: $ext = 'webp'; break;
    +            default: continue 2; // Should never happen
    +        }
    +
    +        // 4. Securely process the file name
    +        // 4.1 Strip any path information (prevents directory traversal)
    +        $original_basename = basename($curfile['name']);
    +        
    +        // 4.2 Remove any character that is not a letter, digit, dot, dash or underscore
    +        //     This eliminates any possible HTML tags or unsafe symbols.
    +        $clean_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $original_basename);
    +        
    +        // 4.3 Check if the original name contained unsafe characters
    +        if ($clean_name !== $original_basename) {
    +            $message .= sprintf("Имя файла %s содержит недопустимые символы. Загрузка отклонена.", htmlspecialchars($original_basename));
    +            $message .= "\\n";
    +            $log = new CSystemLog("w", "Подозрительное имя файла", "Попытка загрузить файл с недопустимыми символами: " . $original_basename);
    +            continue;
    +        }
    +
    +        // 4.4 If the name becomes empty after cleaning (should not happen if check passed, but just in case)
    +        if (empty($clean_name)) {
    +            $message .= sprintf("Имя файла %s стало пустым после очистки. Загрузка отклонена.", htmlspecialchars($original_basename));
    +            $message .= "\\n";
    +            $log = new CSystemLog("w", "Пустое имя файла", "Имя файла после очистки оказалось пустым: " . $original_basename);
    +            continue;
    +        }
    +
    +        // 4.5 Replace any extension with the correct one
    +        $filename = pathinfo($clean_name, PATHINFO_FILENAME) . '.' . $ext;
    +
    +        // 5. Build the full destination path
    +        $destination = SB_MAP_LOCATION . '/' . $filename;
    +
    +        // Prepare safe versions for output (to prevent XSS in messages)
    +        $fileNameSafe = htmlspecialchars($filename);
    +        $origNameSafe = htmlspecialchars($curfile['name']);
    +
    +        // 6. Prevent overwriting existing files – reject upload if file already exists
    +        if (file_exists($destination)) {
    +            $message .= sprintf("Файл с именем %s уже существует. Загрузка отклонена.", $fileNameSafe);
    +            $message .= "\\n";
    +            continue;
    +        }
    +
    +        // 7. Move the uploaded file to its destination
    +        if (move_uploaded_file($curfile['tmp_name'], $destination)) {
    +            // 8. Optional: re-encode the image using GD to strip any malicious code from metadata
    +            $reencoded = false;
    +            if (function_exists('imagecreatefromjpeg') && function_exists('imagejpeg') ||
    +                function_exists('imagecreatefrompng') && function_exists('imagepng') ||
    +                function_exists('imagecreatefromwebp') && function_exists('imagewebp')) {
    +
    +                $img = null;
    +                switch ($image_type) {
    +                    case IMAGETYPE_JPEG:
    +                        if (function_exists('imagecreatefromjpeg')) $img = @imagecreatefromjpeg($destination);
    +                        break;
    +                    case IMAGETYPE_PNG:
    +                        if (function_exists('imagecreatefrompng')) $img = @imagecreatefrompng($destination);
    +                        break;
    +                    case IMAGETYPE_WEBP:
    +                        if (function_exists('imagecreatefromwebp')) $img = @imagecreatefromwebp($destination);
    +                        break;
    +                }
    +
    +                if ($img) {
    +                    // Save back with appropriate quality/compression
    +                    switch ($image_type) {
    +                        case IMAGETYPE_JPEG:
    +                            imagejpeg($img, $destination, 90);
    +                            break;
    +                        case IMAGETYPE_PNG:
    +                            // Compression level 9 = maximum (safe, no quality loss)
    +                            imagepng($img, $destination, 9);
    +                            break;
    +                        case IMAGETYPE_WEBP:
    +                            imagewebp($img, $destination, 80); // quality 80
    +                            break;
    +                    }
    +                    imagedestroy($img);
    +                    $reencoded = true;
    +                } else {
    +                    // The file could not be opened as a valid image – delete it and report error
    +                    unlink($destination);
    +                    $message .= sprintf("Файл %s повреждён или содержит некорректные данные.", $origNameSafe);
    +                    $message .= "\\n";
    +                    continue;
    +                }
    +            }
    +
    +            // 9. Log successful upload
    +            $log = new CSystemLog("m", "Изображение карты загружено", "Новое изображение карты загружено: " . $fileNameSafe);
    +            $message .= sprintf("Файл %s загружен как %s.", $origNameSafe, $fileNameSafe);
    +        } else {
    +            $message .= sprintf("Не удалось сохранить файл %s.", $origNameSafe);
    +        }
    +
    +        $message .= "\\n";
    +    }
    +
    +    $message .= '"); self.close();</script>';
     }
     
    +// Assign template variables
     $theme->assign("title", "Загрузить изображение карты");
     $theme->assign("message", $message);
     $theme->assign("input_name", "mapimg_file[]");
     $theme->assign("form_name", "mapimgup");
    -$theme->assign("formats", "JPG");
    +$theme->assign("formats", "JPG, PNG, WEBP");
     
     $theme->display('page_uploadfile.tpl');
    -?>
    
3ecd95e09e7a

Fix #374

https://github.com/sb-materialadmin/webSergey GutFeb 7, 2026via body-scan
1 file changed · +4 3
  • pages/admin.uploadmapimg.php+4 3 modified
    @@ -47,9 +47,10 @@
     		if ($curfile['error'] != 0 || $curfile['type'] != "image/jpeg")
     			$message .= sprintf("Не удалось загрузить файл %s. Причина: %s.", $curfile['name'], getReasonByCode(($curfile['type'] != "image/jpeg")?100500:$curfile['error'], "JPG"));
     		else {
    -			move_uploaded_file($curfile['tmp_name'], SB_MAP_LOCATION."/".$curfile['name']);
    -			$log = new CSystemLog("m", "Изображение карты загружено", "Новое изображение карты загружено: ".htmlspecialchars($curfile['name']));
    -			$message .= sprintf("Файл %s загружен.", $curfile['name']); // $curfile['name']
    +			$filename = explode('.', $curfile)[0] . '.jpg';
    +			move_uploaded_file($curfile['tmp_name'], SB_MAP_LOCATION."/".$filename);
    +			$log = new CSystemLog("m", "Изображение карты загружено", "Новое изображение карты загружено: ".htmlspecialchars($filename));
    +			$message .= sprintf("Файл %s загружен.", $filename); // $curfile['name']
     		}
     		$message .= "\\n";
     	}
    

Vulnerability mechanics

Root cause

"Missing content-based file validation in admin.uploadmapimg.php allows attackers to bypass a client-supplied Content-Type check and upload arbitrary files (e.g., PHP shells) to a web-accessible directory."

Attack vector

An authenticated attacker with the ADMIN_ADD_SERVER flag sends a crafted multipart POST request to pages/admin.uploadmapimg.php. The original code [patch_id=2974415] only checks the client-supplied Content-Type header (`$curfile['type'] != "image/jpeg"`) and the PHP upload error code, which are trivially spoofed [ref_id=2]. The uploaded file is saved with its original filename into the web-accessible `images/maps/` directory without any extension restriction or content validation [ref_id=1]. An attacker can upload a PHP web shell and a separate `.htaccess` file to enable PHP execution in that directory, achieving remote code execution as the web server user [ref_id=1][ref_id=2].

Affected code

The vulnerable code is in `pages/admin.uploadmapimg.php` [ref_id=2]. The original upload handler checked only `$curfile['type'] != "image/jpeg"` (a client-supplied header) and saved files via `move_uploaded_file($curfile['tmp_name'], SB_MAP_LOCATION."/".$curfile['name'])` without sanitizing the filename or validating the file content [patch_id=2974415][ref_id=1].

What the fix does

The patch [patch_id=2974415] replaces the Content-Type check with `getimagesize()` to verify the file is a genuine JPEG, PNG, or WebP image. It strips path information via `basename()`, removes unsafe characters with a regex, forces the correct file extension based on the detected image type, and prevents overwriting existing files. Additionally, the patch re-encodes the image using GD library functions (`imagecreatefromjpeg`, `imagepng`, etc.) to strip any embedded malicious payload from metadata. A new `.htaccess` file in `images/maps/` disables the PHP engine and denies access to script file extensions as a defense-in-depth measure [patch_id=2974415].

Preconditions

  • authAttacker must be authenticated with the ADMIN_ADD_SERVER flag (or ADMIN_OWNER).
  • networkAttacker must be able to send HTTP POST requests to the admin.uploadmapimg.php endpoint.
  • inputAttacker must craft a multipart upload with a spoofed Content-Type header (e.g., image/jpeg) and a file containing arbitrary code (e.g., PHP shell).

Generated on May 28, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.