Description

Spurdo decided to develop a web game while he was solo leveling. But due to lack of skill, spurdo developed a game that cannot be completed by anyone sane enough. And because of that he decided to invite some beta players(you) for his game.

Good luck.

https://spurdo-ebinling.1337.sb

author: es3n1n, cpp.dog

web_spurdo-leveling.7z

Approach

Understanding the Challenge

The challenge involves using a protocol buffer to interact with a web socket across two services: backend and frontend. The goal is to unlock all achievements. If successful, the application will automatically present the flag.

Here is the list of achievements that we need to obtain.

available_achievements = [
    Achievement(name='kill 1 mob', condition=lambda x: x.total_mobs_killed >= 1),
    Achievement(name='kill 5 mobs', condition=lambda x: x.total_mobs_killed >= 5),
    Achievement(name='kill 100 mobs', condition=lambda x: x.total_mobs_killed >= 100),

    Achievement(name='unlock level 2', condition=lambda x: x.level >= 2),
    Achievement(name='unlock level 5', condition=lambda x: x.level >= 5),
    Achievement(name='unlock level 75', condition=lambda x: x.level >= 75),

    Achievement(name='survive 5 waves', condition=lambda x: x.wave_number >= 5),
    Achievement(name='survive 50 waves', condition=lambda x: x.wave_number >= 50),
    Achievement(name='survive 150 waves', condition=lambda x: x.wave_number >= 150),

    Achievement(name='unlock all items', condition=lambda x: len(x.inventory) >= ITEMS_COUNT),

    Achievement(name='cheater!', condition=lambda x: x.is_cheater),

    # is this enough? if not, please let me know, and we'll make a pt.2 of this challenge :^)
]

Here is the trigger condition.

    if len(session.game.unlocked_achievements) == len(available_achievements):
        session.inject_packet(SAchievementUnlocked(
            name=config.FLAG
        ))

How to Obtain All Achievements

Survive Waves & Kill Mobs

To achieve these two goals, we need to bypass these 2 restrictions.

@handler.dispatcher()
async def on_wave(session: Session, message: Wave) -> Wave:
    cleared_in: float = .0
    min_clearance_time: float = .0

    if session.game.wave_number > 0:
        if message.number > len(session.game.waves):
            return message

        wave_info = session.game.waves[message.number - 1]

        if len(wave_info.mobs.enemies) != len(message.mobs):
            session.log_critical('Attempted to send invalid wave end message. len(enemies) != mobs_killed')

        if wave_info.mobs.contains_invincible_enemies:
            session.log_critical('Attempted to clear wave with an invincible mob!')

        cleared_in = message.current_time - wave_info.started_at_user_time
        min_clearance_time = (min_wave_clearance_time(
            sum([x.health for x in wave_info.mobs.enemies]), session.game.level
        ) * 1e3)
        min_clearance_time -= WAVE_REMOTE_CLEARANCE_WINDOW

        if cleared_in < min_clearance_time:
            session.log_critical(f'Cleared wave too quickly ({cleared_in} vs {min_clearance_time} diff '
                                 f'{min_clearance_time - cleared_in})')

        cleared_in_local: float = time() - wave_info.started_at_local_time
        cleared_in_local -= WAVE_CLEARANCE_WINDOW
        cleared_in_local *= 1e3
        if cleared_in_local > cleared_in:
            session.log_critical(f'Cleared wave too quickly #2 ({cleared_in_local} vs {cleared_in})')

        session.game.total_mobs_killed += len(wave_info.mobs.enemies)

    if session.game.wave_number > 0:
        session.log_info(f'Cleared wave in {cleared_in:.0f} (best {min_clearance_time:.0f} diff '
                         f'{(cleared_in - min_clearance_time):.0f}). Starting a new one.')

    for i, achievement in enumerate(available_achievements):
        if i in session.game.unlocked_achievements:
            continue

        if achievement.condition(session.game) is False:
            continue

        session.game.unlock_achievement(session, i, achievement.name)

    if len(session.game.unlocked_achievements) == len(available_achievements):
        session.inject_packet(SAchievementUnlocked(
            name=config.FLAG
        ))

    session.game.wave_number += 1
    new_wave = LocalWave.new(message.current_time, session.game.wave_number)
    session.game.waves.append(new_wave)

    session.game.xp_count += new_wave.mobs.total_xp

    return Wave(
        number=session.game.wave_number,
        mobs=[
            Wave.Mob(
                health=mob.health,
                damage=mob.damage,
                invincible=mob.invincible,
                xp=mob.xp,
                drop=ItemDetail(
                    name=mob.dropped_item.name,
                    damage=mob.dropped_item.damage,
                    icon=mob.dropped_item.icon,
                ) if mob.dropped_item is not None else None
            )
            for mob in new_wave.mobs.enemies
        ],
        current_time=message.current_time,
    )

Here are the 2 restrictions:

  1. if wave_info.mobs.contains_invincible_enemies: We can bypass this by exploiting the fact that the application doesn't check the current wave. Thus, we can send the results from the first wave to advance to the next wave.
  2. If the conditions if cleared_in < min_clearance_time: and if cleared_in_local > cleared_in: are only checking if cleared_in is less than min_clearance_time and cleared_in_local, we can bypass these checks by making cleared_in greater than these two variables.