Ram Gall speaking at WordCamp Phoenix 2020

Episode 67: Avoiding Common Vulnerabilities When Developing WordPress Plugins

Almost every week, a new vulnerability is discovered in a popular WordPress plugin or theme, leaving developers scrambling to fix it before it’s widely exploited. Surprisingly, almost all critical vulnerabilities boil down to a few common mistakes. In this talk from WordCamp Phoenix, Ramuel Gall reviews these common errors and provides advice on creating secure plugins.

Find us on your favorite app or platform including iTunes, Google Podcasts, Spotify, YouTube, SoundCloud and Overcast.

Click here to download an MP3 version of this podcast. Subscribe to our RSS feed.

You can find Ram on Twitter as @ramuelgall.

Please feel free to post your feedback in the comments below.

Transcript for Episode 67

Kathy Zant:
Hey everyone. This is Kathy Zant and this is Think Like a Hacker, the podcast about WordPress, security and innovation. This is episode 67. This week we will have two episodes, 67, which is this one, and a separate episode later this week with the latest in WordPress and WordPress security news. We have a couple of stories in the works, but today we wanted to feature Ramuel Gall’s talk at WordCamp Phoenix earlier this month in February 2020. Ram is one of our senior quality assurance professionals. He makes sure that everything that happens at Wordfence is of the highest quality. From malware signatures, to firewall rules, to proofreading a blog post, Ram is one of those guys that just gets things done and can do a lot of different things. Ram is also a GIAC certified web application pen tester or penetration tester. That means he finds web application vulnerabilities, and no matter what I need Ram to do at any given moment, Ram is there to help. He’s just definitely one of those quintessential team players that you always want on your side. He’s also a ton of fun to work with.

I’ve been encouraging Ram for the last year to throw his name in the ring and share his love of application security with the WordPress community. He jumped up to the challenge. This is his second WordCamp talk. His first was at WordCamp Long Beach last fall. This talk is entitled Shut the Front Door, How to Avoid the Most Common Critical Vulnerabilities When Developing Your Plugin. Ram will talk about the different types of vulnerabilities and how these flaws end up in WordPress plugins. If you’re thinking about writing a WordPress plugin, or maybe you already have, Ram tells you about the common pitfalls plugin developers run into and how you can write more secure code. This talk is also available on our YouTube channel where you can also see Ram’s slides. Without further ado, here’s one of my favorite members of the Defiant/Wordfence team. Who am I kidding… I have so many favorite members of our team. We hope you enjoy.

Ramuel Gall:
Welcome to Shut the Front Door. My name is Ramuel Gall. I am a QA engineer at Wordfence. I am a GIAC certified web application penetration tester, also an EC-Council Certified Hacking Forensic Investigator, but I didn’t put that on the slide show because it’s not as relevant. If you put these together, I get to spend a lot of time looking at our firewall rules and which vulnerabilities are commonly targeted. Here’s what we’re going to cover. First we’ll go over some definitions, cross site scripting, cross-site request forgery, SQL injection and remote code execution. I just showed you the acronyms. We’re going to cover what successful attacks have in common. We’re going to cover what attackers do once they get in. We’re going to cover what could go wrong, which turns out it’s mostly broken access control. I’m going to provide some details and examples. We’re going to cover what to do instead.

Let’s start with the definitions. First is XSS, cross site scripting. That really is just running code in someone else’s browser, usually JavaScript, almost always JavaScript. CSRF, cross-site request forgery, is using someone else’s session, doing an action as someone else because you got them to click a link. Now SQL injection, SQLi, that’s running commands on someone else’s database. RCE, remote code execution, that’s running code on someone else’s server.

Cross site scripting, there’s two kinds. There’s reflected cross site scripting. It’s usually used in targeted attacks. It usually requires user interaction. You have to get someone to click a specialized link. It’s useful for a social engineering and it’s usually done to exploit CSRF, or at least that’s what it’s mostly used for in targeted attacks. Then there’s stored cross site scripting, which is the kind we see more often in untargeted wide-scale attacks. It’s using widespread drive-by attacks. It’s used to permanently insert malicious scripts. It is much more common in attacks against WordPress plugins.

