한번 봅시다아
코드 ㄱㄱ
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 |