Ajaxform Hack? 600k requests in a few days

Hello there!

It’s been a while since last time i’ve been on this forum.

I got notice of a problem with a website that got blocked by the hoster due to compromised files on the server.

A quick check of the files on the webspace didn’t indicate any added or changed files in the recent past. So i took a look at the logfiles and witnessed heavy usage of more than 600k times of the php-file in “assets/components/ajaxform/action.php” in about 3 days.

The site where this happened is running with MODX 2.8.3 on PHP 8.1.

Since i’m using Ajaxform on many projects, i am a bit anxious this is going to happen on the other sites as well.

Anyone else here who had this problem? What can i do to prevent this?

Thanks in advance,

Marc

So where were these “compormised files” if you say that no files were “added or changed in the recent past”?


Was this server used to send spam?

Theoretically this AJAX-endpoint should only run FormIt. The same as it would without AjaxForm.


I haven’t heard of any such problems with AjaxForm.

However, the development of this extra is no longer continued.
Maybe use FetchIt instead (which is the official replacement).

Hey halftrainedharry!

Thanks a lot for your reply.

So where were these “compormised files” if you say that no files were “added or changed in the recent past”?

No, i could not find any changes in the timestamps of files nor folders since the last update i did a year ago.

Was this server used to send spam?

Theoretically this AJAX-endpoint should only run FormIt. The same as it would without AjaxForm.

Yeah, after some further research i got a notice that spam got send via an insecure contact-form. Since there is only only AjaxForm-form on the site, and due to the heavy usage of the action.php, this must be the issue from my perspective.

Maybe use FetchIt instead (which is the official replacement).

Thanks for the hint. Once the site gets unblocked i’m gonna have a look into that. For now i deleted the whole ajaxform-folder and the transport package.

As AjaxForm only executes the FormIt snippet, the problem could also be FormIt (or the way you set it up).

Is the extra FormIt up-to-date?
Did you use any spam protection for this form?
Was there a way to choose the receiver of the email from the front-end?

Is the extra FormIt up-to-date?

Nope, it’s formit-4.2.6, that’s currently being used. So yeah, an update is needed for this for sure!

Did you use any spam protection for this form?

No further spam protection than the standard hidden-input field that needs to be blank, and a kind of honeypot-email input, which must also be blank or empty for the form to validate.

Was there a way to choose the receiver of the email from the front-end?

No, the receiver is only set in the Ajaxform-call, no option to choose the email from the front-end.

I fail to see how the AjaxForm endpoint (action.php) could have been used to alter the FormIt settings or execute any other code.

Is there anything else in the logfiles that helps to understand how this endpoint was used? Any request parameters that were sent?

Is there anything else in the logfiles that helps to understand how this endpoint was used? Any request parameters that were sent?

No, unfortunately not. This attack happened mid January 2024, and the logfiles only go back to two days after this incident. Since i can’t communicate with the hoster directly it’s a bit complicated. I’m hoping and waiting for some more information, since script-based sending of messages is still disabled on the site, though the site itself got unblocked.

Is your form sending reply emails to submitter email that contain email addresses and content? If so, it was likely used for the purposes of spam. Spammers load up submissions with a series of email addresses and often ilnks to nasty sites and fire away.

Other things to do includes rate limiting form submissions, Rampart, reCAPTCHA3 (which doesn’t use box puzzles), JavaScript testing, token checking to make sure the page was actually loaded in the browser or, if the site is of any importance to the org, subscribe to a WAF with rate limiting and bot blocking.

Is your form sending reply emails to submitter email that contain email addresses and content?

Yeah, there was an option in the form to send a copy of the sent message to the submitter. This copy then holds address-information and the website-email.

Other things to do includes rate limiting form submissions, Rampart, reCAPTCHA3 (which doesn’t use box puzzles), JavaScript testing, token checking to make sure the page was actually loaded in the browser or, if the site is of any importance to the org, subscribe to a WAF with rate limiting and bot blocking.

Thank you for all these hints, i gotta dive deeper into that for sure! So far the honeypot worked good, but this really hit me out of the blue.

We have this problem too. A lot of requests to action.php!

It’s because direct calls to this script are avoiding
all server-side validations and FormIt hooks!