Cross-site request forgery, basically that’s taking an action as the targeted user by hijacking their session. It typically, but not always, requires user interaction. It’s often used with cross site scripting because if you can get someone to click a link, let’s say someone’s logged in as an administrator on the WordPress site, you get them to click a link that submits a form that creates a new administrator … you. That’s a good example of that. Where cross site scripting comes in, well, that’s usually useful in getting past nonce protection. Nonces do help prevent this if there’s no cross site scripting vulnerabilities present.

SQL injection, usually it occurs when input isn’t validated correctly. It’s been less common in recent years. WordPress makes preparing statements easy so that you’re not just injecting raw SQL into your database queries. Then there’s remote code execution. Usually, PHP or shell code are used when WordPress is targeted because all WordPress runs on PHP and most WordPress runs on Linux. It’s typically very high severity because anyone who manages to pull this off can basically take any action as the PHP process owner. It can be used to establish permanent back doors.

What do successful attacks have in common? Well, first of all, they tend to take advantage of multiple flaws. It’s very rarely just a cross site scripting vulnerability. It’s cross site scripting, plus we forgot to check that this person’s allowed to insert a script because administrators usually should be allowed to do stuff like insert scripts. They tend to be low complexity. That is to say someone can just look up an exploit and copy it and put their own payload in it. They tend to require minimal privileges. A vulnerability that an unauthenticated user can exploit or that someone who’s only got subscriber permissions can exploit is going to be much more commonly attacked than one where only an editor or someone with publishing permissions can pull it off.

They tend to require minimal user interaction. Social engineering takes effort, not always a lot, but it does take some effort. If it’s an attack that happens when someone just visits the site or goes to a specific page on the site that they were going to visit anyway, it’s going to be much more commonly attacked than anything that requires someone to click a specific link. They tend to be high impact or high severity. Most attackers are not going to go after vulnerability that just gives them general information on how your server’s configured unless they’re specifically targeting you. The last and maybe most important thing, successful attacks tend to be monetizeable. Hackers like making money. That’s why they do all this.

What do attackers do when they get in? Well, usually one of three things. They tend to insert malicious JavaScript, which counts as stored cross site scripting. They tend to update options or they tend to upload a backdoor if they can. Let’s go over insert malicious JavaScript. Usually that will be used to redirect visitors to a malvertising site, which is like advertising except it takes you to an online gambling or something less benign. We also see malicious JavaScript used to scrape payment information. Anyone heard of Magecart? We have seen at least one campaign where the malicious stored JavaScript was actually used to insert an admin user via AJAX. It would actually show up on the administrative panel and when an administrator logged in, it would literally scrape the nonce used to make changes. At that point it’s basically stored cross site scripting plus stored CSRF except it’s no longer cross-site either.

We see a lot of attacks attempting to update options. A lot of the time they will change the home or site URL values to redirect visitors and, again, to malvertising sites where they can make money. The next two usually go together and usually they’ll change “users can register” to 1 if users can’t already register. They will change the default role to administrator, so the next time they make a user, they’re an administrator and they own your site now. We also do see options update vulnerabilities used to add malicious JavaScript.

Again, a lot of the time it’s via custom CSS fields or social media user ID fields, anything where the output isn’t sanitized. If they can, they will try to upload a backdoor. One of the big uses for this is hosting spam or a phishing content. SEO spam is a big business, building dirty back links and phishing. I mean, if you can get someone’s account credentials using phishing on a site that’s not blacklisted yet, people aren’t going to get that Google Chrome warning. We see basically back doors used to add malicious redirects to malvertising sites as well. You’ll see that happen a lot. We will also see that used to scrape payment information though through a server side code instead of Javascript. Last, but not least, if someone has a backdoor on your site, they can use it to attack other people’s sites, which is not a good look.

What could go wrong? Well, it’s mostly broken access control. One of the number one things we actually see broken is failing to add access control to settings or import functionality and sufficient access control, I should say. Usually this means using a little function called is_admin for access control, which does not do what it sounds like it does. We also see plugins using a function called admin_init for access control, which also doesn’t do exactly what it sounds like it does. We also see admin_action hooks for access control, and those also don’t do what they sound like they do. Finally we see plugins using ajax_nopriv hooks for administrative functions, and those do exactly what they sound like they do. They allow an unprivileged user to take an action.

