본문 바로가기
드림핵

[드림핵] filestorage 풀이

by jwcs 2024. 3. 3.
728x90

https://dreamhack.io/wargame/challenges/643

 

filestorage

파일을 관리할 수 있는 구현이 덜 된 홈페이지입니다.

dreamhack.io

 

prototype pollution(프로토타입 폴루션) 문제이다.

const express=require('express');
const bodyParser=require('body-parser');
const ejs=require('ejs');
const hash=require('crypto-js/sha256');
const fs = require('fs');
const app=express();


var file={};
var read={};
function isObject(obj) {
  return obj !== null && typeof obj === 'object';
}
function setValue(obj, key, value) {
  const keylist = key.split('.');
  const e = keylist.shift();
  if (keylist.length > 0) {
    if (!isObject(obj[e])) obj[e] = {};
    setValue(obj[e], keylist.join('.'), value);
  } else {
    obj[key] = value;
    return obj;
  }
}

app.use(bodyParser.urlencoded({ extended: false }));
app.set('view engine','ejs');


app.get('/',function(req,resp){
	read['filename']='fake';
	resp.render(__dirname+"/ejs/index.ejs");

})

app.post('/mkfile',function(req,resp){
	let {filename,content}=req.body;
	filename=hash(filename).toString();
	fs.writeFile(__dirname+"/storage/"+filename,content,function(err){
		if(err==null){
			file[filename]=filename;
			resp.send('your file name is '+filename);
		}else{
			resp.write("<script>alert('error')</script>");
			resp.write("<script>window.location='/'</script>");
		}
	})

})

app.get('/readfile',function(req,resp){
	let filename=file[req.query.filename];
	if(filename==null){
		fs.readFile(__dirname+'/storage/'+read['filename'],'UTF-8',function(err,data){
			resp.send(data);
		})
	}else{
		read[filename]=filename.replaceAll('.','');
		fs.readFile(__dirname+'/storage/'+read[filename],'UTF-8',function(err,data){
			if(err==null){
				resp.send(data);
			}else{
				resp.send('file is not existed');
			}
		})
	}

})

app.get('/test',function(req,resp){
	let {func,filename,rename}=req.query;
	if(func==null){
		resp.send("this page hasn't been made yet");
	}else if(func=='rename'){
		setValue(file,filename,rename)
		resp.send('rename');
	}else if(func=='reset'){
		read={};
		resp.send("file reset");
	}
})


app.listen(8000);

 

/

첫 페이지다. 파일 이름과 파일 내용을 입력할 수 있다.

 

파일 제목 123 입력

이런 형식의 값을 뱉어낸다.

 

app.get('/',function(req,resp){
	read['filename']='fake';
	resp.render(__dirname+"/ejs/index.ejs");

})

app.post('/mkfile',function(req,resp){
	let {filename,content}=req.body;
	filename=hash(filename).toString();
	fs.writeFile(__dirname+"/storage/"+filename,content,function(err){
		if(err==null){
			file[filename]=filename;
			resp.send('your file name is '+filename);
		}else{
			resp.write("<script>alert('error')</script>");
			resp.write("<script>window.location='/'</script>");
		}
	})

})

해당 내용에 대한 코드다.

 

입력한 내용을 해싱한 후 해싱한 값으로 저장한다. 그 값을 우리에게 보여준다.

app.get('/readfile',function(req,resp){
	let filename=file[req.query.filename];
	if(filename==null){
		fs.readFile(__dirname+'/storage/'+read['filename'],'UTF-8',function(err,data){
			resp.send(data);
		})
	}else{
		read[filename]=filename.replaceAll('.','');
		fs.readFile(__dirname+'/storage/'+read[filename],'UTF-8',function(err,data){
			if(err==null){
				resp.send(data);
			}else{
				resp.send('file is not existed');
			}
		})
	}

})

파일을 읽을 수 있다. 우리가 받은 해싱 값을 넣어보자.

/readfile

 우리가 입력한 본문 내용을 확인할 수 있다.

 

app.get('/test',function(req,resp){
	let {func,filename,rename}=req.query;
	if(func==null){
		resp.send("this page hasn't been made yet");
	}else if(func=='rename'){
		setValue(file,filename,rename)
		resp.send('rename');
	}else if(func=='reset'){
		read={};
		resp.send("file reset");
	}
})

