Notes API

  • 백엔드 API 들을 테스트 할 수 있는 웹 서비스를 제공해주고 있다.
  • 웹 서비스로부터 취약점을 찾아 익스플로잇 하고 플래그를 획득해라!

소스코드

index.html

  • index.html 페이지에서는 /api 경로로 POST 요청을 보낼 수 있는 폼 태그가 있다.
  • 폼의 이름(name)은 “api_info”이고, 값(value)은 기본 값으로 아래 값이 설정돼 있다.
    • {"path": "/notes", "method": "POST", "body": {"title": "my note", "content": "my fancy contents :)"}}

server.js

  • server.js 파일에서는 아래 3가지 POST 요청에 대한 정보를 확인할 수 있었다.
    • /auth 에서는 요청 바디에 입력받은 값이 관리자 패스워드와 일치하면 세션(session) 값을 “true”로 설정한다.
    • /api 에서는 요청 바디에 입력받은 api_info 값을 setAPI(...) 함수의 인자로 넘기고 있다.

  • setAPI(...) 함수는 전달받은 인자를 다시 update(...) 함수의 인자로 넘긴다.
  • update(...) 함수는 전달 받은 값을 이용해서 어떤 값을 업데이트 하는 것으로 보인다.

  • 위 동작 과정을 정리해보면 다음과 같다.
  • (1) 폼 태그로 /api 경로로 요청을 보낸다.
{
      "path": "/notes", 
      "method": "POST", 
      "body": {"title": "my note", "content": "my fancy contents :)"}
}
  • (2) /api 경로로 요청을 보내면 server.js 파일의 아래 부분이 실행된다.
    • 전달받은 json 값은 req.body.api_info 값으로 사용되어 userApiInfo 변수에 저장된다.
    • 이후 setAPI(apiInfo, userApiInfo); 호출된다.
app.post('/api', async (req, res, next)
 
[ . . . ] 
 
// req.body.api_info → {"path": "/notes", "method": "POST", "body": {"title": "my note", "content": "my fancy contents :)"}}
const userApiInfo = req.body.api_info;
setAPI(apiInfo, userApiInfo);
  • (3) setAPI 함수 내부에서 update 함수가 호출된다.
    • 이 때, 인자로 사용되는 값은 아래와 같다.
function setAPI(apiInfo, userApiInfo) {
    update(apiInfo, JSON.parse(userApiInfo));
}
 
[ . . . ]
 
  update("{'host': 'backend:8000'}", JSON.parse('{"path": "/notes", "method": "POST", "body": {"title": "my note", "content": "my fancy contents :)"}}'));
  • (4) update 함수에서는 아래와 같은 과정으로 apiInfo 값이 업데이트 된다.
    • {‘host’: ‘backend:8000’, ‘title’: ‘my note’}
    • {‘host’: ‘backend:8000’, ‘title’: ‘my note’, ‘content’:‘my fancy contents :)’}
