iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🦙

Writeup for Vending Machine (AlpacaHack)

に公開

Problem

This is a challenge called "Daily AlpacaHack 2026/05/08 Vending Machine".

What I tried

I checked the distributed files.

server.py
import os

FLAG = os.getenv("FLAG", "Alpaca{dummy}")

class VendingMachine:
    def __init__(self):
        self.stock = 'a'*30 + 'b'*60 + 'c'*20 + 'd'*50 + 'e'*40 + 'f' # 'aaa...eeef'
        self.item_names = {
            'a': 'apple juice',
            'b': 'banana juice',
            'c': 'coke',
            'd': 'draft beer',
            'e': 'energy drink',
            'f': 'flag'
        }

    def print_menu(self):
        print("Please select an item.")
        print("-" * 16)
        for mark, name in self.item_names.items():
            print(f"{mark}: {name}")
        print("x: exit")
        print("-" * 16)

    def buy(self, mark:str):
        # check choice
        if mark not in ['a', 'b', 'c', 'd', 'e']: # No 'f'? Hmm...
            print("Invalid choice.")
            return
        # check stock
        if len(self.stock) <= 0:
            print("All sold out.")
            return
        # find the location of the product
        loc = self.stock.find(mark)
        # take the product from stock
        stock_list = list(self.stock)
        item = stock_list.pop(loc)
        self.stock = ''.join(stock_list)
        # dispense the product
        name = self.item_names[item]
        print(f"You bought {name}.")
        if item == 'f':
            print(f"Flag:", FLAG)
        else:
            print("Thank you!")

def main():
    vm = VendingMachine()
    vm.print_menu()
    while True:
        mark = input("your choice> ").lower()
        if mark == "x":
            print("Bye.")
            break
        vm.buy(mark)

if __name__ == '__main__':
    main()
class VendingMachine:
    def __init__(self):
        self.stock = 'a'*30 + 'b'*60 + 'c'*20 + 'd'*50 + 'e'*40 + 'f' 
        self.item_names = { ... }

This is the configuration executed when the VendingMachine class is initialized. Products are managed as one long string. There are 30 'a's, 60 'b's, and so on, with a single 'f' placed at the very end. self.item_names is a dictionary mapping 'a' through 'f' to their actual product names.

    def print_menu(self):
        # ...omitted...
        for mark, name in self.item_names.items():
            print(f"{mark}: {name}")
        # ...omitted...

This is a simple function that just prints the product list to the screen. The variable mark receives 'a' through 'f', and name receives the corresponding name. In other words, it displays the aforementioned mapping table, including f: flag.

    def buy(self, mark:str):
        # check choice
        if mark not in ['a', 'b', 'c', 'd', 'e']: # No 'f'? Hmm...
            print("Invalid choice.")
            return

This is the buy function that handles product purchases. There is a vulnerability here. It checks if the user's input is 'a', 'b', 'c', 'd', or 'e'. Because of this, a user cannot buy the FLAG simply by choosing 'f'.

        # check stock
        if len(self.stock) <= 0:
            print("All sold out.")
            return

If the stock string is empty, it treats it as sold out and terminates the process.

        # find the location of the product
        loc = self.stock.find(mark)
        # take the product from stock
        stock_list = list(self.stock)
        item = stock_list.pop(loc)
        self.stock = ''.join(stock_list)

loc = self.stock.find(mark) searches for how many characters from the left the specified character (mark) is located within the string (stock). find() is used to return the index where the specified substring first appears. It has a feature where it returns -1 instead of throwing an error if the character is not found.
Since Python strings cannot be modified directly, it first converts the string into a list using list() to make it a mutable list type. Once it is a list, we can use pop(). stock_list.pop(loc) removes and returns the element at the specified position from the list. If the product is sold out and loc has become -1, pop(-1) in a Python list acts to remove the last element of the list. The last character of the stock string is 'f'!
Finally, it performs a process to join the list, which has been broken apart after one character was removed, back into a single string.
['a', 'b', 'd', 'e', 'f'] ----> "abdef"

        # dispense the product
        name = self.item_names[item]
        print(f"You bought {name}.")
        if item == 'f':
            print(f"Flag:", FLAG)
        else:
            print("Thank you!")

It prints the retrieved character (item) to the screen.

def main():
    vm = VendingMachine()
    vm.print_menu()
    while True:
        mark = input("your choice> ").lower()
        if mark == "x":
            print("Bye.")
            break
        vm.buy(mark)

It starts the vending machine and accepts purchase inputs until the user enters 'x' to exit. vm = VendingMachine() executes the __init__ method. All inputs are converted to lowercase.

Now, let's go find the FLAG. First, the character entered must absolutely be either 'a', 'b', 'c', 'd', or 'e'. What happens if we erase one of these characters from the stock? This time, let's take 'c', which has the smallest count, as an example. We enter 'c' 20 times, and then enter it a 21st time. Of course, since 'c' is included in 'a' through 'e', the input check is passed. Next, for the stock check, it passes because it looks at the total stock rather than 'c' individually. Afterward, processing is passed to find(c), and since 'c' does not exist in the stock, -1 is returned. Now, loc contains -1. In that state, processing is passed to pop(-1). As explained in the code, this acts to remove the very last element of the list. Since the FLAG is at the end of the stock, we end up taking the FLAG. After the FLAG is gone, characters like 'e', 'd', ... will be taken in order.

FLAG

Alpaca{?myst3ry-z0ne?}

Others

If this were C or Java, the program would terminate with an error, stating that index -1 does not exist! However, Python lists are built flexibly, and specifying a negative number for an index has the special behavior of counting from the end.
In other words, pop(-1) turned into an instruction to take the last element of the list!

  • Difference between functions and methods
    While writing this article, I wasn't entirely sure about the difference, so I summarized it.
    A function exists independently and does not belong to anything.
    A method is created within a class and belongs to that class or an instance created from it.
    In Python code, it seems easy to distinguish them by checking if the first argument is self in the process defined by def; if it is, it's a method; otherwise, it's a function.

  • Correct input check

    if mark not in self.stock:
        print(f"{mark} is sold out.")
        return
    

    The biggest cause of this bug was confusing "Is the vending machine empty?" with "Is cola (c) sold out?". Therefore, as shown above, it should have been implemented to reject the request if the requested product (mark) is not present in the stock (self.stock).

Discussion