Web Application Firewall

Project Member

  • Inweol Bae (TroubleMaker)
  • Jeongwon Jo (Pocas)

Code Analysis

WAF(Web Application Firewall)

웹 방화벽의 원리 파악과 부족한 개발 능력을 키우기 위해 Node.js를 이용한 Reverse Proxy 타입의 WAF를 구축해보았습니다.

fs.readFileSync("../rule/waf.rule").toString().split('\n').forEach(function(waf_regex) {
    if (waf_regex !== undefined) {
        RuleData.push(waf_regex);
    }
});

const Regex = (re) => {
    return new RegExp(re);
}

const filtering = (re, d) => {
    if(re.exec(d)) {
        return true;
    }
    return false;
}

const waf = (rule, data) => {
    result = true
    for(let i = 0; i < rule.length; i ++ ){
        REGEX = Regex(rule[i]);
        for (key in data) {
            if (key == 'query' || key == 'form') {
                for (p in data[key]){
                    if(filtering(REGEX, data[key][p])) {
                        result = false;
                    }
                    if (result == false) { break }
                };
            }
            if ( result == false ) { break }
            else{
                if(filtering(REGEX, data[key])) {
                    result = false;
                    break
                }
            }
        }
        if (result == false) {
            return result;
        }
    }
    return result;
}

위 코드의 waf함수는 인자값으로 rule과 data를 전달받아 rule을 기반으로 data에 악의적인 구문이 포함되어 있는지 검사하는 함수이며 rule에는 악의적인 구문을 검사하는 정규표현식, data에는 Request에 포함되어 있는 모든 헤더 및 쿼리와 Raw data가 들어있습니다.

const proxy_request = async (req, res, condition, ip) => {
    await db.all(`select * from waf where ip = ?`, ip, async (err, rows) => {
        // 5회 이상이면 응답을 안 함.
        let count = 0;
        if (rows.length !== 0) {
            count = rows[0].count;
        }
        if (count <= 5) {
            if (condition) {
                await proxy.web(req, res, {
                    target: `http://localhost:3009/`
                });
            } else {
                req.url = '/error';
                req.method = 'GET';
                await proxy.web(req, res, {
                    target: 'http://localhost:3009/'
                });
            }
        } else {
            console.log(`The ${ip} is block target`);
        }
    });
}

이번 프로젝트에서 구현한 WAF는 요청값에 악의적인 구문이 포함되어 있을 경우 요청한 IP의 count컬럼을 1씩 증가 시키고, count의 값이 5를 초과하면 IP를 차단하도록 구현하였습니다.

위의 porxy_request함수를 보면 현재 요청한 사용자의 IP를 확인하고 해당 IP의 count컬럼 값이 5를 초과하는지 검사합니다. 이때 count컬럼의 값이 5를 초과하지 않는다면 정상적인 응답값을 반환하고, 5를 초과하게 된다면 응답값을 반환하지 않고 요청을 끊게됩니다.

const value_parsing =  (element) => {
    let count = 0
    let searchChar = '=';
    let pos = element.indexOf(searchChar);

    while (pos !== -1) {
        count++;
        pos = element.indexOf(searchChar, pos + 1);
    }

    if (count > 1) {
        return element.substr(element.indexOf('=') + 1, element.length);
    } else {
        return element.split('=')[1];
    }
}

const ip_block = async (req, res , ip) => {
    await db.all(`select * from waf where ip = ?`, ip, (err, rows) => {
        if (err) {throw err;}
        if (rows.length == 0) {
            db.run('insert into waf (ip, count) values(?, ?)', [ip, 1], async (err) => {
                if (err) {console.error(err);}
                else {
                    console.log('[*] Successful Insert statement execution');
                    await proxy_request(req, res, false);
                }
            });
        } else {
            rows.forEach(async (row) => {
                count = row['count'];
                if (count > 5) {
                    console.log(`The ${ip} is block target`);
                } else {
                    db.run ('update waf set count=? where ip = ?', [count + 1, ip], async (err) => {
                        if (err) {console.error(err)}
                        else {
                            console.log('[*] Successful Update statement execution');
                            await proxy_request(req, res, false);
                        }
                    })
                }
            });
        }
    });
}

