Malware Cleanup to Arbitrary File Upload in Gravity Forms

During our regular cleanup process we came across a reinfection case that caught our attention.

This particular environment didn’t have anything special or fancy, it was an updated WordPress installation and had 3 out-of-date plugins; that’s pretty reasonable.

After running through our processes and cleaning the environment we kept coming back to a reinfection; the attacker kept uploading nefarious files on the server.


This got us very curious and so we had to dig a little deeper.

The malicious files were being uploaded to ‘/wp-content/uploads/gravity_forms’ and ‘/wp-content/uploads’ on Feb 21st, but how?

While Forensics is not a default offering and the client was not using our Website Firewall (which would have prevented the reinfection), we do love a good challenge. So why not investigate when the cases are curious – such as this one. Fortunately, we had access to the logs and were able to find some interesting requests to those particular files.

With that in mind, we started looking for different variations of “_input_” and found a lot requests to those files.


We went back a few days in the logs to find what preceded those requests and where they could have come from.


In analyzing the files, we came across these requests to “?gf_page=upload”; that sounds interesting doesn’t it?

We searched for that string in the file system and found an instance within the WordPress plugin GravityForms (out-dated version 1.8.19).

Gravity Forms is a WordPress plugin used originally for contact forms, but in a more general sense, it allows site owners to create forms to collect information. Gravity Forms can be used for contact forms, WordPress post creation, calculators, employment applications and more.

Written in PHP, Gravity Forms uses many WordPress built-in functions and features to power its form builder. It also uses the same MySQL database system as WordPress, but stores all forms and entries in its own tables.

Gravity Forms is open source and GPL licensed. All of the code included is unencrypted, and easy to modify. We’ve added in tons of hooks and filters to be able to customize Gravity Forms to your hearts content.

Upon further investigation, we found “?gf_page=upload” inside common.php in line 3635.


It was very interesting, not in a good way though; there was no sanitization of that request.

For testing purposes, I requested “?gf_page=upload” to see what would happen and interestingly enough, we got this:


Which lead to searching where the message was being processed.


From checking the upload.php, we see that $_REQUEST[“form_id”] has to be set otherwise the upload fails.
Keep in mind that at this time, we already bypassed any protections that could prevent unauthorized users from accessing that resource.

I set the value of form_id to 1 and made another request in a crafted upload form and this time the error was a little bit different.


As I tried sending a test.php, we hit a function file_name_has_disallowed_extension() that didn’t allow the file to be uploaded, but we’re stubborn, so let’s not give up.

By checking the declaration of those functions in common.php we found out why this happened.

There’s a list of disallowed extensions in get_disallowed_file_extensions() that the function file_name_has_disallowed_extension() checks against, and PHP is among the extensions in the list.


It basically means that we can’t upload .PHP files, right? Wrong – and let’s see why.

Inside includes/upload.php, we see that we have total control over the filename that is being uploaded as well how the file is saved into the server. The lines 54 and 55 give us such power through simple HTTP requests.

We also see that if $field is empty, the execution dies (59,60), so we set a value of 1 to field_id.


After changing the filename from test.php to test.jpg we got a very interesting response.


The file was uploaded to the server but its name is _input_1_, therefore we can’t do much with it.
It turns out that this is how the temp_filename is created:


Breaking that down, we have the following:

$form_unique_id		= We didn't set any value here
_input_			= Hardcoded
$field_id		= We set that 1 as mentioned above
_			= Harcoded
$file_name		= We control this data, therefore we can set .php here

Our $tmp_file_name is ready to go and we finally have what we were looking for! 🙂



From checking the changelog for Gravity Forms we see that the security fix was applied in the version (1.8.20) please update ASAP:

Gravity Forms v1.8.20 is now available via automatic update and the customer downloads page. This is an important security and maintenance release.
We recommend all users update as soon as possible. It is important to always keep WordPress, plugins and themes up to date as a matter of best practice.

  • Fixed a security issue with the file upload field.

The versions 1.8.19 and lower might be affected by this vulnerability.

We always say that keeping all software updated is one of the most important steps you can take towards reducing the risks of infection and this post is a good example of why.

