본문 바로가기

Web Hacking/Dreamhack

[Dreamhack] filestorage write up

 

한번 봅시다아

 

 

코드 ㄱㄱ

 

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

 

인덱스 페이지에서 파일을 작성하면 /mkfile에 전달되어 파일이 생성되고 파일명이 해시화되어 화면에 뿌려진다. 파일을 /storage 밑에 저장된다. 

 

 

mkfiile에서 파일 생성 시 전역 변수인 file에 filename을 key value로하여 값이 저장된다.

 

readfile을 보면 

 

 

flag가 /flag에 위치하기 때문에 path traversal로 인한 언인텐을 막기 위해 .을 필터링 하고 있다. 그럼 문제를 어떻게 해결하지 하고 마지막 /test를 봤다.

 

 

func, filename, rename 세 가지 변수에 입력을 가져와 func 값에 따라 if문을 실행한다. 핵심은 rename과 reset인 것 같다. 차례대로 분석해보자.

 

rename


먼저 file, filename, rename을 인자로 setValue함수를 실행시킨다. (file은 전역변수로 선언된 빈 객체이다)

 

 

setValue는 인자로 밭은 key를 .을 기준으로 구분하여 배열로 변환 후 keylist에 전달한다. e에는 keylist의 배열에서 맨 첫 번째 값이 저장된다. 이후 keylist에 값이 더 존재한다면 전역 변수 file에 저장된 filename이 오브젝트인지 판단한다.

-> file[filename] = {} ???

 

오브젝트가 아니면 오브젝트로 만든 후 다시 해당 오브젝트와 keylist를 문자열로 변경하여 setValue에 잔달한다. 

 

오브젝트라면 obj를 리턴한다. 즉 정리해보면 file = {'__proto__.hello': 'alert(1)'} 을 넘긴다고 가정하자.

 

keylist = ['__proto__', 'hello']가 들어가며

e = '__proto__'가 들어간다.

 

이때 file['__proto__']는 null이기 때문에 file['__proto__'] = {}가 실행되며 setValue(file['__proto__'], 'hello', 'alert(1)')이 실행된다. 다시 setValue로 가면

 

keylist의 길이가 0이기 때문에 file['__proto__']['hello'] = alert(1)로 덮힌다.

 

즉, file.__proto__hello = alert(1)로 오염되게 된다.

 

따라서 prototype pollution을 이용해 read 객체도 마음대로 덮을 수 있게 된다. 이제 공격벡터를 찾아보자. 파일을 읽어와야 되기 때문에 /readfile을 살펴본다.

 

 

코드를 보면 filename에 값이 있을 때 없을 때 실행되는 로직이 다르다. else문의 경우 read[filename]에 새로 값을 할당하기 때문에 prototype pollution으로 오염된 값을 쓸 수가 없게 된다. 따라서 if문에서 실행돼야 한다.

 

즉, read['filename']의 값을 ../../flag로 덮으면 된다. 페이로드를 작성해보자.

 

 

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

 

이후 read 객체를 초기화 해야 한다. prototype pollution의 경우 기존에 있던 값은 덮을 수 없다. 따라서 객체 초기화를 통해 빈 배열로 만들어줘 read['filename']을 덮어야한다.

 

 

 

이제 /readfile로 접근할 때 파라미터에 아무것도 넘기지 않으면 된다.

 

 

'Web Hacking > Dreamhack' 카테고리의 다른 글

[Dreamhack] EZ_command_injection write up  (0) 2024.05.19
[Dreamhack] Dream Gallery write up  (0) 2024.04.26
[Dreamhack] crawling write up  (0) 2024.04.26
[Dreamhack] file-csp-1 write up  (0) 2024.04.23
[Dreamhack] baby-sqlite write up  (0) 2024.04.22