We do also see failing to check nonces is a lot. Now these are usually a little bit less critical because again, if a nonce is the only thing standing in your way, then, well, you’ve got bigger problems, but not having a nonce will lead to a CSRF vulnerability and, while those usually need to be targeted attacks, they are still problematic. And last but not least, making nonces available to unauthorized users. This is actually worse because it doesn’t require social engineering or anything like that. If someone can log in as a subscriber and view the nonce that you think is standing between them and an administrative function, then they’ll be able to do that administrative function.

So I brought some details and examples today. So let’s start with is_admin. Now, it only checks if an administrative page is attempting to be displayed. It doesn’t check if you’re an administrator. So a logged in subscriber will pass this check and an unauthenticated request to admin-ajax.php or admin-post.php will also pass this check. And the example I chose for this is the Social Warfare plugin. It used the is_admin function to protect a debugging and settings migration function. It could be pointed to a crafted configuration document. You could just give it a getParameter to a URL that you set up ahead of time and stick a malicious script in there, or stick some code in there and it would be executed.

So most attacks earlier on changes the Twitter ID since it’s a social plugin, and they also failed to sanitize the output of the Twitter ID. Like I said, broken access control plus other things. We’re mostly focusing on the access control because it’s the mistake you see most frequently. But yes, most attacks [on Social Warfare] changed the Twitter ID to a malicious script and use it to redirect visitors to malvertising sites, or they used it to add a backdoor. Not the Twitter ID, they actually just put PHP code that grabbed yet another file and wrote it to the server. It also did lack a nonce check, not that that would have been sufficient, but it would have helped.

And the good news is, the developer of this plugin acted very quickly to fix this issue. I think it was within a day or two. One of the big lessons from this is, if you have an issue, it’s how you respond to it is the most important thing. I’m cutting out. There we go. I brought some code screenshots. If you look near the top, there’s a is_admin function, and look, they’re trying to use it to keep people out. And that clearly doesn’t work. And you’ll notice how the next thing is, it tries to assign file_get_contents of whatever URL it gives you, or you pass to it, and puts that in options array. Then it doesn’t eval on that array. But if they decide not to go for the remote code execution vulnerability, it will then update the Social Warfare settings options. See, we got a two-fer, an options update and remote code execution vulnerability. And this might seem a little bit limited except for the fact that, again, the Twitter user ID wasn’t sanitized, so cross site scripting.

The next not-so-safe function we’re going to cover is admin_init, and it runs when any administrative screen or script is initialized. A logged in subscriber will pass this check, and an unauthenticated request to admin-post.php or admin-ajax.php will pass this check as well, just like the last function. The main difference is that, that one’s supposed to only run or only pass when you’re attempting to load an admin screen. This one actually runs stuff when you attempt to load into admin screen. The Easy WP SMTP vulnerability is the one we’re going to cover for this. It used admin_init to protect a settings import feature that allowed arbitrary options to be updated. Some attacks updated the home or site URL to a malvertising site. Others changed users_can_register to 1 and updated the default role to administrator, and people ended up finding rogue administrators on their sites.

Again, the plugin developer released a patch a very quickly. I think it was again within a few days. Kudos to them. And here’s some screenshots. I did skip around in the code a little bit, but see how they’re adding an action on admin_init, and they’re literally naming the function that they’re running on admin_init, “admin_init.” Which is very creative. Anyways. Just pass it something in the files array and it unserializes whatever’s in that file you pass it, which also could be an object injection vulnerability, depending on what else is installed. But those are fairly rare these days. Well, fairly rarely exploitable these days, I should say. But if that’s not exploitable, it’ll just update your site options with whatever was in that serialized data in that file that you passed it.

