You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
constfs=require('fs');constaxios=require('axios');constexpress=require('express');constmulter=require('multer');constmustacheExpress=require('mustache-express');constRedis=require('ioredis');const{v4: uuidv4}=require('uuid');constRECAPTCHA_SITE_KEY=process.env.RECAPTCHA_SITE_KEY||'[site key is empty]';constRECAPTCHA_SECRET_KEY=process.env.RECAPTCHA_SECRET_KEY||'[secret key is empty]';constSECRET=process.env.SECRET||'s3cr3t';constFLAG=process.env.FLAG||'Neko{dummy}';constREDIS_URL=process.env.REDIS_URL||'redis://127.0.0.1:6379';constapp=express();app.use(require('cookie-parser')());app.use('/static',express.static('static'));app.engine('mustache',mustacheExpress());app.set('view engine','mustache');app.set('views',__dirname+'/views');constport=5000;conststorage=multer.diskStorage({destination: './tmp/'});constredis=newRedis(REDIS_URL);letuploadedFiles={};letcheckedFiles={};constID_TABLE='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';functiongenerateId(n=8){letres='';for(leti=0;i<n;i++){res+=ID_TABLE[Math.random()*ID_TABLE.length|0];}returnres;}// admin only!functionadminRequired(req,res,next){if(!('secret'inreq.cookies)){res.status(401).render('error',{message: 'Unauthorized'});return;}if(req.cookies.secret!==SECRET){res.status(401).render('error',{message: 'Unauthorized'});return;}next();}app.get('/',(req,res)=>{res.render('index');});app.get('/flag',adminRequired,(req,res)=>{res.send(FLAG);});constSIGNATURES={'png': newUint8Array([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]),'jpg': newUint8Array([0xff,0xd8])};functioncompareUint8Arrays(known,input){if(known.length!==input.length){returnfalse;}for(leti=0;i<known.length;i++){if(known[i]!==input[i]){returnfalse;}}returntrue;}functionisValidFile(ext,data){// extension should not have special charsif(/[^0-9A-Za-z]/.test(ext)){returnfalse;}// prevent uploading files other than imagesif(!(extinSIGNATURES)){returnfalse;}constsignature=SIGNATURES[ext];returncompareUint8Arrays(signature,data.slice(0,signature.length));}constupload=multer({
storage,limits: {files: 1,fileSize: 100*1024}});app.post('/upload',upload.single('file'),(req,res)=>{const{ file }=req;fs.readFile(file.path,(err,data)=>{constbuf=newUint8Array(data);constfileName=file.originalname;constext=fileName.split('.').slice(-1)[0];// check if the file is safeif(isValidFile(ext,buf)){constnewFileName=uuidv4()+'.'+ext;fs.writeFile('uploads/'+newFileName,buf,(err,data)=>{letid;do{id=generateId();}while(idinuploadedFiles);uploadedFiles[id]=newFileName;res.json({status: 'success',
id
});});}else{res.json({status: 'error',message: 'Invalid file'});}});});// show uploaded contentsconstMIME_TYPES={'png': 'image/png','jpg': 'image/jpeg'};app.get('/uploads/:fileName',(req,res)=>{const{ fileName }=req.params;constpath='uploads/'+fileName;// no path traversalres.type('text/html');// prepare for error messagesif(/[/\\]|\.\./.test(fileName)){res.status(403).render('error',{message: 'No hack'});return;}// check if the file existstry{fs.accessSync(path);}catch(e){res.status(404).render('error',{message: 'Not found'});return;}// send proper Content-Type headertry{constext=fileName.split('.').slice(-1)[0];res.type(MIME_TYPES[ext]);}catch{}fs.readFile(path,(err,data)=>{res.send(data);});});app.get('/:id',(req,res)=>{const{ id }=req.params;if(!(idinuploadedFiles)){res.status(404).render('error',{message: 'Not found'});return;}res.render('file',{path: uploadedFiles[id],checked: idincheckedFiles,siteKey: RECAPTCHA_SITE_KEY,
id
});});// report image to adminapp.post('/:id/report',async(req,res)=>{const{ id }=req.params;const{ token }=req.query;/* const params = `?secret=${RECAPTCHA_SECRET_KEY}&response=${encodeURIComponent(token)}`; const url = 'https://www.google.com/recaptcha/api/siteverify' + params; const result = await axios.get(url); if (!result.data.success) { res.json({ status: 'error', message: 'reCAPTCHA failed' }); return; }*/redis.rpush('query',id);redis.llen('query',(err,result)=>{console.log('[+] reported:',id);console.log('[+] length:',result);res.json({status: 'success',length: result});})})// admin onlyapp.get('/:id/confirm',adminRequired,(req,res)=>{const{ id }=req.params;if(idinuploadedFiles){checkedFiles[id]=true;}res.send('done');});app.listen(port,'0.0.0.0',()=>{console.log(`Example app listening at http://localhost:${port}`);});
Writeup
We can upload a file but the file extension is restricted. For /uploads/:fileName, the default content type is text/html, so our goal is to bypass the extension check below:
constSIGNATURES={'png': newUint8Array([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a]),'jpg': newUint8Array([0xff,0xd8])};functioncompareUint8Arrays(known,input){if(known.length!==input.length){returnfalse;}for(leti=0;i<known.length;i++){if(known[i]!==input[i]){returnfalse;}}returntrue;}functionisValidFile(ext,data){// extension should not have special charsif(/[^0-9A-Za-z]/.test(ext)){returnfalse;}// prevent uploading files other than imagesif(!(extinSIGNATURES)){returnfalse;}constsignature=SIGNATURES[ext];returncompareUint8Arrays(signature,data.slice(0,signature.length));}
If you are familiar with JavaScript, it's easy to find a valid ext which is toString, a default function in Object.prototype, so 'toString' in SIGNATURES is always true.
How about SIGNATURES[ext].length? In JavaScript, function also has length attribute, represent the length of parameters:
Next, we need to report this file to admin. The route for reporting is app.post('/:id/report') but our image url is /uploads/{uuid}.toString, so we need to encoded / to %2f: /uploads%2fuuid.toString/report
The text was updated successfully, but these errors were encountered:
Source code
Writeup
We can upload a file but the file extension is restricted. For
/uploads/:fileName
, the default content type istext/html
, so our goal is to bypass the extension check below:If you are familiar with JavaScript, it's easy to find a valid
ext
which istoString
, a default function inObject.prototype
, so'toString' in SIGNATURES
is always true.How about
SIGNATURES[ext].length
? In JavaScript, function also haslength
attribute, represent the length of parameters:So, we can use
.toString
as file extension and bypass the check. Here is the content:Next, we need to report this file to admin. The route for reporting is
app.post('/:id/report')
but our image url is/uploads/{uuid}.toString
, so we need to encoded/
to%2f
:/uploads%2fuuid.toString/report
The text was updated successfully, but these errors were encountered: