파일 관리 홈페이지를 덜 구현한 것을 재현한 문제인 것 같다
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);
문제 파일에는 크게 index.ejs, app.js, dockerfile 이렇게 세 개가 있습니다 위의 코드는 app.js 파일의 소스코드이다
위 코드를 대충 훑어보면 우선 오브젝트를 생성하여 파일을 만드는 기능, 만든 파일을 읽는 기능, test페이지의 만들다가 만 것 같은 기능 크게 세 개의 구역으로 나눠서 보이는 것 같다
그럼 이제 문제 페이지를 살펴보자
이런 페이지가 나온다
대충 보면 파일 이름과 내용을 입력 후 'send'버튼을 누르면 그 이름과 내용으로 된 파일이 만들어지는 구조인 페이지인 것 같다.
입력창 이름은 각각 'filename', 'content'이다 그렇다면 이것을 입력하면 어떤 식으로 작동되는지 소스코드에서 다시 살펴보자
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');
}
})
}
})
파일을 읽는 기능을 구현하는 코드이다
'filename'의 값이 null일 경우 저장 디렉터리에 있는 파일을 읽어들이는 것 같고 아닐 경우에는 'filename'에서 기호 '.'이 있거나 공백이 있는지 필터링을 거친 이후 읽어들이는 것 같다
이 필터링을 넣은 것은 filedownload 취약점에 대비하기 위해 사용하는 것 같다
다음으로 test 페이지를 구성하는 코드를 살펴보자
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");
}
})
간단하다
막 들어가면 "this page hasn't been made yet" 이라는 문구가 뜨게 되어있고, request query로 'rename'이나 'reset'을 받을 경우엔 이름에 맞는 기능을 구현하게끔 만들어져 있는 것 같다
문제 풀이 Point
https://blog.coderifleman.com/2019/07/19/prototype-pollution-attacks-in-nodejs/
Node.js에서의 프로토타입 오염 공격이란 무엇인가
__proto__을 이용한 프로토타입 오염(prototype pollution) 공격의 원리를 설명하면서 노드 환경에서 실제 공격이 가능한 사례를 함께 소개합니다.
blog.coderifleman.com
이 문제는 프로토타임 오염 공격을 사용하는 문제이다
// 오브젝트
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;
}
}
// test페이지
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");
}
})
이 코드가 프로토타임 오염 취약점을 발생시키는 부분이다
오브젝트를 구성하는 과정에서 setValue를 사용하여 값을 구성하고, test 페이지에선 setValue를 사용하여 rename 기능을 구현해놓았기 때문에 test 페이지에서 rename 기능을 사용하여 read 기능을 오염시키는 것이 핵심이다.
공격방식
공격은 /test 경로에서 시작한다
test 경로는 request query를 func=rename을 받으면 rename 기능을 사용할 수 있기 때문에 ` /test?func=rename&filename=__proto__.filename&rename=경로 ` 이런식의 익스플로잇 코드를 url에 적어서 보내주면 공격이 가능하다
그런데 여기서 경로를 어떻게 지정을 해야할까 위 경로는 우리가 열람 하고자 하는 flag 파일의 경로를 지정해주면 된다
하지만 우리는 flag파일의 경로를 모르기 때문에 ` ./../../../../../../flag ` 를 입력해주면 상위 디렉터리로 이동한 후 다시 현재 디렉터리로 돌아오기 때문에 __proto__가 위에서 설정한 flag의 경로로 바뀌게 된다.
이렇게 url을 보내고 나서 test페이지 기능 중 reset 이라는 기능을 써줘야 우리가 바꾼 경로이름으로 readfile을 실행시킬 수 있다 왜냐하면 read['filename'] 이라는 객체가 오버라이딩 되어있기 때문에 부모 객체를 참조하지 않기 때문 그래서 꼭 reset을 시켜주어야 한다
위에서 설명한대로 url에 익스플로잇 해주면 성공적으로 rename이 완료된다
그러고 나서 reset을 시켜주면
reset도 성공적으로 완료되었다
그리고 나서 /readfile에 접속하면 filename값은 자동으로 null값이 입력되기 때문에 성공적으로 flag가 출력된다
'DreamHack > Web hacking' 카테고리의 다른 글
[LEVEL-2] Addition calculator (1) | 2024.01.05 |
---|---|
[LEVEL-2] Dream Gallery (1) | 2024.01.02 |
[LEVEL-1] NoSQL-CouchDB (1) | 2023.12.26 |
[LEVEL-1] Command Injection Advanced (1) | 2023.12.25 |
[LEVEL-1] CSRF Advanced (0) | 2023.12.24 |