This is a dangerous vulnerability, you should update all of your websites using this plugin as soon as possible. If for any reason you cannot, we highly recommend you to have a look at our Website Firewall (WAF) product. It’s designed to help you stay ahead of vulnerabilities like the one described here, and many more.

  1. Thanks for this. I noticed i had the same php files uploaded on my server as well.

    Thankfully I have the following code in my NGINX configuration, that stops people running uploaded php code.
    It gets displayed as text when they try to open it. But, i should really setup an OSSEC rule to notify me if something similar happens again.

    location ~* ^/wp-content/uploads/.*.(html|htm|shtml|php)$ {

    types { }

    default_type text/plain;


    1. This wouldn’t have been enough to secure yourself against this vulnerability. Your fix is still vulnerable to files such as “.php5”, so a better fix would be to just let Nginx serve files statically from the uploads directory, and not pass anything from dangerous directories like that to your CGI. Additionally, GravityForms prior to version 1.8.20 is vulnerable to directory traversal (this is what the “Fixed a security issue with the file upload field.” in v1.8.20 was all about), so they can easily escape the uploads directory and place their malicious files in a directory where it would get executed.

      After 1.8.20, you can still bypass the file extension sanitisation, but without the directory traversal, this vulnerability becomes a lot less useful, especially if directory listing is turned off. GravityForms stores the uploaded files in a non-guessable directory (“gravity_forms/”.$form_id.”-“.wp_hash($form_id)), so without a reliable way of knowing the directory name (directory listing or known secret-keys from wp-config.php), you can consider yourself somewhat safe from v1.8.20 and up if you have directory listing turned off.

      Later versions includes yet another fix (> which really makes things harder: File uploads are now only allowed for file-upload fields. Obviously this should have been there from the beginning, but for whatever reason it was not. This of course means any GravityForms instance prior to v1.9 is vulnerable to arbitrary file uploads, even if they had no file-upload fields what-so-ever (I have PoC for this)! However, like I said, without the directory traversal of directory listing, it’s very unlikely that a hacker would be able to execute their malicious files.

      Some earlier versions, ironically enough, are somewhat safer due to how the filenames are sanitized. 1.8.13 is safer than 1.8.20, as you cannot bypass the file extension check here, and it has a rather complete list of disallowed extensions. Earlier than 1.8.13 and you’re shit out of luck, though, as “php5” is an allowed file extension and they have directory traversal vulnerability.

      I did some research on this after one of our servers got hacked through a very similar approach and wrote a small document on my various findings + some patches for every version I checked, but I never released it. The directory traversal vulnerability was fixed just a few days after I had completed my research, so I decided to just let it slide. People who don’t update their plugins are doomed anyway.

      Just for fun I checked v1.9.2, the latest version, while writing this. It’s still vulnerable to the file extension bypass. I have not reported this issue because they only have a dumb contact form on their website without a “security issue” category, and no email to which we can report security related issues. Laaaaame! If they don’t care about their security issues, then why should I?

      With that said, this file extension bypass is really not useful in the big scheme of things, so you shouldn’t have to worry about it unless you have:
      1) A form with a file-upload field.
      2) Directory Listing on, or have old secret-keys in wp-config.php after being hacked previously.
      3) PHP executable upload directory.

      If you satisfy all 3 requirements above, here’s the patch, straight out of my document (Note: the patch is untested; I don’t use GravityForms, but it should work):

      # All versions after v1.8.16 (Prevent file extension check bypass)
      File: /wp-content/plugins/gravityforms/include/upload.php
      – $file_name = isset($_REQUEST[“name”]) ? $_REQUEST[“name”] : ”;
      + $file_name = $uploaded_filename;


      The filename written to disk includes $_REQUEST[“name”] as its suffix, which is never sanitized. This patch overwrites $_REQUEST[“name”] with the actual filename in the request, causing the file extension check to check the /actual/ filename that would get written to the disk.

        1. Yeah, basically.
          Unless the index.html files have been removed and directory listing has been turned on, or the attacker can learn the location through other means, but this is very unlikely.

          1. I approve of your idea.
            Anyway, if the server’s OS is windows, I could find the folder not more than 43680 times requests (because of the short filename).

      1. I follow everything apart from the directory traversal – I’ve versioned the code and cannot see anyway this was possible prior to 1.8.20 – could you shed some light on how this is possible?

      2. @Cytlan – I’m looking at 1.8.5 and I’m confused how it’s vulnerable to a directory traversal. This line:

        $file_name = preg_replace(‘/[^w._]+/’, ‘_’, $file_name);

        seems to prevent it. Do later versions (>1.8.5 and <1.8.20) omit that?

  2. Hi, I’m unable to reproduce that vuln. When using php extention with the $file_name it show ‘The uploaded file type is not allowed’ and in the last pic show that you are able to use php in $file_name (you used file named ‘.php’).

    1. The approach differs a little between versions. Older versions (<=1.8.13) cannot use the file_name field to bypass the extension check. Try using ".php." instead; That often works.

      Also, are you sure the file you're trying to upload does not have a ".php" extension?

  3. As an additional security measure: place .htaccess file in the /wp-content/uploads/

    Order Deny,Allow
    Deny from All

    That would prevent files like php, php1 etc and phtml from running in that directory and all sub-directories. Not sure why a measure like that was not included in WP

  4. As an additional security measure: place .htaccess file in the /wp-content/uploads/

    Order Deny,Allow
    Deny from All

    That would prevent files like php, php1 etc and phtml from running in that directory and all sub-directories. Not sure why a measure like that was not included in WP

    1. Grrr… the filesmatch line above should end with $”>

      not sure but the comments system converts it to what it is in my first comment

  5. User=ID10T here (n00b who’s less advanced than a 5 year-old
    at building/policing WP sites here) – QUESTION: Do I have to worry about this
    vulnerability if I do NOT have the Gravity Forms plugin? I use a different
    contact form. I don’t really understand a lot of what I see in the dashboard
    and Audit Logs of Sucuri, so I just let it do its thing; on my other site I
    tried WordFence and I guess MY lil brain just “gets” that a little easier,
    so I have WF on both my sites. [I did notice in Audit Log the changes I was
    making in Wordfence, and of course my logins (my home IP, ok), but I don’t
    understand seeing what looks to be post deletions when I’m not deleting any of my
    posts; the source IP for the deletions was my site IP so okay I guess?? And
    once it was my home IP deleting a post – does it list that way when I update a
    post and it just gets rid of the old edit of the post?] …Anyway, in WordFence
    I noticed someone accessing this /?gf_page=upload “page” on my site
    and didn’t even know it was a real page, but it wasn’t in the Page Not Found
    area; it was in the All Hits area looking all legit. I followed the link and it
    looks like a clone of my home page. Um… is this typical? Should I be worried?

    1. You are fine if you don’t use Gravity Forms. Some hacker was probably attempting to compromise your site, and thought you may be using Gravity Forms, but you are not. Sometimes, it pays to use the less popular options.

  6. We have a lot of sites that are using this plugin. Are you offering some sort of discount for the license key renewal since we HAVE to update this plugin now?

      thanks for lightening up my dev team today
      (you’re supposed to have a license to have any version; the license is an incentive to update, rather than just passing your old version around).

  7. FYI my version was 1.8.7 and was still compromised via the file upload so v1.8.2 does not fix the problem.

    1. 1.8.20 isn’t the same as 1.8.2. Trailing zeroes don’t fall off in software version numbering 🙂

      1.8.7 < 1.8.19 < 1.8.20 < 1.8.21.

    1. The fix is replacing your entire system from backups.
      If they hacked and reprogrammed the kernel there is nothing you can trust.
      They could just create the illusion that the server is ok, giving you a sandbox to play in while completely hiding their activity.
      More likely, they didn’t manage to get root access and just corrupted a few php files, installed backdoors and proxies.
      Solution, compare backups using rsync -n or side by side using mc going through file by file.
      Fix security holes. Change all passwords and ssh keys
      Check and fix or restore databases as necessary.
      It’s a huge amount of work multiplied by the number people affected.

      Having disinfected more machines than I can remember, reported 2.5 million spam messages, built up a database of 3 million infected ip addresses and sent out 500,000 messages telling providers to fix their infected machines, I sometimes think that programmers that make such stupid and expensive mistakes like this should be just shot! Just kidding!
      Websites can be made secure if people just think about security with every line they write and don’t take shortcuts.

      Now after 20 years of this, I made a preemptive adaptive firewall so if any site on any server sees any potential attack, the ip address is just banned for a month and shared so others can be protected too.
      If more people do this, then infected machines will have nowhere to go and be very lonely until fixed. 😉

  8. The same uploading of files has occurred on my wordpress site, but we don’t have Gravity Forms.

  9. Add this line to the .htaccess file in the root:

    RewriteRule ^wp-content/.*.(php|php3|php4|php5)$ – [R=404,L,NC]

    This will prevent users from executing .php files from publicly accessible folders like uploads, themes and plugins.

    Also, install the plugin, Wordfence, the free version will detect any malicious script and alert you by e-mail.

  10. Just got alerted by my host that my wordpress site has been compromised. Two PHP files in /media/2015/3/tmp/[garbage]/ and yes I have the Gravity Forms plugin – Updated it before noting what version I was on though.

    QUESTION: Once the PHP files are there in /media, how do the hackers expect them to be called by visitors? SHould I expect some links in to those PHP files somewhere?

  11. My site has been hacked by this, I blocked the IP for now and updated my gravity forms, that is best for now I think.. But is there a proof of concept for this or how did you even test this out from “/?gf_page=upload” ? Is it via cURL or POST data or what??

  12. Anyone have problems with gravity forms since their update? I updated all my plugins (including Gravity) but still got hacked in a major way to the point that my website is beyond repair and I have to start from a fresh install… ugh:(

  13. The original signature plugin available on Gravity Forms website saves an image of hand drawn signatures on an ftp server, putting both the site owner and their signers at high risk for identity theft. You have the opportunity to create a legal contract which is customized with values of the recent form submission using ApproveMe’s WP E-Signature in conjunction with this new Gravity Forms Signature Add-On.

  14. It seems that nForms is no longer in circulation…
    I bought the original plugin and I’m running 1.5.5 right now but I can’t see where to get the update ver.


Comments are closed.

You May Also Like