iTranslated by AI
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 isselfin the process defined bydef; 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.") returnThe 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