https://alpacahack.com/daily/challenges/vending-machine-revised-version

コード (一部抜粋)

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 buy(self, mark:str):
        if 'abcde'.find(mark) < 0: # No 'f'? Hmm...
            print("Invalid choice.")
            return
        loc = self.stock.find(mark)
        #...
        stock_list = list(self.stock)
        item = stock_list.pop(loc)
        self.stock = ''.join(stock_list)
        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()
        # ...
        vm.buy(mark)

if __name__ == '__main__':
    main()

:thinking:

itemf が入ればよい。ところでこの変数は

if 'abcde'.find(mark) < 0: # No 'f'? Hmm...
    print("Invalid choice.")
    return
# ...
loc = self.stock.find(mark)
# ...
item = stock_list.pop(loc)

の通り代入されているので、ここを見ればよく

ことが期待される。というわけで、この条件の満たし方を考えてみる。

はじめに str.find(mark)mark が空文字列だと 0 を返す仕様である(後述するが、mark の0文字目は部分文字列に空文字列をもつため)。 self.stock.find(mark) も同様に、mark が空文字列だと 0 を返す仕様であるので、結局 loc = 0 なので先頭が pop() の対象になるから、 stock_list の先頭が item になる。

このことから、

のいずれの方法でも Flag を入手することができる。 入力の方法には pwntools を使うなりなんでもよいので、今回は pwntools を使った。

solve

import sys
from pathlib import Path

from pwn import args, process, remote

if not args.FILE and not (args.IP or args.PORT):
    print("You must specify either (IP and PORT) or FILE")
    exit(1)

IP = args.IP or "127.0.0.1"
PORT = int(args.PORT or 1337)
FILE = Path(args.FILE) if args.FILE else None

STOCK = "a" * 30 + "b" * 60 + "c" * 20 + "d" * 50 + "e" * 40

def connect():
    file = FILE
    if file and not file.is_absolute():
        file = Path(__file__).parent / file
    return process([sys.executable, "-u", str(file)]) if file else remote(IP, PORT)

def solve() -> None:
    io = connect()
    try:
        print(io.recvuntil(b"your choice> ").decode(), end="")

        for mark in STOCK:
#           io.sendline(b"") # 空文字列でもいい
            io.sendline(mark.encode()) # あるいは在庫を律儀に1つずつ入力してもいい
            print(io.recvuntil(b"your choice> ").decode(), end="")

        io.sendline(b"")
        print(io.recvall(timeout=2).decode(), end="")
    finally:
        io.close()

if __name__ == "__main__":
    solve()

余談: find() が空文字列を引数にしたとき 0 を返す理由

grep.app で cpython/cpython の実装を見てみると、この挙動はドキュメントが保証しているものではなく実装由来だとわかる。