// dest : {'host': 'backend:8000'}
// src : JSON.parse('{"path": "/notes", "method": "POST", "body": {"title": "my note", "content": "my fancy contents :)"}}')
function update(dest, src) {
    for (var key in src) {
        if (typeof src[key] !== 'object') {
        // `key` 값은 path, method, body
            dest[key] = src[key];
        } else {
            if (typeof dest[key] !== 'object') {
                dest[key] = {};
            }
            update(dest[key], src[key]);
        }
    }
}
  • (5) app.post('/api', async (req, res, next) 호출되면 callAPI 함수가 비동기로 실행된다.
    • callAPI 함수의 상세 동작 과정은 아래와 같다.
  • 전달받은 “isAdmin” 인자 값이 참(true) 이면, 요청 헤더(‘is-admin’) 값을 ‘true’ 로 설정하고,
  • 설정된 변수(baseUrl, path, method) 값을 이용하여 GET/POST 요청을 보내 응답을 반환 받는다.

  • 여기서 req.session['isAdmin'] 값이 참(true)이기 위해서는 /auth 경로 접근 시 요청 바디에 함께 입력된 값이 실제 관리자 계정의 패스워드(adminPassword)와 일치해야만 한다.

  • 음.. req.session['isAdmin'] 값이 참(true)이 되야만 “get_admin(..)” 함수를 호출해서 플래그를 얻을 수 있다.
  • 자, 그럼 어떻게이걸 가능하게 할 수 있을까? (관리자 패스워드 브루트포스는 불가능하다.)
    • 바로 자바스크립트의 프로토타입 객체를 사용하면 그게 가능해진다.

javascript - prototype object

  • 자바스크립트에서는 함수를 생성하면 함수만 생성되는 것이 아니라 Prototype Object 도 같이 생성되고,
    • 생성된 함수는 프로토타입(prototype) 속성을 통해 Prototype Object 로의 접근이 가능하다.
    • 프로토타입 오브젝트는 기본적으로 cunstructor, __proto__ 속성을 갖는다.

  • 여기서 생성자는 자바 생성자와 유사하게 해당 함수에 대한 인스턴스 생성 시 사용되고,
  • 프로토타입은 자바에서의 상속 개념처럼, this 와 유사하게 동작한다.
  • 예를 들면 아래와 같다.
    • instance_obj_001, instance_obj_002는 a 값을 설정하지 않았음에도 1 이라는 값이 출력되는데,
    • 이는 자바스크립트에서 특정 객체/함수에 대한 인스턴스를 생성했을 때 인스턴스.{임의 변수명}이 실제로 존재하지 않으면 상위 객체로 한 단계씩 올라가면서 해당 변수에 값이 존재하는지 찾는 특징을 갖기 때문이다.

Tips

prototype​ 속성은 오로지 ‘함수’ 만 가지고 있던 것과는 달리 (Person.prototype),
__proto__속성은 모든 객체가 빠짐없이 가지고 있는 속성입니다.
__proto__는 객체가 생성될 때 조상이었던 함수의 Prototype Object를 가리킵니다.

  • 다른 방법으로 다시 확인해보자.
  • test123 이라는 객체를 생성하고 test.__proto__ 값과 Object.prototype 을 비교하면 참 값이 나온다.
    • var test123 = {} 을 다르게 표현하면, var test123 = new Object()에 해당되는데,
    • __proto__는 객체가 생성될 때 조상이었던 함수의 Prototype Object를 가리킨다고 한다.
      • (1) test123 은 Object 객체의 인스턴스이므로, test123.__proto__ Object.prototype 과 같은 의미를 갖는다.

  • 다시 server.js 파일로 돌아와 코드를 분석해보면 아래의 순서로 동작한다.
    • (1) POST /api > setAPI(apiInfo, userApiInfo) 호출
    • (2) update(apiInfo, JSON.parse(userApiInfo)) 호출 (apiInfo 변수에 저장된 값이 수정된다.)
    • (3) callAPI(apiInfo, req.session[‘isAdmin’]);
      • isAdmin 값이 참(true)이면, "is-admin": "true" 값을 반환한다.
      • “is-admin”: “true” 값은, /admin 경로로 GET 요청을 보내 플래그를 얻을 때 필요하다.
    • *update 함수 내부에서는 자기 자신을 재귀적으로 호출하며 apiInfo 변수 값을 수정하고 있다.
      • 수정 과정에서 특별히 입력 값 검증 로직은 없었다.

EXPLOIT

  • 자바스크립트에서 __proto__ 는 객체가 생성될 때 자기 부모 객체의 Prototype Object를 가리킨다.
  • 아래 2개 객체 모두, 부모를 따라가면 결국 최상단 Object 객체의 프로토타입 객체를 가리키고 있을 것이다.
    • apiInfo = {‘host’: ‘backend:8000’}
    • userApiInfo = {“path”: “/notes”, “method”: “POST”, “proto”: {“title”: “my note”, “content”: “my fancy contents :)”}}
  • req.session['isAdmin'] 값도 마찬가지다. 결국 이것도 올라가다 보면 Object 객체의 프로토타입 객체를 가리키고 있다.

  • 여기서 req.session['isAdmin'] 값은 관리자 계정으로 로그인에 성공해야 최초 발급된다.
  • 그런데, 로그인을 한 적이 없기 때문에 req.session['isAdmin'] 값은 조회되지 않는다.
    • (애초에 req.session 객체에 “isAdmin” 이라는 속성이 존재하지 않을 것이다.)
  • 자기 자신에서 값이 조회되지 않으면 위로 올라가면서 값을 확인해보다가 결국에는 최상단에 있는 오브젝트(Object) 객체의 Object.prototype.isAdmin 값까지 조회했을 거다. (여기서도 값이 없으면 “undefined” 결과를 반환한다.)

  • 테스트 시에는 “Object.prototype.isAdmin” 값이 “undefined” 였기 때문에 /admin 경로로 GET 요청을 보내면 null 값이 반환됐는데, 만약 여기서 Object.prototype.isAdmin 값을 공격자가 임의로 변조할 수 있다면 headers 값으로 "is-admin": "true"를 반환 받을 수 있을 것이다. (그럼 /admin 경로로 요청을 보내서 플래그도 확인해 볼 수 있다.)

  • 그럼 여기서 Object.prototype.isAdmin 값을 어떻게 공격자가 변조해야 할까!?
  • 에 대한 정답이 바로 Prototype Pollution 공격이다.
  • 상속(?) 받은 부모의 프로토타입 객체 속성을 변조해서 인증/인가 로직을 우회하는게 핵심 포인트다.

  • /api 경로로 POST 요청을 보낼 때, 아래와 같은 값을 보냈다고 가정해보자.
apiInfo = {'host': 'backend:8000'}
userApiInfo = {"path": "/notes", "method": "POST", "__proto__": {"title": "my note", "content": "my fancy contents :)"}}

  • 키(key) 값이 "__proto__" 일 때, userApiInfo[key], apiInfo[key] 값은 오브젝트 형식이므로 30 라인이 실행된다.
  • 여기서 apiInfo[key] = {} 코드는, apiInfo[key] = new Object() 와 동일한 기능을 수행한다.
    • 따라서, apiInfo.__proto__.{임의의 속성명} = {임의 값}Object.prototype.{임의의 속석명}={임의 값} 임을 의미한다.

  • 그럼 이제, 공격을 수행해보자.
    • POST /api, (HTTP Request Body) api_info={"path": "/admin", "method": "GET", "__proto__": {"isAdmin":"true"}}
  • Wks, 플래그가 튀어나온다. 휴..

🐛.. 🐛.. 🐛..

REFERENCE

  1. https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-Prototype-%EC%99%84%EC%A0%84-%EC%A0%95%EB%B3%B5-%E2%9D%97
  2. https://leapcell.io/blog/ko/node-js-webs-app-eul-prototype-ohm-oem-request-milsu-gonggyeok-eulo-bueong-hagi