티스토리 뷰

728x90
반응형

문제 개요

express cookie-parser 모듈로 object 파싱하고 XSS 기법으로 관리자의 세션을 탈취하는 문제

 

문제 풀이

우선 app.js 상단 코드는 아래와 같습니다.

const express = require("express");
const cookieParser = require('cookie-parser');
const goose = require("./goose");
const clean = require('xss');

const app = express();
app.use(cookieParser());
app.use(express.urlencoded({extended:false}));

const PORT = process.env.PORT || 9988;

const headers = (req, res, next) => {
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('X-Content-Type-Options', 'nosniff');
    return next();
  }
app.use(headers);
app.use(express.static('public'))

goose.js 에는 단순히 관리자가 puppeteer 모듈로 사용자가 보낸 url 에 세션값에 FLAG 값을 설정한 뒤에 접근하는 코드가 정의되어 있습니다.

 

위 코드에서 특별하게 볼 부분은 xss 모듈인데요. xss 모듈 버전은 최신버전으로 보였고, 만약 이 모듈로 막혀있다면 우회하기는 쉽지 않아보였습니다.

 

나머지 코드는 아래와 같습니다.

const template = (goosemsg, goosecount) => `
<html>
<head>
<style>
H1 { text-align: center }
.center {
    display: block;
    margin-left: auto;
    margin-right: auto;
    width: 50%;
  }

  body {
    place-content:center;
    background:#111;
  }

  * {
    color:white;
  }

</style>
</head>
${goosemsg === '' ? '': `<h1> ${goosemsg} </h1>`}
<img src='/images/goosevie.png' width='400' height='700' class='center'></img>
${goosecount === '' ? '': `<h1> You have honked ${goosecount} times today </h1>`}

<form action="/report" method=POST style="text-align: center;">
  <label for="url">Did the goose say something bad? Give us feedback.</label>
  <br>
  <input type="text" id="site" name="url" style-"height:300"><br><br>
  <input type="submit" value="Submit" style="color:black">
</form>
</html>
`;


app.get('/', (req, res) => {
    if (req.cookies.honk){
        //construct object
        let finalhonk = {};
        if (typeof(req.cookies.honk) === 'object'){
            finalhonk = req.cookies.honk
        } else {
            finalhonk = {
                message: clean(req.cookies.honk), 
                amountoftimeshonked: req.cookies.honkcount.toString()
            };
        }
        res.send(template(finalhonk.message, finalhonk.amountoftimeshonked));
    } else {
        const initialhonk = 'HONK';
        res.cookie('honk', initialhonk, {
            httpOnly: true
        });
        res.cookie('honkcount', 0, {
            httpOnly: true
        });
        res.redirect('/');
    }
});

app.get('/changehonk', (req, res) => {
    res.cookie('honk', req.query.newhonk, {
        httpOnly: true
    });
    res.cookie('honkcount', 0, {
        httpOnly: true
    });
    res.redirect('/');
});

app.post('/report', (req, res) => {
    const url = req.body.url;
    goose.visit(url);
    res.send('honk');
});

app.listen(PORT, () => console.log((new Date())+`: Web/honksay server listening on port ${PORT}`));

보면 알겠지만 조건문에서 req.cookies.honk 가 object 타입이라면 honk 객체를 finalhonk 변수에 그대로 대입하고 있는 것을 알 수 있습니다. 만약 object가 아닌 string 타입이라면 finalhonk 변수에 message 키값과 amountoftimeshonked 키값에 각각 xss 필터링을 한 req.cookies.honk 값과 req.cookies.honkcount 값을 문자열로 변환해서 대입합니다.

 

이 대목에서 단순히 보면 req.cookies.honkcount 값은 xss 필터링을 하지 않았기 때문에 XSS 공격에 취약하다는 것을 알 수 있습니다. 다만 애초에 req.cookies.honk 타입이 object 라면 xss필터링을 하지 않기 때문에 express 의 cookie parser 모듈이 object 타입으로 인식하게만 해주면 문제는 풀립니다.

j:{"message":"<script>공격페이로드</script>","amountoftimeshonked":"1"}

위와 같은 형태로 cookie parser 에 보내면, cookie parser 는 아래 항목에서 j: 로 시작하는 문자열이 들어올 경우에 JSON object 로 파싱하게 됩니다.

// https://github.com/expressjs/cookie-parser/blob/1.4.6/index.js#L84
function JSONCookie (str) {
  if (typeof str !== 'string' || str.substr(0, 2) !== 'j:') {
    return undefined
  }

  try {
    return JSON.parse(str.slice(2))
  } catch (err) {
    return undefined
  }
}

이 원리를 이용해서 object 를 만들어주고, template 에 message와 amountoftimeshonked 값이 그대로 반영되는 점을 이용해서 XSS(Cross Site Scripting) 공격을 할 수 있게 됩니다.

로컬호스트 상에서 관리자의 세션인 flag를 잘 가져온 것을 확인할 수 있습니다.

 

본 문제는 MapleCTF2022에서 웹 분야 중 제일 쉬운 문제에 속한 문제입니다. 또한 SSTF2022에서 출제된 JWT Decoder 문제와도 유사했던 문제였던 것 같습니다.

 

- 끝 -

728x90
반응형
댓글