- Critical Thinking - Bug Bounty Podcast
- Posts
- [HackerNotes Ep. 141] Pwning the Pod - Google Docs 0-day & React createElement Exploits with Nick Copi (7urb0)
[HackerNotes Ep. 141] Pwning the Pod - Google Docs 0-day & React createElement Exploits with Nick Copi (7urb0)
In this episode, Justin sits down with Nick Copi to: unveil a Google Docs 0-day, represent John Denver's second favorite state, discuss the potential for XSS in React's createElement function, theorize on CSPTs, bring attention to a window.replace.location quirk, walk-through a CSS injection exploit, and hear what Nick considers to be his "worst crit".
Hacker TL;DR
createElement: React's
createElement()
function is a potential sink for DOM XSS/CSS injection if you are able to influence its parameters that set the component type, its attributes/properties, and its child nodes.React useState + YAML Deserialization Vulnerability: Older versions of
js-yaml
include a dangerousyaml.load()
method that returns functions from YAML files instead of objects. If these deserialized functions are passed directly to a ReactuseState
setter, it will be interpreted as a state updater and executed.window.location.replace: Due to browser throttling,
window.location.replace
can only be called up to 200 times.fontleak: Have a CSS injection? Use fontleak to exfiltrate page content.
"Who is Nick Copi?" - Rhynorater Senior
"Heralded from birth", Nick Copi claims to have learned how to spell from playing NetHack. At age 10, he began writing in Small Basic. In his teen years, he became proficient with HTML and JavaScript and gained his first web hacking experience on a Rails application.
When he wasn't reverse-engineering/hacking highly-obfuscated flash games, he was attending class at the Chesterfield Technical Center and collecting security certifications.
After participating in local CTFs it didn't take long until he decided to host his own CTFs for a non-profit. What a guy.
At 17, he met dawgyg at a local event and came to the realization that everything DeVoss was saying, as crazy as it seemed, was real.
In true hacker spirit, today's guest skipped his own high school graduation ceremony to go to the RVASec conference instead.
At Virginia Commonwealth University he worked as a part-time developer for the school's lab and classroom management software, and still found the time to run the campus cybersecurity club for over a year.
Born in Richmond, he is in close proximity to CTBB's very own Justin Gardner, and has been bestowed with the title of being the "best web hacker in Richmond" by Justin himself.
With "more hours spent debugging JavaScript in the last year than in direct sunlight", nowadays, Nick Copi is an AppSec engineer at a Fortune 200 company by day and an independent security researcher by night.
It is likely that many of you are already familiar with Nick (formerly known as "Demo", now known as "7urb0") as he is a highly active member in the Critical Thinking community and is always there chatting and helping people.
You can read what he's up to by following him on X (@7urb01) or by visiting his website (https://www.turb0.one).
Bring-a-Bug: Google DocS
In this week's episode, the "Bring-a-Bug" segment was taken in the literal sense as Nick was able to achieve a client-side DoS attack in the podcast preparation Google Doc he sent to Justin.
Using Chrome DevTools, Nick set a breakpoint on RegExp.prototype.exec
using debug()
and refreshed the page. He then replaced the function with a proxy wrapper call to enumerate every unique regular expression that was called.
With a list of all the regex calls in hand, Nick manually investigated the ones that seemed "sketchy". Eventually, he discovered a source that reached an inefficient regular expression as a sink.
Because the native copy event on a document allows copying arbitrary MIME type data to the clipboard, Nick demonstrated that an attacker could paste in the payload as "rich content" that would cause a crash on hover. To prevent the crash once the payload was added to the document, an attacker could set conditional breakpoints. However, without these protective breakpoints, the crash would occur immediately.
From Component to Compromised: XSS via React createElement
Throughout the episode, Justin and Nick discuss the potential to pop DOM XSS if you are able to find a source that passes your input to a createElement call.
Read Nick's detailed write-up on how he was able to turn a createElement based XSS vulnerability into RCE: Weaponizing Chrome CVE-2023-2033 for RCE in Electron: Some Assembly Required
Due to technical issues, Nick's DEFCON presentation on the subject wasn't fully recorded. However, you can view all of the resources and try to exploit instances of the vulnerability yourself in labs by visiting https://defcon.turb0.one/.
The createElement()
function is used to (you guessed it) create React elements.
It takes three parameters:
const element = createElement(type, props, ...children)
type: Can be either tag name strings (i.e.
"div"
becomes<div></div>
) or a component class that will be called to construct the element.props: Can be either an object or
null
. Key/value pairs in an object will be assigned to the created element as attributes if the type is a tag name string, or as properties if the type is a component class....children: The child node(s) of the created element.
If an attacker is able to pass input from a source into a createElement()
sink via one or more of these parameters, they can influence the generation of HTML, and achieve DOM XSS/CSS injection (version permitting).
Assuming you control deserialized JSON or serialized HTML that is being passed to createElement()
, the parameters you can dictate determine what you can achieve:

CSS Injection
If you missed the conversation on this bug, start here: https://discord.com/channels/1110206757227216916/1168685918920638614/1365777376138493973
Nick covers how he used CSS injection to exfiltrate a 1,400 character session token from a <script>
tag with finely-tuned key frame animations by modifying the fontleak font.
You can read more about fontleak here: https://adragos.ro/fontleak/
Tom's Crazy S**t
If you missed the conversation and have access to the critical-thinkers
channel in the podcast's Discord server, you can catch up by starting here: https://discord.com/channels/1110206757227216916/1413595112306376804
The TL;DR is:
If you have an injection in a window.location.replace()
call that is immediately followed by window.location.replace(window.origin)
, the window.origin
call takes priority, meaning you payload will not get the chance to execute. However, thanks to @Rafax00 a workaround was discovered:
I've found a way to pop the alert(), but it requires a harmless sync in the same origin as the target.
If you look at the window.location.replace()
docs, it says (https://developer.mozilla.org/en-US/docs/Web/API/Location/replace):
Browsers throttle navigations and may throw this error, generate a warning, or ignore the call if it's called too frequently.
So there is a limit to it (200 calls on Chrome and Firefox). By putting it into the console, you will see the alert:
for (i=0; i<199;i++){window.location.replace("#"+i.toString());};
window.location.replace('javascript:alert()');
window.location.replace(window.origin)
However, the limit is tied to the ( origin + tab ), so an external website can't trigger the limit alone; We need a navigation sync in the website, like the following:
window.onhashchange = function(event) {
const newHash = event.newURL.split('#')[1] || '(none)';
document.getElementById('log').textContent += `${newHash}\n`;
if (newHash != "dashboard"){
window.location.replace('#dashboard');
}
};
I've built this POC http://impossible-xss.bugport.net/:
/impossible-xss.html (our
javascript:
injection)/navigation-sync.html (a page in the same origin that performs an arbitrary number of navigations)
This is the exploit code (on the attacker's website):
target="http://impossible-xss.bugport.net/navigation-sync.html";
win=open(target);
setTimeout(function(){
for (i=0; i<199;i++){
win.location.replace(target+"#"+i.toString())
};
}, 1000);
setTimeout(function(){
win.location.replace("http://impossible-xss.bugport.net/impossible-xss.html")
}, 1200);
Genius.
7urb0's Proverbs
Although he appears youthful, 7urb0 is actually 1337 years old and has gifted us with some words of wisdom to reflect upon:
"Fuzzing is really a form of agriculture. Bug agriculture."
Go tend to the crops.
"Imagine you're a React useState setter and you get passed a function and you're like: 'Hell yeah, I'm running that'."
In prior versions of the
js-yaml
package,yaml.load()
calls deserialized and returned functions from YAML files. If these functions were passed directly to a React useState setter it interpreted the function as a state updater and executed it.
"F***ing the application on the first date."
The concept of working backwards to achieve exploitation. Envisioning the exploit and then collecting the required gadgets in order to construct a chain that will produce it. Get intimate with the application first, wine and dine later.
"Hacking is about watering those mustards seeds of faith that say, "This can be exploited", until they sprout into magnificent full chain mustard tree."
In Closing: 7urbo's Worst Crit
In contrast to the technical depth of the vulnerabilities that Nick had covered throughout the episode, what he considers to be his "worst crit" is simple.
While testing a Next.js application, Nick noticed a JWT reflection in the checkout page. After decoding the token, he noticed something strange in the claims: someone else's email address.
Apparently, a cache misconfiguration was leaking other the tokens of other users. So, each time he refreshed the page, he could become someone else and hit authenticated endpoints to gain access to sensitive PII.
That's it for this week.
And as always, keep hacking!