~/posts/why-the-fuck-did-i-write-a-rust-crate

Why the fuck did I write a Rust crate?

12 min read 2336 words
rustcybersecurityfreeboxapitooling

tl;dr

I wrote freebox-rs because curl and Python scripts became the weakest part of my Freebox OS research. The crate made pairing, tokens, sessions, TLS, typed endpoints and raw probing reliable, and over one weekend I reported two vulnerabilities, kept digging into a third, and rediscovered an old Freebox easter egg.

This started as a challenge I gave myself: could I get something RCE-shaped on a Freebox, software only?

The idea came from hardware-heavy router and terminal research, especially the Starlink work where Lennert Wouters used voltage fault injection against the user terminal. The write-ups are ridiculous in the best way: WIRED covered the custom modchip and fault injection path, Ars Technica summarized the solder-heavy route to code execution, and the talk itself, Glitched on Earth by Humans, is exactly the kind of research that makes you look at consumer network hardware differently.

I loved the idea. Unfortunately, I suck at hardware.

So I looked at the box I already had: a Freebox in my living room, running a lot of software, exposing a documented API, and carrying the strange historical weight of French ISP hardware that has been iterated on for a very long time.

What came out of it:

  • freebox-rs, a Rust crate for Freebox OS,
  • two vulnerabilities privately reported to the vendor, which I scored 8.5 (High) and 8.7 (High),
  • a third bug that got me closer than expected to becoming root,
  • and, somehow, the rediscovery of an easter egg old enough to make the codebase feel like archaeology.

No payloads, no vulnerable endpoints, no reproduction steps. This is about the research loop, the tooling, and why the crate was the difference between poking at a router and doing something repeatable.

The starting point was not elegant

The first research stack was what you’d expect:

  1. curl
  2. Python + requests
  3. some JSON
  4. regrets

That’s fine for a first look. It’s even good for a first look. curl makes the shape of an API visible, Python makes iteration cheap, and Burp-shaped workflows plus throwaway scripts are how a lot of useful security research begins.

But Freebox OS has enough surface that the scripts became the weakest part of the process almost immediately. Every idea needed the same boring setup, done correctly:

  1. discover the API version and base URL,
  2. handle the login challenge,
  3. compute the session password,
  4. send the session request,
  5. carry the session token,
  6. refresh when the session expires,
  7. remember which app token belongs to which local tool,
  8. deal with TLS policy around Freebox hosts,
  9. call a typed endpoint when possible,
  10. drop to raw JSON when necessary.

Tinkering is fun, but finding actual bugs is more fun, so I had to drop this garbage.

Freebox OS has a very particular API

The Freebox OS API isn’t a tiny hidden debug port with three routes and a prayer. It’s a real REST-ish API with authentication, permissions, system configuration, LAN state, DHCP, filesystem operations, downloads, Wi-Fi, TV/PVR, player control, storage, firewall, calls, notifications, VMs, domotics, and a long tail of optional domains.

It also feels old, the way long-lived product APIs do. Not bad! Just.. old.

Some endpoints are clean. Some return shapes that need compatibility handling. Some domains are typed enough to deserve proper models. Some endpoints return configured credentials in cleartext. That surprised me for about five seconds, then I remembered this is a router admin API: if a caller can read that configuration, it can usually change it too.

With that many domains, stateful auth, old compatibility layers, physical pairing and privileged router config, the problem is less about sending one clever request and more about moving quickly across the system without lying to yourself. That’s where the crate came from.

Physical approval changes the tooling problem

Freebox OS apps authenticate through pairing. A tool asks for authorization, the actual, physical Freebox shows a prompt, and you approve it by hand.

Sending an HTTP request that needs a physical button press is super cool. It’s also annoying during repeated local research, because I have to get out of my bedroom.

So the crate makes pairing a first-class flow: describe the app, request authorization, poll its status, persist the granted app token in a token store, load it later, start sessions from it, refresh them automatically, and keep secrets out of debug output.