value_parsing함수의 경우 파라미터의 값을 정확하게 가져오기 위해 만들어진 함수입니다. 파라미터 값의 경우 “=”를 기준으로 파싱하게 되는데, 만약 요청값에 “=”가 여러개 존재한다면 파라미터의 값을 제대로 파악하지 못하게 됩니다.

ip_block함수는 waf함수에서 걸릴 경우 실행되는 함수입니다. 현재 요청한 IP가 DB에 존재하는지 확인하고, 존재하지 않는다면 해당 IP의 대한 값을 생성하고 만약 존재한다면 해당 IP의 count컬럼 값이 5를 초과하는지 확인하여 초과하면 IP를 차단하고, 초과하지 않는다면 count컬럼의 값을 1만큼 증가시킵니다.

http.createServer(function (req, res) {
    setTimeout(async function () {
        const rhost = req.connection.remoteAddress.split('ffff:')[1] ||
            req.socket.remoteAddress.split('ffff:')[1] ||
            req.connection.socket.remoteAddress.split('ffff:')[1];

        console.log(`[*] Connected IP : ${rhost}`)
        const request_data = {'method':'', 'host':'', 'port':'', 'path':''};
        request_data.method = req.method;
        request_data.host = req.headers.host.split(':')[0];
        request_data.port = req.headers.host.split(':')[1]; delete req.headers.host

        Object.keys(req.headers).forEach(element => {
            request_data[element] = req.headers[element];
        })

        if (request_data.method == "GET") {
            if (req.url.includes('?')) {
                request_data.path = req.url.split('?')[0];
            } else {
                request_data.path = req.url;
            }

            if (req.url.includes('?')) {
                request_data.query = {};
                query = decodeURI(req.url).split('?')[1].split('&');
                query.forEach(element => {
                    request_data.query[element.split('=')[0]] = value_parsing(element);
                });
            }
            if(waf(RuleData, request_data)) {
                await proxy_request(req, res, true, rhost);
            } else {
                await ip_block(req, res, rhost)
            }
        } 
        else if (request_data.method == "POST") {
            request_data.path = req.url;
            request_data.form = {};
            query = decodeURIComponent(req['_readableState']['buffer']['head']['data'].toString()).split('&');

            query.forEach(element => {
                request_data.form[element.split('=')[0]] = value_parsing(element);
            });
            if(waf(RuleData, request_data)) {
                await proxy_request(req, res, true, rhost);
            } else {
                await ip_block(req, res, rhost);
            }
        }
        else {
            return "hacking is fuck!!!!!!!!";
        }
    }, 500);
}).listen(8001);

위와 같이 http모듈을 이용하여 프록시 서버를 구동하고 있으며, req객체를 통해 http헤더를 request_data객체에 저장하고 위에서 분석한 waf, ip_block, proxy_request함수를 이용하여 요청 헤더를 검사하고 악의적인 구문이 존재하는지 파악한 뒤 적절한 응답처리 및 IP 차단기능을 실행하고 있습니다.


Demonstration video

Demonstrate by get method

Demonstrate by post method


Review

개인적으로 웹해킹을 공부하면서 부족한 점을 많이 느끼게되어 보안 공부와 코딩 공부를 동시에 할만한게 없을까? 생각하다가 떠오른게 바로 이번 프로젝트 였습니다. WAF의 원리를 파악하며 직접 개발해본다면 많은 도움이 될 것 같다는 생각이 들었고 같이 프로젝트를 진행할 사람을 찾게되어서 빠르게 진행하였습니다. 처음 진행하는 프로젝트다 보니 개발이 서툴어 도움을 많이 받게되었고 종종 미안함을 느꼈지만 그럴수록 더 열심히 참여하여 좋은 결과물을 만들어냈다고 생각합니다. 이번 프로젝트를 통하여 많은 배움과 경험이 있었고 자주는 못하더라도 앞으로 종종 이런 프로젝트를 진행해야겠다는 생각이 들었습니다.