[Clear] LIT CTF 2021 web/LIT BUGS Writeup
주어진 페이지에 가보면 대회 페이지와 유사한 페이지가 나타난다. 다른 기능들은 막혀있고, 회원 가입 및 로그인 기능만 살아있다. 문제에서 언급되어있듯이, 가입되어있는 팀 이름이 플래그라고 한다.
주어진 소스의 일부이다. 핵심 기능인 로그인 부분을 분석해보자. 특이한점은 소켓 통신을 하고 있다는 점이다.
io.on('connection',(socket) => {
socket.on('login',(tn,pwd) => {
if(accounts[tn] == undefined || accounts[tn]["password"] != md5(pwd)) {
socket.emit("loginRes",false,-3); ##### ............................. (1)
return;
}
socket.emit("loginRes",true,accounts[tn]["rand_id"]); ##### ................. (2)
return;
});
socket.on('reqName',(rand_id) => {
name = id2Name[parseInt(rand_id)];
socket.emit("reqNameRes",name); ##### ....................................... (3)
});
... 중략(register)
});
우선, 계정 정보를 맞추어 로그인 시도를 하여 (1)의 조건을 만족하면 (2) 로직이 수행된다.
정상적인 통신 시 (1)~(3) 까지의 로그인 과정이 실제로 어떻게 수행되는지 프록시 툴을 사용하여 확인하면 아래와 같다.
최종적으로는 (3)의 요청에 대해 아래와 같은 응답이 옴으로써 팀 이름을 확인 할 수 있다.
(1), (2), (3) 각각의 과정이 socket.io / socket.emit 으로 각각 수행되기 때문에, 아무 계정으로 로그인을 시도하여 (1)의 조건을 만족시키고, (1)과 (2) 사이 혹은 (2)와 (3) 사이에 rand_id 를 변조하면 타 계정으로 로그인이 될 것이라고 생각했고, 실제로 두 개의 계정을 통해서 테스트 한 결과 실제로 타 계정으로 로그인하는데 성공했다.
플래그인 기생성되어있는 계정은 아래 로직으로 생성되기 때문에, rand_id가 0~1000 범위안의 임의의 값임을 확인 할 수 있다.
const flag = fs.readFileSync("flag.txt",{encoding:'utf8', flag:'r'});
var accounts = {};
// Special account for LIT Organizers
var admin_id = Math.floor(Math.random() * 1000)
accounts[flag] = {
"password": md5(flag),
"rand_id": admin_id
};
소켓 통신이기 때문에 패킷 재사용이 불가하기 때문에 brute force를 자동화하기엔 번거롭다고 생각했고,
버프 스위트의 Match and Replace 기능을 통해 아래와 같이 수동으로 brute force 공격을 수행함으로써 플래그를 획득 할 수 있었다.
FLAG = flag{if_y0u_d1d_not_brut3force_ids_plea5e_c0ntact_codetiger}