🌑

Web Application Firewall based on Reverse Proxy

Not written by me. it was to write by troublemaker. I didn’t do anything other than translate (Korean to English)


Project Member

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

Code Analysis

WAF(Web Application Firewall)

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;
}

The waf function in the above code is a function that takes a rule and data in the value of the argument and checks whether the data contains malicious payload based on the rule, and the rule is a function that checks whether the malicious payload is included. Regular expression to check, data contains all the headers and queries and Raw data contained in Request.

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`);
        }
    });
}

The WAF implemented in this project increments the count column of the requested IP address by 1 if the request value contains malicious payload, and blocks the IP address when the count value exceeds 5. I implemented it to do.

Looking at the porxy_request() function above, it checks the IP address of the current requesting user and checks if the value in the count column of that IP address is greater than 5. At this time, if the value of the count column does not exceed 5, the normal response value is returned, and if it exceeds 5, the request is cut off without returning the response value.

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);
                        }
                    })
                }
            });
        }
    });
}

In case of value_parsing() function It is a function made to get the value of the parameter accurately. In the case of the parameter value, it will be parsed based on “=”, but if there are multiple “=” in the requested value, the parameter value will not be properly grasped.

The ‘ip_block()’ function is executed when the waf() function is blocked. Checks if the currently requested IP address exists in the DB, if it does not exist, generates a value for the IP address, and if it exists, it checks whether the value of the count column of the corresponding IP exceeds 5. If it exceeds, the IP is blocked. If it does not exceed the value of the count column, the value of the count column is increased by 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);

As above, the proxy server is running using the http module, and the http header is stored in the request_data object through the req object, and the request header using the waf(), ip_block(), and proxy_request() functions analyzed above. After inspecting and determining whether malicious payload exists, appropriate response processing and IP blocking functions are implemented.


Demonstration video

Demonstrate by get method

Demonstrate by post method


— Sep 10, 2021