[보안/write-up] Dark Runes

Shadis·2026년 3월 4일

보안/write-up

목록 보기
2/4

About This Wargame

문제 분석

  • 웹 서버: node.js
  • WAS: node.js
  • Framework: express(JS)
  • DB: sqlite

function

  • 회원가입, 로그인 기능 탑재
  • 로그인하면 document 작성 가능
  • 내 계정으로 작성한 document 목록 확인 가능

endpoint

  • /register
    username과 password의 hash값을 sqlite에 저장

  • /login
    유저가 post 요청으로 넘겨준 username을 이용하여 DB의 저장된 user 정보 가져옴
    DB의 password의 hash값과 유저가 post요청으로 넘겨준 password의 hash값을 비교해서 맞을 시 쿠키 생성

  • /documents (POST)
    content를 sanitize-html 라이브러리를 이용해서 보안 위험성이 있는 HTML 태그를 제거한 이후 user.id와 함께 DB에 저장.

  • /document/export/{documentId}
    documentId에 해당하는 document의 내용을 node-html-markdown 라이브러리를 사용해서 HTML에서 markdown으로 변환한 이후 markdown-pdf 라이브러리를 이용해서 pdf로 출력해줌.

middleware

  • isAuthenticated
    cookie에 저장된 stringifiedUser값을 가져와 서버의 SECRET과 조합하여 cookie에 저장된 sig와 일치하는지 확인하여 cookie가 조작되었는지 확인. 일치하지 않을 시 다음 엔드포인트로 넘어가지 못하게 함.

  • isAdmin
    /document/export/... endpoint에 접근할 때 실행. isAuthenticated로 cookie 조작 확인 이후 실행. username이 admind인지 확인, id는 확인 x

utils

  • generateCookie
    stringifiedUser: usernameid를 JSON 형태로 만들어준 다음 문자열로 변환한 값
    sig: 랜덤값 값 SECRET을 stringifiedUser에 더해 hash한 값
    {stringifiedUser}-{sig} 구조의 cookie를 만들어주는 함수

flag 분석

isAdmin 우회

const isAdmin = (req, res, next) => {
  if (req.user.username === "admin") {
    return next();
  }

  return res.status(403).send("Forbidden");
};

isAdmin은 isAuthenticated 이후 실행되는 코드로 단순히 username이 admin인지 아닌지를 기준으로 확인하고 있다.

router.post('/register', (req, res) => {
	...
    const existingUser = findUser(username);
    if (existingUser) {
        return res.status(401).render("register", {
            error: "Username already exists"
        })
    }
	...
});

register 코드를 확인해보면 username이 존재하는지 확인해서 username이 존재할 시 회원가입을 거부한다. 그래서 당연히 admin 계정이 이미 DB에 존재해서 admin으로 회원가입을 불가능하게 막아놓았을 줄 알았는데 admin 회원가입이 가능하다. 그래서 단순히 admin으로 계정을 만들면 isAdmin을 우회할 수 있다.

isAdmin 우회 2

내가 생각했을 때에는 admin 회원가입을 이용해서 isAdmin 로직을 우회하는 것은 이 wargame 문제 출제자의 의도가 아닌 단순 실수이고 정말 출제자가 의도한 isAdmin 우회 방법은 이 방법이라고 생각한다.

login에 성공하면 제공하는 cookie값을 생성하는 generateCookie() 함수를 살펴보면 signString() 함수를 사용해서 sig 값을 생성하는 것을 확인할 수 있다.

const generateCookie = (username, id) => {
  const stringifiedUser = btoa(JSON.stringify({ username, id }));
  const sig = signString(stringifiedUser);
  return `${stringifiedUser}-${sig}`;
};

그리고 document를 생성하는 코드를 살펴보면 여기에서도 동일한 signString() 함수를 사용해서 integrity 값을 생성하는 것을 확인할 수 있다.

router.post("/documents", isAuthenticated, (req, res) => {
  ...
  const integrity = signString(content);
  addDocument(user.id, sanitizedContent, integrity);
  return res.redirect(`/documents`);
});