… because they’re written only in snippet call. So if send requests from actual page, all is fine, because all logic of snippet result with validations and hooks is performing. But via direct call to action.php this logic is unrechable, so any request will be sent to server without any security checks. AjaxForm must made update for this or accept fixing PR from somebody. It’s security invulnerability and strict one

Are you sure?

As far as I can tell, the same validations and hooks are used, no matter if the page/resource is loaded directly or the AJAX endpoint (action.php) is called.

If you are sure, can you explain in more detail how action.php can be called to circumvent the validation? Maybe with an example?

Yes, I sure. Because I have e.g. g-recaptcha-response:required and it doesn’t block bot requests without even existing of this field in their spam form $_REQUEST’s (g-recaptcha-response). So in case if validators are work in action.php, their requests will be blocked, but doesn’t.

And hook of my custom blocks also doesn’t work for bot spam requests, only when I do real form submissions as normal people. And they’re still avoiding captcha. That’s my proofs.

My snippet params:

&validate=`fullname:blank,phone:required:minLength=^18^:maxLength=^18^,g-recaptcha-response:required`
&hooks=`spam,csrfhelper_formit,recaptchav2,FormIt_custom_protections,FormItSaveForm,AmoCRM_hook`

What version of AjaxForm do you use?
Do you have sessions enable for anonymous users on your system?


When the snippet “AjaxForm” runs, the properties (like &validate or &hooks) are stored in the session ( $_SESSION) or in a cache file (core/cache/default/ajaxform/props_<some_hash>.cache.php).

When the AJAX-endpoint (action.php) is called, the properties (like &validate or &hooks) are read from the session or the cache.

If the cache file or the session variable doesn’t exist, the code execution terminates.

I don’t see a way how FormIt could be executed without the stored properties.
If you don’t use sessions, what’s the content of the cache file (core/cache/default/ajaxform/props_<some_hash>.cache.php) on your system? Does it contain all the properties?

That’s as it should work, but actually it doesn’t. Need to debug why. I said and provided example that it doesn’t work as in mind and as we expect how it should work. We’re not in world where all the code working as expected. Bugs and people factors are eternal

To fix the bug and create a patch, one has to first know what the problem with the existing code is. I looked at the code, did some debugging, but couldn’t find the error. So please try to be helpful and provide as much information as you can.

  • What version of AjaxForm do you use? Maybe the vulnerability was introduce in a specific version.
  • Do you use sessions or the cache file? The session seems safer as the cached file is shared between different users, but the session is user specific.
  • You say that spam request don’t run your custom hooks. But some hook (like “email”) has to run (or otherwise the spam request is pointless). Do these spam requests still get saved (hook FormItSaveForm)? Does the requests have the parameters “pageId” and “af_action”? Do you see any “weird” request parameters (or “weird” parameter values) that may change the code logic?
  • Are you sure your AjaxForm tag ([[!AjaxForm?...]]) is correct? Maybe there is some syntax error and that’s the reason why the properties (like &validate) aren’t parsed correctly.

You say that spam request don’t run your custom hooks. But some hook (like “email”) has to run (or otherwise the spam request is pointless)

About hooks yes, I was wrong a half. Emails hook are working, but captcha hook doesn’t. And captcha wasn’t even exist before spam-requests appeared, so I guess spammers are passing their own list of hooks & validators someway.

Does the requests have the parameters “pageId” and “af_action”?

Yes

What version of AjaxForm do you use? Maybe the vulnerability was introduce in a specific version.

Was 1.1.9 (latest official), but in update 1.2.2 (latest fork) nothing fixed too.

Are you sure your AjaxForm tag ([[!AjaxForm?...]] ) is correct?

Yes

[[!AjaxForm?
    &snippet=`FormIt`
    &form=`form.measurement_request`
    &submitVar=`measurement_request_form`
    &emailTpl=`mail.measurement_request`
    &emailTo=`[[++emailsender]]`
    &validate=`fullname:blank,phone:required:minLength=^18^:maxLength=^18^,g-recaptcha-response:required`
	&hooks=`spam,csrfhelper_formit,recaptchav2,FormIt_custom_protections,FormItSaveForm,CRM_hook`
    &emailSubject=`Application for calculation/measurement`
    &successMessage=`Your application has been sent! Our manager will call you shortly <script>$.fancybox.close();</script>`
    &validationErrorMessage=`Data filled in incorrectly`
    &_frontend_css=``
	&csrfKey=`measurement_request_csrf`
	&for=`[[+for]]`
]]