Next we’re going to cover admin_action hooks. Now, these are registered on most pages in the admin interface. They can be triggered by subscribers, again. The Duplicate Page vulnerability is the one we’re going to cover for this, I used an admin_action hook to trigger duplicate post functionality, and this resulted in a SQL injection vulnerability. You will see it probably, hopefully, as soon as it’s highlighted. And the plugin author responded reasonably quickly. It took them about a week to actually get an updated version out. There were some problems, but they still managed to take care of it and had a decent response.

I’ve highlighted that admin_action_dt_duplicate_post_as_draft, and once that action runs it takes whatever you put in the getParameter, the getParameter called post, yes, that’s confusing, and assigns that to the post ID variable. And then if you look at the very bottom, it’s literally doing a SQL query with whatever it is you just fed it from that getParameter, and that can lead to SQL injection. Now, the good thing about modern SQL injection is most of the time you can’t actually use it to change things in a database, at least the way WordPress is set up, but it still can lead to exfiltration of data.

I don’t actually know what they use this one for much. I just thought it was a cool example. Now, this was actually one of my favorite, or least favorite, because it’s actually becoming a little bit less common. But when it does happen, it’s usually very bad. That’s using ajax_nopriv hooks. That’s not to say you shouldn’t use them, just don’t use them for anything that should require privileges, because an unauthenticated user can use the registered action. Adding capability checks and nonce protection does make these safer. If you want to have one AJAX function to rule them all, you can still do it and use regular AJAX hooks and ajax_nopriv hooks, and just have your capability checks if you want to do something administrative. But it’s probably safer to split it up. Yeah, you should probably still only use these for actions you want to be publicly accessible.

The example here is the Total Donations plugin. It used an unprotected ajax_nopriv hook, and this allowed arbitrary options updates. So some attacks added malicious JavaScript via a custom CSS option, other attacks, once again, changed users_can_register to 1 and changed the default role to administrator. We still see people trying to attack this even though… Well, spoilers. Even worse, the plugin had an alternate AJAX endpoint that meant it was vulnerable even if it was deactivated. You had to completely remove this plugin in order to make it not vulnerable.

Worst of all, and this is what I was talking about spoilers, there was no response from the plugin developer when people tried to notify them. CodeCanyon, Envato, had a wonderful response. They removed it as soon as it was made clear that the developer wouldn’t be fixing it. At least someone didn’t drop the ball. If you take a look at this it adds a add_action to miglaA_update_me(), and it just grabs whatever is in the POST[‘key’] and POST[‘value’] parameters and updates those options to whatever you want, which is kind of terrifying. No checks, no balances, just, “Oh yeah, I’m going to update all your options.”

The next two examples I’m going to cover are… Well, I wouldn’t call this one quite as not severe. Nonce available to unauthorized users. Because this one could actually be fairly severe and we still have seen some attacks for it. Basically the problem is it makes nonce checks irrelevant if anyone can grab your nonce. It doesn’t require social engineering, just access to a location where the nonce is shown. Now, that is usually a subscriber-accessible area so it’s going to be a little bit less heavily attacked than something that doesn’t require authentication at all, but there are a ton of sites that do allow open registration. This would have been a problem on those sites. The good news for these is that access controls do still work. This is also nonce available, but they also didn’t actually check to see if the person taking the action was an administrator.

The example I’m using for this is the Ad Inserter plugin. Attackers could basically use the ad preview functionality to execute arbitrary code. An attacker could basically set a cookie for a certain value, and once they set that cookie they could get a nonce that let them preview code on the homepage if they were logged in as a subscriber. They used a check_admin_referer to check the nonce, which is fine if all you’re doing is protecting against CSRF. It is not adequate to actually perform access control. The good news is the plugin developer released a patch the next day so great response on their part. This was actually one of the folks on our team who discovered this one. Here’s just a screenshot of, hey, there’s the nonce just chilling in the source code of the homepage after you set that cookie.

Okay, this is a pretty busy screen, and there’s a lot going on here. But basically, it does a check_admin_referer, grabs the post preview, sends it to a function called generate code preview. After we skip a bunch of steps it evals whatever you put in the POST[‘code’] parameter.