your toolFreebox OS APIFreebox displayonce, physicallyevery run, automaticallyrequest authorizationshow the promptyou press “yes”poll status → grantedstore the app tokenchallenge → session tokencalls + X-Fbx-App-Authinvalid sessionrefresh once, retry
Pairing happens once, with a button press on the box. After that, every run turns the stored app token into a session, sends it as X-Fbx-App-Auth, and refreshes and retries once if the session expired.

This is the kind of work that feels over-engineered until the first time it saves an entire evening.

Once token storage and session refresh were reliable, I could stop thinking about authentication. The crate opens a session, attaches X-Fbx-App-Auth, retries once on an invalid session, and hands back a stable Freebox handle:

let client = Freebox::client_for(&base_url, TlsPolicy::Auto)?;
let token_store = TokenStore::default_for(&app)?;

let token = match token_store.load(&app)? {
    TokenLoad::Valid(token) => token,
    TokenLoad::Missing | TokenLoad::NeedsAuthorization => {
        authorize_app(
            &client, &base_url, &app, &token_store, config,
            |track_id| {
                eprintln!("approve this app on the Freebox \
                           display: {track_id}");
            },
        )
        .await?
    }
};

let freebox = Freebox::from_app_token(
    client, base_url, app.app_id, token.app_token,
);
let config = freebox.system().config().await?;

Why Rust?

Honestly: I wanted to learn Rust.

There are better marketing answers. Rust has good type modelling, strong error handling, no garbage collector, nice async tooling, and a culture of explicit API boundaries. All true. But the real reason was that I wanted a project where Rust was useful without being performative.

This was a good fit. The crate had to model a network API, handle secrets carefully, offer ergonomic builders, keep raw escape hatches, and avoid turning every endpoint into a pile of serde_json::Value. It needed enough structure to be useful and enough flexibility to survive a router API that wasn’t designed around my type preferences.

That tension is productive. If everything is raw JSON, the crate is just a fancy requests.Session. If everything is modelled too aggressively, it becomes brittle and every undocumented field is a crisis. Where I landed:

LayerDesign
authtyped and strict
token storagetyped and careful
transportshared, authenticated, refresh-aware
stable domainstyped structs and builders
uncertain domainsraw JSON escape hatches (unfortunately, soon to be deprecated though)
unsupported endpointsgeneric get, post, put, delete, raw_response

That made it useful both for normal automation and for security research.

What the crate actually gave me

To test a hypothesis, I could quickly authenticate cleanly, enumerate a domain, inspect the raw shape, promote stable parts into typed models, change configuration through builders, compare behaviour before and after a state change, chain one observation into the next request, keep the same token/session behaviour across every test, and fuzz around an endpoint without rewriting setup.

That last point matters. Fuzzing an API by hand isn’t random strings and chaos. It’s structured exploration: vary one field, hold the rest, watch the error classes, pivot to a nearby endpoint, compare authenticated and unauthenticated behaviour, see which objects accept partial updates, and keep enough logs to reconstruct what happened.

“I should test this later” versus “I can test this in thirty seconds” is an enormous difference. Most ideas are wrong, and the faster you can kill them, the more time you have for the weird ones (and god did I have weird ideas, like physically shaking my Freebox).

The research loop

Roughly:

  1. start with a hypothesis,
  2. send a small probe,
  3. observe the API’s behaviour,
  4. decide whether it’s interesting,
  5. discard boring branches quickly,
  6. stabilize promising requests in the crate,
  7. explore nearby variants,
  8. try to chain behaviour across domains,
  9. validate impact safely,
  10. write a private report.

The important word is “safely”. This was my own device, in my own environment, against a system I was allowed to test (I hope so at least). Even then, the goal was never to brick the box or break my WAN. I needed to understand impact, reduce uncertainty, and write reports a vendor can triage without a dramatic story around them.

The crate helped because it made probes reproducible. A report is much stronger when you can say what state was required, what the API did, and what impact follows, without mixing in “also my script was a bit cursed”.

What I found (without disclosing it)