test페이지도 있다. 기능을 살펴보자. setValue를 해주는 것을 확인할 수 있다. 보통 setValue를 해주는 곳에서 프로토타입 폴루션이 발생하는 경우가 많다.

function setValue(obj, key, value) {
  const keylist = key.split('.');
  const e = keylist.shift();
  if (keylist.length > 0) {
    if (!isObject(obj[e])) obj[e] = {};
    setValue(obj[e], keylist.join('.'), value);
  } else {
    obj[key] = value;
    return obj;
  }
}

객체, 키, 밸류를 입력해서 값을 설정한다.

 

여기서 취약점이 발생한다. 지금부터 setValue를 어떻게 활용해서 익스플로잇 하는지 알아보자.

 

var file={};
var read={};

파일 최상단에 file과 read라는 객체가 생성되는 것을 볼 수 있다.

app.get('/test',function(req,resp){
	let {func,filename,rename}=req.query;
	if(func==null){
		resp.send("this page hasn't been made yet");
	}else if(func=='rename'){
		setValue(file,filename,rename)
		resp.send('rename');
	}else if(func=='reset'){
		read={};
		resp.send("file reset");
	}
})

test 페이지에서 우리가 입력한 filename과 rename을 통해 값을 설정할 수 있다.

 

filename에 `__proto__.filename`을 입력하고

rename에 `../../../../../../flag`를 입력한다고 해보자.

 

function setValue(obj, key, value) {
  const keylist = key.split('.');
  const e = keylist.shift();
  if (keylist.length > 0) {
    if (!isObject(obj[e])) obj[e] = {};
    setValue(obj[e], keylist.join('.'), value);
  } else {
    obj[key] = value;
    return obj;
  }
}

setValue(file, '__proto__.filename', '../../../../../../flag ')가 된다. 이것은 setValue(file['__proto__'],'filename', '../../../../../../flag' )가 된다. 앞서 봤던 file 객체의 프로토타입의 filename 속성을 '../../../../../../flag'으로 설정한다. 

app.js의 77번줄 ~ 80번줄

이후 read 객체를 리셋 시켜준다. 이 과정이 왜 필요하냐면,

30번줄 ~ 34번줄

처음에 인덱스 페이지에서 read 객체에 값을 설정한 것을 볼 수 있다. 이것을 리셋 시켜줘야 한다.

51번줄 ~ 56번줄

그래야 여기서 기본으로 설정해놓은 read['filename']에서 `fake`를 가져오지 않고 null로 인식해 프로토타입 체인을 통해 검색을 값을 검색하게 된다.

 

위 사진으로 프로토타입 폴루션에 대해 간략히 설명이 가능할 것 같다.

file, read 객체를 각각 만들었다.

file의 프로토타입에 filename이라는 속성에 1이라는 값을 넣었다.

read도 file과 같은 객체 타입이고, 같은 프로토타입을 공유하기 때문에 read를 통해 프로토타입의 filename에 접근하면 file 객체에서 선언한 1을 찾아볼 수 있다.

또한 read 객체에 직접적으로 `filename` 속성이 정의되어 있지 않으면 프로토타입 체인에 의해 부모 프로토타입의 속성을 검사한다. 따라서 read['filename']에서 1이 출력된다.

 

위와 같은 개념으로 디렉터리 트레버셜을 이용해 flag를 가져올 수 있다.

flag 위치

문제에서 flag 위치에 대한 언급이 없는데, dockerfile에서 찾을 수 있다.

 

flag

방어 방법

 

객체의 속성값을 사용자로부터 입력받아 설정하면서 해당 취약점이 발생했다. 따라서 속성값을 다룰 때 사용자의 입력을 받는 것을 지양한다. 받아야 한다면 엄격한 검증이 있어야 할것이다.

혹은 `Object.freeze(Object.prototype)`을 사용하여 prototype을 freeze할 수 있다고 한다.

출처:https://www.hahwul.com/cullinan/prototype-pollution/

 

Prototype Pollution

Introduction Prototype Pollution은 Javascript 처리 로직의 문제로 Object 들의 prototype을 수정할 수 있을 때 발생하는 보안 문제를 의미합니다. Object의 protype을 변경할 수 있는 경우 의도된 로직을 벗어나거

www.hahwul.com

 

728x90
반응형