Finally, and probably least frequently attacked but still important if you are developing plugins and want to follow best practices, failing to check nonces is a bad idea. They’re much less frequently attacked. They typically do require social engineering, but CSRF POST attacks, you can basically send someone a link that goes to a site you control, and it will have a form that auto submits itself if you want to send a post request instead of, say, GET request to their site. Like I said, nonces cover most of the really important functionality in core WordPress these days, or a whole of it, I should say, most likely, but if your plugin does something interesting, then well, you should still have them. Failing to check nonces will allow CSRF attacks, even if you do have other access controls in place, so even if you are actually checking that someone has permissions to do something. It basically allows the attacker to use the unprotected function as the targeted user. It’s harder to exploit, but if you can exploit it, then it can get very bad.

The worst thing is that firewalls offer no protection against CSRF. They can offer protection against certain payloads, like if you’re trying to trick someone via CSRF into inserting malicious JavaScript on their own site, then a firewall might catch it. But if you’re trying to trick them into creating a new admin user, and your plugin allows them to do that, it probably won’t.

The example I have here is the WP Maintenance plugin. I think Chloe discovered this. Hi, Chloe. Basically, plugin failed to check the nonce for updating its own settings, and attackers could add a malicious script to newsletter titles. If for some reason it had a newsletter functionality, whenever it sent out a newsletter, you could basically send a malicious JavaScript out along with that newsletter. Or whenever an administrator checked on their newsletters, then they would be… the script would run.

Attackers could also enable maintenance mode, which meant that they could, temporarily at least, take down your site. Good news is, the plugin developer did release a patch within a day. Again, an excellent response. Most of the responses we see are actually pretty good. Plugin developers are getting much better about this than they may have been in the past.

Code screenshot, I haven’t highlighted anything because it just shows that they can update the options for that. That entire page is hidden behind, well, more typical WordPress admin menu access controls, just like when you log into… as a subscriber, you can’t see the admin menu page for plugins. This is one of those pages.

The question becomes, “What should I do instead?” That’s pretty easy. If you’re going to check access control use current_user_can. It allows more fine-grain control if you only want to allow access to people who can say, publish posts or edit plugin settings, you can set that as well. You can also use is_super_admin or is_network_admin if you plan on having your plugin run nicely on a multi-site installation that may cause complications, where only network or super administrators can do the thing and you might want local administrators to do the thing.

Use wp_create_nonce and wp_verify_nonce to protect it against CSRF. It’s not a substitute for access control, but it can still help. It’s better to have a nonce in place than not, and adding more friction to an attack is always a good thing. They always go for the low-hanging fruit, as they say. Be careful which pages show your nonce because nonces can be scraped or otherwise compromised. Also, check_admin_refer also works to verify nonces. I know we actually covered a vulnerability where this is what they did, but just don’t make it the only thing you do to check access control.

Finally, be aware of who can access what. Just because functionality isn’t exposed in the user interface doesn’t mean it can’t be accessed. I guess this is one of those real core lessons because that’s why it’s always settings imports, stuff like that, options updates. It’s the stuff that maybe gets added on as an extra feature, and maybe doesn’t even get fully developed, but they have the functionality in place. Just send it the right parameters and someone can change your settings. Add access controls to anything that changes settings or options.

Finally, treat anything a user can change as user input, because I have seen all sorts of weird stuff being used as user input. Cross-site scripting through HTTP referer headers is a thing.


Audience Question:
Does that work with Booleans?

Ram Gall:
With what?

Audience Question:
Does that apply to Booleans as well?

Ram Gall:
Booleans? He’s asking you if it applies to Booleans. Usually, casting something as a Boolean or an integer does provide some defense, but I have seen at least one options update vulnerability where it allowed you to update the option key to whatever you wanted, but it only allowed you to update the option value to a Boolean value. That meant you could update the home or site URL to false and that would take down someone’s site as well. It can limit the amount of damage someone can do. Again, casting to an integer or Boolean will help limit the amount of damage someone can do with say, a SQL injection, but there are techniques like blind SQL injection that can sometimes overcome this.

Ram Gall:
I’m just going to leave this slide show up to keep in touch. Here’s my contact info and now, I will answer more questions if anyone has any. Okay.

