Trustwave's 2024 Financial Services Threat Reports Highlight Alarming Trends in Insider Threats & Phishing-as-a-Service. Learn More

Trustwave's 2024 Financial Services Threat Reports Highlight Alarming Trends in Insider Threats & Phishing-as-a-Service. Learn More

Services
Managed Detection & Response

Eliminate active threats with 24/7 threat detection, investigation, and response.

Co-Managed SOC (SIEM)

Maximize your SIEM investment, stop alert fatigue, and enhance your team with hybrid security operations support.

Advisory & Diagnostics

Advance your cybersecurity program and get expert guidance where you need it most.

Penetration Testing

Test your physical locations and IT infrastructure to shore up weaknesses before exploitation.

Database Security

Prevent unauthorized access and exceed compliance requirements.

Email Security

Stop email threats others miss and secure your organization against the #1 ransomware attack vector.

Digital Forensics & Incident Response

Prepare for the inevitable with 24/7 global breach response in-region and available on-site.

Firewall & Technology Management

Mitigate risk of a cyberattack with 24/7 incident and health monitoring and the latest threat intelligence.

Solutions
BY TOPIC
Microsoft Security
Unlock the full power of Microsoft Security
Offensive Security
Solutions to maximize your security ROI
Rapidly Secure New Environments
Security for rapid response situations
Securing the Cloud
Safely navigate and stay protected
Securing the IoT Landscape
Test, monitor and secure network objects
Why Trustwave
About Us
Awards and Accolades
Trustwave SpiderLabs Team
Trustwave Fusion Security Operations Platform
Trustwave Security Colony
Partners
Technology Alliance Partners
Key alliances who align and support our ecosystem of security offerings
Trustwave PartnerOne Program
Join forces with Trustwave to protect against the most advance cybersecurity threats

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

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:

 

18736_picture1c

 

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

 

18737_picture2c

 

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

 

18738_picture3c

 

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:

 

18739_picture4c

 

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/

 

18740_picture5c

 

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.

 

18741_picture6c

 

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

Now, turn on Apache and MySQL in XAMPP.

 

18742_picture7c

 

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.

 

18743_picture8c

 

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:

 

18744_picture9c

 

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

 

18745_picture10c

 

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.

 

18746_picture11c

 

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

 

18747_picture12c

 

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

 

18748_picture13c

 

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

 

18749_picture14c

 

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:

 

18750_picture15c

 

Click the “Run and Debug” at the top:

 

18751_picture16c

 

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.

 

18752_picture17c

 

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).

 

18753_picture18c

 

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.

 

18754_picture19c

 

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

 

18755_picture20c

 

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.

 

18756_picture21c

 

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:

 

18757_picture22c

 

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

 

18758_picture23c

 

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.

 

18759_picture24c

 

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.

 

18760_picture25c

 

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

 

18761_picture26c

 

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:

 

18762_picture27c

 

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

 

18763_picture28c

 

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

 

18764_picture29c

 

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!

ABOUT TRUSTWAVE

Trustwave is a globally recognized cybersecurity leader that reduces cyber risk and fortifies organizations against disruptive and damaging cyber threats. Our comprehensive offensive and defensive cybersecurity portfolio detects what others cannot, responds with greater speed and effectiveness, optimizes client investment, and improves security resilience. Learn more about us.

Latest Intelligence

Discover how our specialists can tailor a security program to fit the needs of
your organization.

Request a Demo