Do you see any “weird” request parameters (or “weird” parameter values) that may change the code logic?

Only this:

if (!function_exists('mrs_log')) {
    function mrs_log($arr) {
        $log = '/absolute/path/to/oursite.com/public_html/mrs.log';
        if (!file_exists($log)) {
            touch($log);
        }
        $fp = fopen($log, 'a');
        fwrite($fp, json_encode($arr, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . "\n\n");
        fclose($fp);
    }
}

mrs_log([
    'when' => date('d.m.Y H:i:s'),
    'phone' => $_REQUEST['phone'],
    'ip' => $_SERVER['REMOTE_ADDR'],
    'browser' => $_SERVER['HTTP_USER_AGENT'],
    'referer' => $_SERVER['HTTP_REFERER'],
    'uri_que' => $_SERVER['QUERY_STRING'],
    'reqwith' => $_SERVER['HTTP_X_REQUESTED_WITH'],
    'request' => $_REQUEST,
]);
{
    "when": "08.07.2024 20:30:43",
    "phone": "+1 (111) 123-45-67",
    "ip": "123.45.67.89",
    "browser": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:127.0) Gecko/20100101 Firefox/127.0",
    "referer": "https://oursite.com/",
    "uri_que": "",
    "reqwith": "XMLHttpRequest",
    "request": {
        "amocrm_action": "submitted a request for a free estimate/measurement",
        "measurement_request_form": "1",
        "nospam:blank": "",
        "form_title": "",
        "page_id": "29",
        "page_name": "Page name",
        "build_type": "",
        "build_kind": "",
        "page_kind": "",
        "csrf_token": "MTcyMDExNDU3OS0tNmJiMzBjMzYxODE1YWZhMTEzMWNmY2IxYzI3OGI2OTI1ZGQyYWFiMDgxNGZhZWNiZWI3ZDM5NzQ3YWQ4ZmY2ZTNlNDQ5ZGQ3YmU1MzU1ZDk5ZGU2ODIzZGFhZTEwZmU2ZmY5YWQ0MWE3YmU1YjUzMThhODdhNDM5MTM0YTA1YjA=",
        "fullname": "",
        "phone": "+1 (111) 123-45-67",
        "af_action": "4479c5f4947beb78ab1afe9e101e5aef",
        "pageId": "29"
    }
}

Forgot only to check php://input

I really don’t see how that’s possible.
As you’re using version 1.1.9, the properties to call FormIt with are always stored in the user session ($_SESSION).

In the code there is this function process():

This function is called both from the snippet “AjaxForm” as well as from the AJAX endpoint action.php. The code reads the properties from the session and uses them to run the snippet “FormIt”. I see no way to change their values from the outside.

You could log the properties before they are used in this line

to make sure their values are ok, for example with code like this:

$this->modx->log(modX::LOG_LEVEL_ERROR, "AjaxForm: Snippet {$name}, property 'hooks' = " . $scriptProperties['hooks']);
$this->modx->log(modX::LOG_LEVEL_ERROR, "AjaxForm: Snippet {$name}, property 'validate' = " . $scriptProperties['validate']);

I have idea. Spammers don’t visiting actual web-site, so their sessions are probably not overwriting and they have stored old one version of validators/hooks lists (which was before we implemented e.g. captchas). Purging of all sessions not the way, so I just using some another protections, e.g. this one

Yes this makes sense.
If the FormIt properties in the manager are changed, but someone calls the AJAX endpoint action.php directly (without first visiting the page with the [[!AjaxForm?...]] snippet tag), then the old property-values from the session are used.

This fix only works, when you change the value of $_SESSION['afchk'] (or the key afchk itself) after every important change to the FormIt properties. Otherwise you still have the same problem with the old values in the session.


I think the option with the cache-file for the properties (introduced in this commit) is the better option, as the cache-file gets deleted when changes are made to the form in the manager.

Why does this commit actually doesn’t work? It was added on 2021