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); 호출된다.
- 전달받은 json 값은
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 이라는 값이 출력되는데, - 이는 자바스크립트에서 특정 객체/함수에 대한 인스턴스를 생성했을 때
인스턴스.{임의 변수명}이 실제로 존재하지 않으면 상위 객체로 한 단계씩 올라가면서 해당 변수에 값이 존재하는지 찾는 특징을 갖기 때문이다.

- instance_obj_001, instance_obj_002는
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과 같은 의미를 갖는다.

- (1) test123 은 Object 객체의 인스턴스이므로,
- 다시
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 요청을 보내 플래그를 얻을 때 필요하다.
- isAdmin 값이 참(true)이면,
- *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, 플래그가 튀어나온다. 휴..

🐛.. 🐛.. 🐛..