따라서 documents의 content에 admin의 stringifiedUser값을 넣으면 document의 integrity 값이 admin의 sig 값이 될 것이다.

JS console을 통해 admin의 stringifiedUser값을 찾아보자.

var username = "admin"
var id = 1234
btoa(JSON.stringify({ username, id }))

-> 'eyJ1c2VybmFtZSI6ImFkbWluIiwiaWQiOjEyMzR9'

document의 content에 이 값을 쓰고 등록하면

admin의 sig값을 찾을 수 있다.

{stringifiedUser}-{sig} 구조로 cookie값을 설정하고 admin만 들어갈 수 있는 페이지(/document/export/{id})에 들어가서 isAdmin을 우회하는데 성공했는지 확인해보면 isAdmin 우회를 성공한 것을 확인할 수 있다.

sig를 생성하는 함수가 숨겨져있어야 하는데 document의 integrity를 생성하는데 사용됨으로써 노출되었기 때문에 isAdmin 우회가 가능하였다.

markdown-pdf 취약점 공략 (CVE-2023-0835)

wargame을 풀 때 반드시 해야하는 작업 중 하나가 application에서 사용하는 라이브러리 중 CVE가 공개된 라이브러리가 있는지 확인하는 것이다. 그리고 요즘에는 AI를 활용하여 라이브러리의 CVE를 손쉽게 찾을 수 있다.

AI의 도움으로 CVE를 찾아보니 markdown-pdf에 server side XSS 취약점이 존재하는 것을 확인할 수 있었다. isAdmin을 우회하면 접근할 수 있는 /document/export 엔드포인트에 접근하면 document content를 markdown 포맷으로 변환한 이후 markdown을 pdf로 변환시켜준다. pdf로 변환하기 위해 내장 브라우저를 띄우고 내장 브라우저에 markdown 내용을 HTML 코드로 나타내는 과정에서 <script>와 같은 태그를 필터링하지 않아 server side XSS가 실행될 수 있는 취약점이다.

그렇다고 바로 document에 <script> document.write("test") </script>를 심고 server side XSS를 유도하더라도 server side XSS가 실행되지 않는 것을 확인할 수 있는데 이는 document를 등록할 때 sanitize-html 라이브러리를 활용해서 document content 중 <script>와 같은 위험한 코드를 필터링하기 때문이다.

router.post("/documents", isAuthenticated, (req, res) => {
  const { content } = req.body;
  ...
  // 필터링 수행
  const sanitizedContent = sanitizeHtml(content, {
    allowedAttributes: {
      ...sanitizeHtml.defaults.allowedAttributes,
      a: ["style"],
    },
  });
  ...
});

하지만 HTML entity를 사용하면 sanitize-html 라이브러리의 필터링을 우회할 수 있다. HTML entity는 단순히 HTML 문법상 약속된 기능이 있는 특수기호가 약속된 기능을 실행시키는 것이 아닌 단순 문자열로 사용될 수 있도록 하는 것이기 때문에 보통 HTML entity를 통해 <, > 와 같은 특수문자 필터링을 우회한다고 해서 XSS가 발생하지는 않는다. 하지만 Dark Rune application은 document content를 node-html-markdown 라이브러리를 통해 markdown으로 랜더링한 이후 markdown-pdf가 한번 더 markdown을 HTML로 랜더링해주기 때문에 이야기가 달라진다. document content에서 HTML entity &gt;를 이용해 <script> 문자를 표현하면 node-html-markdown 라이브러리가 <script> 문자를 markdown으로 랜더링해준다. 이후 server side XSS 취약점이 존재하는 markdown-pdf가 <script>를 필터링하지 않고 그대로 내부 브라우저를 통해 HTML로 랜더링하기 때문에 server side XSS가 발생한다.

이 방법을 이용해 <iframe src="file:///flag.txt" width="100%" height="500px"> 태그를 삽입하고 pdf로 출력해보면 flag가 출력되는 것을 확인할 수 있다.

profile
HGU 20 김민석

0개의 댓글