I privately reported two vulnerabilities. They’re still in triage, and I’m not publishing technical details. Both were tested against the then-current Freebox OS release, 4.10.1, and at a high level both can lead to taking over the admin account.

My own CVSS 3.1 scores:

FindingStatusMy scoreHigh-level impact
Vulnerability 1reported, triage ongoing8.51-click account takeover on LAN
Vulnerability 2reported, triage ongoing8.71-click account takeover
Vulnerability 3not reported yet, still investigating?got me unusually close to root

The third one made me rediscover an old easter egg that I’m pretty sure had been forgotten since. I haven’t reported it yet because I want to see whether the path can become an RCE, which would let me explore the system much more efficiently and find more issues with a real view of the runtime.

The easter egg problem

Old consumer devices are weird because code survives: jokes, comments, assumptions, half-removed features, compatibility layers, internal names, test hooks, behaviour designed for a product generation that’s no longer in your living room (or in anyone’s, in fact).

Freebox history has a lot of that energy. A not-so-old Univers Freebox article about Frédéric Hoguin’s old Freebox HD work tells a 13-year-old story involving Doom, root access and developer easter eggs. It mentions the Freebox HD default user id 4242 (and, 20 years later, it still seems to be 4242), a reference to The Hitchhiker’s Guide to the Galaxy, and a root password containing find_this_openfreebox as a nod to the OpenFreebox community: Après 13 ans, une faille insolite et ultra pratique sur une ancienne Freebox dévoilée mais….

Consumer network gear has a long memory. Features get replaced, assumptions stay. Services move, conventions remain. An easter egg can outlive the engineers who wrote it, the threat model around it, and sometimes the product it belonged to.

Finding one during modern security research feels like opening a wall in your apartment and finding a message from a previous tenant.

openfreebox.webp screenshot of me, reading /etc/passwd remotely, by exploiting my fancy third vulnerability

The crate is meant to be useful

The motivation was security research, but I don’t see the crate as disposable scaffolding. It’s meant to be a real Freebox OS client: async, reusable, token-aware, TLS-aware, typed where useful, raw where necessary, and usable both for normal automation and for research.

Security tooling gets better when it’s built like software instead of a one-off stunt. Automation tooling gets better when it’s written by someone who spent time asking how it breaks. The crate doesn’t need exploit code to be security-relevant! It gives researchers and automation code a stable way to talk to Freebox OS, and that’s enough.

It’s public: github.com/ggmolly/freebox-rs.

The 3-day weekend

The compressed timeline is almost stupid:

DayWhat happened
Friday (08/05)first high-severity issue found and reported
Saturday (09/05)second high-severity issue found and reported
Sunday (10/05)crate finalized and published

The first commits were older, and the idea had been around for a while. But the intense part went fast because the tooling had finally become good enough. By Sunday, publishing the crate felt less like a separate project and more like cleaning up the instrument I’d used all weekend.

This post is incomplete on purpose

Coordinated disclosure means I’m leaving out:

  • affected endpoints,
  • payloads,
  • step-by-step reproduction,
  • vulnerable service names,
  • screenshots of sensitive states,
  • code that demonstrates impact,
  • hints that turn this into a scavenger hunt.

The reports are private and triage is ongoing. The vendor needs time to reproduce, assess, fix and coordinate. Until then, the useful public story is the process, the tooling, and the constraints.

I like technical write-ups. I like full disclosures when the time is right (unlike copyfail & dirtyfrag :^)). I like it when a report eventually becomes a clean postmortem with enough detail to teach something real.

So, why the fuck did I write a Rust crate?

Because curl got me curious, Python got me moving, and then both got too annoying for the research I wanted to do. Freebox OS has a big enough API that the boring parts had to be solid before the weird parts could show up, and I wanted to learn Rust on something real.

Oh, and because after one weekend, two private reports, one still-unreported rabbit hole and one decade-old easter egg, the crate stopped feeling like side tooling and started feeling like the only sane way to keep going!

hash: dad
EOF