Audience Question:
Okay. So most of these attacks, if I understand correctly, come from people scraping GitHub repositories and stuff like that. If you have your code in a private repository, how many protection will that give you?

Ram Gall:
So the question was that most of these attacks appear to come from people scraping GitHub repositories and whether or not having code in a private repository will protect you? The answer is that if you’re plugin code is not in an open repository… Am I correct in asking that? Well the thing is, the only way to really protect PHP plugin code like that is obfuscation. The truth is, anything that can be obfuscated can be de-obfuscated. Even if you don’t have your plugin on GitHub, if you’re releasing it and people are using it, someone can reverse engineer it or might just stumble upon a vulnerability by accident. A lot of attackers find vulnerabilities through essentially something called blackbox testing. They don’t even know what the plugin code looks like, they just fire up Burp Suite, and just batch test a bunch of common cross-site scripting bypasses on whatever parameters they can find.

Ram Gall:

Audience Question:
Can you just be clear? Your belief regarding nonces is try not to make them available, but if we have a public forum, there’s going to be a nonce on the public forums. It needs to be in addition to you… as long as that’s used with public access or proper access control? Am I right?

Ram Gall:
The question is, should nonces on public forums be used for proper access control?

Ram Gall:
I feel like this is where I fell a little bit short in my description, because you don’t use a singular nonce for everything. It’s more that for every type of action you’d like to take, you should have a separate nonce available. That public-facing forum will have a nonce just to prevent someone else from submitting the form as them, whereas… and you will want to make that nonce available just on the main website. Whereas a form that only an administrator should be able to see to make plugin changes will also have a nonce that’s a very different nonce that should only be shown on that page that an administrator can access.

Ram Gall:

Audience Question:
I have a plugin that doesn’t do anything on the front end and it… but your last point about user input has me thinking. It just checks for certain conditions and sends an email alert if that condition happens, but there’s an input box where the user can put in their own email. It doesn’t do anything to any of the standard WordPress settings, it doesn’t do anything on the front end, but could that still be used somehow …

Ram Gall:
The question is, if you have an email, if I’m correct, an email plugin that doesn’t have any user-facing interfaces, it’s going to be only administrative, but all it does is essentially check configuration information and send email. Well, if you don’t have access control, then an attacker could send the information to grab your site’s configuration information and send an email of your site’s configuration to themselves, and that depends on how sensitive that configuration information is. Like I said, configuration grabbing attacks are not as common, but if someone is specifically targeting your site, you really don’t want them to have that information. Also, they could potentially use that same functionality to send a bunch of emails to people who don’t want them and get your site’s IP on an email blacklist.

Audience Question:
Okay, thanks.

Ram Gall:
Any other questions? Oh, yes?

Audience Question:
I’ve asked this at several security talks. Even though I believe I follow best practices, I have questioned if there’s some sort of third party that could read my code and give me feedback. Do you know of any?

Ram Gall:
He is asking if… sorry?

Audience Question:
Yeah, security feedback is the main thing.

Ram Gall:
He’s asking if there’s any third party that will review your code and give you security feedback. From what I understand, there are a number of consultants that do offer this kind of service. Some of them use automation, some of them do manual review. I can’t give you any particular recommendations though. We do try to hunt for vulnerabilities in common open source plugins, but that is something that you can have done, yes.

Audience Question:
But no recommendations?

Ram Gall:
No recommendations at the moment.

Audience Question:
Every time I get recommendations it’s automation, and that’s not what I want.

Ram Gall:
Yeah. I mean, if you are going to have that, at least half the time, a vulnerability might not be obvious just from reading the code, which is, again, another reason why just having your plugin open source, which is a great thing, isn’t necessarily going to make it more or less vulnerable than having closed-source plugins.

Ram Gall:
I think we may be done. Last call?


Kathy Zant:
We hope you enjoyed Ram’s talk and that you learned something about secure plugin development. If you’d like to get in touch with Ram, you can follow him on Twitter @RamuelGall. We’ll have a link to his Twitter profile in the show notes. And we will talk to you next time on Think Like a Hacker. Thanks for listening.

Did you enjoy this post? Share it!


No Comments