Blog: Debunking an RCE which CVSSv3 is 10.0 CVE-2020-35489

Author: Florent
Published on

Patrowl's blog - Debunking an RCE which CVSSv3

WordPress Is all around us

I think reading this article, you are probably aware that Wordpress and its plugins is occupying a very large place in External attack surface landscape. While monitoring our client, we have under our monitoring more than 100 Wordpress, associates with more than 1,5k different plugins constantly removed and renewed, which raises again a new challenge in our automatization.

Detecting all plugins with their associated version was the first basic step we had quickly automated for our client. We want to be sure our solution will be able to detect all installed plugins, all versions, and of course all already known published vulnerabilities on the plugins.

But let’s face the reality of the figures. With more than 6 600 vulnerable plugins and more than 11k related CVE, you can quickly do the math: constantly warned our client about all CVE related to all their plugins will quickly become a mess, and, as always, impossible to patch especially when you have to manage 7k other assets exposed on internet.

Choice, the problem is choice

As always, prioritization is the key. Our algorithms carefully select vulnerabilities that have high chance to be quickly weaponized and exploited by attackers.

Your first though will be “Ok easy, they are probably sorting only high ranked CVSS vulnerabilities, what is the big deal?”. Of course, CVSS vectors are the first hint and one of the first element taking in account for our classification, but again, regarding the number of exposed CVE and their potential exploitation, it would be a loss of time not to use a specific technic.

So, what is the magic? 😜

Follow the white Rabbit

Let’s take a basic example. I think if you use the most basic web scanner, you have probably already encountered the famous vulnerability “WordPress Plugin Contact Form Arbitrary File Upload on version < 5.3.2”, If not, this is a perfect example of how you can lose your time on useless matter.

In our experience, the plugin is widely used and most of web scanner include the check of this CVE because of its ranking: it has been classified as “Critical” hardly everywhere. You can check just by searching the vulnerability on google:

Checking the first websites and scanners, from open source to commercial ones, you will see that, depending on the scanner, you will have:

  • Different severity, from High to Critical
  • Different CVSS score
  • Different prerequisite for exploitation
  • Different detection methods
  • ...

Patrowl's blog - Debunking an RCE which CVSSv3

Patrowl's blog - Debunking an RCE which CVSSv3

Ok now I see you get confused. And just don’t forget, this is one selected vulnerability over the 11k CVE referenced on Worpdress Plugins. And you ain't seen nothing yet.

Ignorance is blessing

Now that the CVE is hardly everywhere, and raised I think in most of Security teams using basic security scans, let’s dig a little closer on the vulnerability and the possible exploitation. An unauthenticated unrestricted file upload on a Wordpress form is a blessing for us and attacker, so we need to dig a little closer.

The article came from here: or you can also find it here: There is also another exploitation code which is slight different:

The exploitation schema seems to be quite simple: if you have a contact form with a file upload functionality configured, you can simply use a double extension separate with a special char (or Unicode char) to bypass security control and upload a PHP file like in 1999. Let’s try it.

So, as precisely described in the article, we integrate the vulnerable plugin version (5.3.1) in our labs, create a valid form upload formular (it requires to have the mail configured properly on the WordPress), and create an article using the contact formular we have just created, no other configurations have been implemented:

Patrowl's blog - Debunking an RCE which CVSSv3

As you can see, first, Pentester are clearly not designer.

Now let’s try to exploit the vulnerability and have our so wanted RCE. The first test with a .php file directly raised an error with an improper type, good news.

Patrowl's blog - Debunking an RCE which CVSSv3

Now, let’s check with special nonprintable characters (tabulation, null byte, non-printable spaces, Unicode char etc etc), response: Patrowl's blog - Debunking an RCE which CVSSv3

Great! Seems to work perfectly. Now let’s check our default upload folder regarding the article, by default it is: WPCF7_UPLOADS_TMP_DIR which is by default in wp-content/uploads/wpcf7_uploads : Patrowl's blog - Debunking an RCE which CVSSv3

Nothing here. Hmm. That is disappointing.

Patrowl's blog - Debunking an RCE which CVSSv3

There is no spoon

Let’s look a little closer to the source code. The so called “vulnerable” component is in the formatting file, in the wpcf7_antiscript_file_name:

function wpcf7_antiscript_file_name( $filename ) {

    $filename = wp_basename( $filename );
    $parts = explode( '.', $filename );

    if ( count( $parts ) < 2 ) {
        return $filename;

    $script_pattern = '/^(php|phtml|pl|py|rb|cgi|asp|aspx)\d?$/i';

    $filename = array_shift( $parts );
    $extension = array_pop( $parts );

    foreach ( (array) $parts as $part ) {
        if ( preg_match( $script_pattern, $part ) ) {
            $filename .= '.' . $part . '_';
        } else {
            $filename .= '.' . $part;

Reading article, it seems obvious that you can bypass the extension check:

1.  If a malicious user were to upload a file with filename containing double-extensions, separated by a non-printable or special character, for example a file called test.php .webp (\t character is the separator).
2.  Then Contact Form 7 does not remove special characters from the uploaded filename and parses the filename up till the first extension but discard the second one due to the separator. Thus, the final filename would become test.php (See the image below).

Ok, the second sentence is interesting, "but discard the second one due to separator".

The first check is analyzing the size of the $parts which is an exploded from the $filename using the “.” separators. You can add as many special chars as you want, the explode will always see your second “.” and the check could not be bypassed.

Patrowl's blog - Debunking an RCE which CVSSv3

I voluntary took very old version of PHP in case we miss a massive vulnerability in last three decades.

So, the first check could not be bypassed with Unicode char. Maybe they are talking about the other loop which will add a ‘_’ as a protection on dangerous extension? Let’s try:

Patrowl's blog - Debunking an RCE which CVSSv3

Ok, inserting non-printable char by the end of the extension allows us to bypass the adding of “_” protection characters. However, the non-printable char is always considered as part of the final extension. To be working, we need the function allowing to move the malicious file to the WPCF7_UPLOADS_TMP_DIR directory to be vulnerable by skipping non-printable char.

Ok. Now potentially we have our exploitation path. Exciting!

But wait, it seems that the article slightly forgot a part of the function, let’s scroll down just a little bit.

    if ( preg_match( $script_pattern, $extension ) ) {
        $filename .= '.' . $extension . '_.txt';
    } else {
        $filename .= '.' . $extension;

Patrowl's blog - Debunking an RCE which CVSSv3

That it’s interesting. Now in an imaginary word where you managed to pass the first check, you will have systematically the last extension added by the end of your file. You can add a double extension and .webp if you want, but by the end of the function you will always have the last (good extension) added and then stored within victim’s filesystem:

Patrowl's blog - Debunking an RCE which CVSSv3

There is no way you can bypass the final extension here.

The only opportunity to have this function returning a “potential” vulnerable path is to insert a filename ending “.php”+nonprintableble char and hope that our storage function will skip the nonprintable characters (like the nullbyte in … 2005 :{:target="_blank"}

It seems to be how the exploit works from PacketStorm: no double extension, using a simple .php+u00xx extension type.

But what in the hell they are mentioning a double extension? You will be surprised.

How deep the rabbit hole goes

During our tests, we never managed to reach the so called vulnerable wpcf7_antiscript_file_name function using a simple or double extension finishing with « .php » in it. Always the same error, different that the double extension error with php in the first place: validation_failed.

Patrowl's blog - Debunking an RCE which CVSSv3

This error seems to indicate there is, somewhere, another control on the type. A quick look on the code allows us to see that there are not one but plenty of checks performed before even reach the “vulnerable” function. First:

    /* File type validation */

    $file_type_pattern = wpcf7_acceptable_filetypes(
        $tag->get_option( 'filetypes' ), 'regex'

    $file_type_pattern = '/\.(' . $file_type_pattern . ')$/i';

    if ( empty( $file['name'] )
    or ! preg_match( $file_type_pattern, $file['name'] ) ) {
        $result->invalidate( $tag,
            wpcf7_get_message( 'upload_file_type_invalid' )

By default, the $file_type_pattern is equal to: webp|webp|webp|gif|pdf|doc|docx|ppt|pptx|odt|avi|ogg|m4a|mov|mp3|mp4|mpg|wav|wmv.

So, any other last extensions will be automatically refused. Probably that is why they are talking about the famous double extension to bypass this check. But as mentioned below, the last extension will always be added at the end of the file.

You want more? If by a certain miracle, you managed to bypass every security function implemented, the file is not stored directly in the WPCF7_UPLOADS_TMP_DIR, but in random directory, generate with a 10digit random name that attacker will then need to guess:

wpcf7_init_uploads(); // Confirm upload dir
$uploads_dir = wpcf7_upload_tmp_dir();
$uploads_dir = wpcf7_maybe_add_random_dir( $uploads_dir );

//the function
function wpcf7_maybe_add_random_dir( $dir ) {
    do {
        $rand_max = mt_getrandmax();
        $rand = zeroise( mt_rand( 0, $rand_max ), strlen( $rand_max ) );
        $dir_new = path_join( $dir, $rand );
    } while ( file_exists( $dir_new ) );

    if ( wp_mkdir_p( $dir_new ) ) {
        return $dir_new;

    return $dir;

You want more? After the previous check, the wp_unique_fileneame() function is called on the filename.

And what does this function do? Such a bad luck, replace non-printable char with “-“ char since I think, the beginning of Wordpress. Example of file uploaded with \t or other special chars: Patrowl's blog - Debunking an RCE which CVSSv3

Again? A .htaccess is systematically created for each upload, and Deny every access of file within the upload directory.

function wpcf7_init_uploads() {
    $dir = wpcf7_upload_tmp_dir();
    wp_mkdir_p( $dir );

    $htaccess_file = path_join( $dir, '.htaccess' );

    if ( file_exists( $htaccess_file ) ) {

    if ( $handle = fopen( $htaccess_file, 'w' ) ) {
        fwrite( $handle, "Deny from all\n" );
        fclose( $handle );

Patrowl's blog - Debunking an RCE which CVSSv3

Another last word? Let’s say the ice on the cake.

Once sent, the file and the related folders are … immediately removed.



Patrowl's blog - Debunking an RCE which CVSSv3

Welcome to the desert of the real

Now you understand, here is how the vulnerability could not be exploited:

  • An administrator has installed Contact-Form in 5.3.1version
  • He created a Contact-Form formular including a File Upload functionality
  • He kept the default value of WPCF7_UPLOADS_TMP_DIR so that the attacker could guess it.

The attacker then managed to:

  • Replace the default created .htaccess file on WPCF7_UPLOADS_TMP_DIR to “Allow from all”
  • Change or bypass the list from wpcf7_acceptable_filetypes() function so that he can include a .phpXX extension
  • Remove or bypass the call to wp_unique_filename() function so that he can include non-printable chars
  • The filename is then passed to the “vulnerable” wpcf7_antiscript_file_name and the php security regex check is bypassed.
  • The move_uploaded_file is called with an extension in .php+u0009 for example.
  • The file is stored within the file system in a random dir for a millisecond, and the +u0009 is skipped by the move_uploaded_file function so that we have our malicious .php file stored.
  • Find a way, in a millisecond, to find the random 10-digits temporary folder and reach the php file
  • Or manage to avoid the call to remove_uploaded_files() function and bruteforce the folder... Patrowl's blog - Debunking an RCE which CVSSv3

What truth?

This vulnerability shows on some source few information that could probably allow lost security teams to understand it could not be exploitable, Wordfense is indicating that “Our team was not able to reproduce this issue which leads us to believe there is a high attack complexity or special configuration requirement. », and a pentester is asking some precisions on a comment but without any response from the writer :

This reveals one of many major issues in non-qualified vulnerabilities:

  • First, the CVE should have never been attributed, or with the lowest CVSS score has it is not exploitable in 100% of the case.
  • The exploit has never been challenged or tested correctly before being implemented in web or global scanners, and then reported to Security teams
  • The CVSS has never been challenged, modified, rejected or even deleted for the last 3 years

Now imagine the time and money lost by security teams, asking web agency or providers to patch, following the correction of the so called “critical” vulnerability with related SLA and the stress involved…

Of course, Patrowl’s customers does not have this kind of problematic. The vulnerability is not raised, the only case which has been automated is a hardening notification if a Contact-Form form is detected on a vulnerable plugin version, not because it could be exploitable, just because the plugins need to be updated, but there is absolutely no rush and better things to do before that.

Note: even the Plugin developer lost sometimes on correction. He added a useless regex on the function that remove non-printable char: $filename = preg_replace( '/[\pC\pZ]+/iu', '', $filename ). Which does not bring any increase of the security level of the plugins at all.