Difficulty
medium
Categories
misc
Description

Got a leaky drain? Give us a call!

Connect using telnet $HOST $PORT flag is in $FLAG

Author
Popax21
Attachments
plumberhub.tar.gz
Service
Challenge has a remote instance.

Overview

The challenge consists of multiple rust files:

CCsaarrrcggoocmppw..oalroltniuorootnmjkcmr.be.klaricrcsntstg.o.rrrs.srs

If we run the program with cargo run, we get dropped into an interactive REPL:

Welcome to PlumberHub!
Enter 'help' for available commands, or 'exit' to quit.

plumberhub〉help
Usage: <COMMAND>

Commands:
  replace-installation     Replace a faulty plumbing installation with a new equivalent
  clear-obstruction        Clears an obstruction from the given plumbing installation
  hire-contractor          Hire a new contractor with the given amount of experience
  delegate-work            Delegate the following N work items to an available contract within the given range of experience
  wait-for-delegated-work  Wait for previously delegated work items to be completed
  status                   Print the currently queued work items
  run                      Simulate a new project with the currently queued work items
  clear                    Clear the currently queued work items
  exit                     Exit PlumberHub
  help                     Print this message or the help of the given subcommand(s)

Options:
  -h, --help  Print help

Goals

There’s an interesting part in in src/contractor.rs:

    async fn clear_obstruction(
        &mut self,
        installation: &plumbing::Installation,
        summary: &mut WorkSummary,
    ) {
        match installation.kind {
            plumbing::InstallationKind::EldritchPresence => {
                if self.years_experience >= AGE_OF_THE_UNIVERSE {
                    smol::Timer::after(Duration::from_secs_f64(installation.task_complexity()))
                        .await;

                    summary.push("i have unblocked your destined path, my child");
                    summary.push("go on, take this, and be free from hence on");
                    summary.push(std::env::var(&installation.name).unwrap_or_default());
                } else {
                    // how dare you, puny mortal...
                    panic!("helph̷e̵l̴p̴h̸̤́é̶̗̯l̸̟͘p̴͖̩͆h̸̟̃̓é̴̢̗̞̹͚͚̓̃̋̔̉̆̍̀́̌͜͝͝l̴̝̼͛̿͑͂p̷̮̠̣̯̣̲̮͖̩̠͓̙̞͙̀͑̅̌͛́̉͛͑͘ĥ̸̗̱͔̯͊̈́͛͊̈́̈̂ę̶̪̥͖̥̜̙͂͂̇͐͋͛̇͝͝ͅͅl̴̛͓̟̱̲̙͓̮̞̪̞̣͖͂̑̌̅̐̇̇̋͗̐̽̀̃͘͘͝p̵̲̯̬͉̮̣̣͇̗͈̦̗͈̺͈̣̱̙̦̳̖͙̦̥̬̮̭̫̺̌̿̀̉͜͜͠h̷̡̢̟͔̖̫̝͍̳͈̪͍̘̜̩͚̗̮́̌͛̔̂̇͛̀̍͛̃̑͗̃̚̚ȩ̷̢̨̼̝͈̠̪͔̻͖̖̮͇͔̳͚̰͓̹̣̭͈̥̩̯̮̳͖̘̙̭̹̖̾̃̈̄͛̈́͑̇̂̾̄͂̂͂̌̎̄̈͐͆̋̓̃́́̍́̀̃̂͗̍̈̆̎̕͘̕͘͜͝ͅl̴̢̧͈̻̩̼͔̲̥̯̭̥͈͎̤̹͚̣͙̹̟͙̯͍̤͇̪͔̩̝̲̲̮̘͇͈͓̲̼̤̠̱̬̅̈͆̽̐̂̂̏͛͌̿͜͠ͅp̶̡̧̢̧̳͕̞̻̱͓͎̩̠̫͇͚͇̹̝̖͍͉̰̟͉̪̹̖̖͖͇̜̮͈̰̩͕̫͓̥̥̜̙̮̼͓͎͔̗͛͋͋͆͌̈́̚ͅ");
                }
            }
            _ => {
                let complexity = installation.task_complexity();
                smol::Timer::after(Duration::from_secs_f64(
                    complexity / (self.years_experience as f64).sqrt(),
                ))
                .await;

                let cost = 30 + complexity as u64 * 15;
                self.bill.increase_by(cost);

                summary.push(format!("cleared out {installation} for {cost}$"));
            }
        }
    }

plumbing::InstallationKind::EldritchPresence is defined in plumbing.rs and is part of the InstallationKind enum:

#[strum(serialize_all = "kebab_case")]
pub enum InstallationKind {
    Sink,
    Drain,
    Faucet,
    Shower,
    Pipework,
    EldritchPresence,
}

We can see that it’s serialized as kebab_case, so calling clear-obstruction eldritch-presence FLAG should give us the flag, as long as we can fulfill the other conditions.

self.years_experience is set during hire-contractor:

impl Contractor {
    pub async fn hire(
        project: &Arc<Project>,
        years_experience: NonZero<usize>,
        summary: &mut WorkSummary,
    ) -> Arc<Self> {
        let years_experience: usize = years_experience.into();

        // skilled labor isn't easy to find, nor is it cheap!
        let scarcity = 3.0 + 0.8 * (years_experience as f64).ln_1p();
        let cost = 100 + years_experience as u64 * 80;

        smol::Timer::after(Duration::from_secs_f64(scarcity)).await;
        ...

Sadly this also means we cannot simply afford it with our initial balance. Attempting to get a worker with the experience of $AGE_OF_THE_UNIVERSE+1 leads to this:

plumberhub〉hire-contractor 13790000001
plumberhub〉delegate-work test 1 13790000001
plumberhub〉clear-obstruction eldritch-presence FLAG
plumberhub〉wait-for-delegated-work test
plumberhub〉run
simulating new project with 4 work item[s]...
project was not able to be completed successfully D:

work summary:
 - worker died of a stroke: ohnoohnoohnonono i can't pay the bill... (ᗒᗣᗕ)՞

Cheating

The total cost is years_experience multiplied by 80, added to 100. We can try (roughly) dividing the max unsigned 64-bit integer limit by 80 and using that as the experience instead:

plumberhub〉hire-contractor 230584300921369395
plumberhub〉delegate-work 1 1 230584300921369395
plumberhub〉clear-obstruction eldritch-presence FLAG
plumberhub〉wait-for-delegated-work 1
plumberhub〉run
simulating new project with 4 work item[s]...

                           user [15000$]:   had a stroke x_x

project was not able to be completed successfully D:

work summary:
 - worker died of a stroke: attempt to add with overflow

We get an error about an overflow :(

However, it turns out that rust does not do integer overflow checks in release builds!

Luckily, this happens to be the case for the instance, and running the same code against the remote gives us the flag:


Flag:

dach2026{help_i_leaked_the_drain_come_asap_fghkjjsdfentrkhksd_b0e55ca48109}