BOOK THIS SPACE FOR AD
ARTICLE ADHey, everybody! 🎉 I’m super excited to share this wild bug I recently found in a public bug bounty program. This one was a fun ride, so grab some popcorn and let me tell you all about it.
So, the scope for this target was as wide as the ocean — *.target.tld. Naturally, I went into full detective mode and started hunting for subdomains. And what’s the first thing any seasoned bounty hunter does? That’s right, a good old-fashioned Google Dork: site:*.target.tld -out_of_scope. 🤓
After scrolling through a bunch of results, I stumbled upon a subdomain that piqued my curiosity. Let’s call it mb.target.tld. I hit the page and there it was—a shiny form that let me search for device capabilities using their ID. So, of course, I did what any of us would do—I poked it with a stick to see what breaks. 🛠️
While exploring the page, I noticed a comment in one of the JavaScript files:
Seems that result format/handling/etc has been changedI did some not too funny fixes to make it work here as well
- onAlldevices - result data is string, eval it
- onSingleDevice - without 'callback' response is lost somewhere
I love it when developers leave hints like this! 🕵️ The mention of eval was enough to make me curious.
I started sending requests to the form and inspecting the responses. The original request looked something like this:
GET /[REDACTED_PATH]/DeviceCapabilityDetails?id=1&callback=eval HTTP/1.1In the respond there was some information like bellow:
eval({"Id":1,"Brand":"Alcatel", "SOME_OTHER_INFO":"INFO"});The page was printing SOME_OTHER_INFO: INFO from the response. Seeing eval being used made me even more curious, so I started experimenting with the callback parameter. I quickly realized that whatever I placed in the callbackfunction was included in the response without any sanitization. For example:
GET /[REDACTED_PATH]/DeviceCapabilityDetails?id=1&callback=terrestrial HTTP/1.1terrestrial({"Id":1,"Brand":"Alcatel", "SOME_OTHER_INFO":"INFO"});This didn’t print anything on the page, likely due to an error, but it confirmed that my input was being executed.
My next step was to manipulate the output on the page. I modified the SOME_OTHER_INFO field in the following request:
GET /[REDACTED_PATH]/DeviceCapabilityDetails?id=1&callback=eval({"Id":1,"Brand":"Alcatel", "SOME_OTHER_INFO":"Terrestrial"});// HTTP/1.1Success! The page displayed SOME_OTHER_INFO: Terrestrial. Now we were getting somewhere.
Knowing that Handlebars was likely being used, I decided to push the limits a bit. I started by executing a simple operation:
GET /[REDACTED_PATH]/DeviceCapabilityDetails?id=1&callback=eval({"Id":1,"Brand":"Alcatel", "SOME_OTHER_INFO":4*4});//And just as I expected, the template engine processed the 4*4 and printed 16 on the page. 🎯
At this point, I wanted to see if I could execute more complex code. I tried the following:
callback=({"SOME_OTHER_INFO":(function(){return+this;})()});//This printed [object Object]. Then, I went a bit further:
callback=({"SOME_OTHER_INFO":(function(){return+this.constructor.constructor;})()});//That printed function anonymous() {}, confirming that I was interacting with the underlying JavaScript engine. Unfortunately, I wasn’t able to escalate this to full RCE, but it was clear that this SSTI could have led to more serious consequences.
Here are the key takeaways from this experience:
Always sanitize and validate user data: Especially when dealing with callbacks and template engines like Handlebars.Keep your libraries up to date: Older versions of template engines often have known vulnerabilities. It’s worth the time to upgrade.That’s it for this write-up! While I couldn’t fully exploit the SSTI to run arbitrary commands, the vulnerability itself posed a significant risk and was definitely worth reporting.
Thanks for reading, and happy hacking! 🕵️♂️
#BugBounty #SSTI #WebSecurity #Infosec #Handebars #CyberSecurity