연구실 안전교육 스킵하기

이준필·2025년 6월 19일
14
post-thumbnail

문제점

우리 학교는 기말고사 성적 확인을 하기 위해서는 연구실 안전교육이라는 것을 의무적으로 이수하여야 한다. 하지만 내 과는 컴퓨터공학부라 연구실에서 연구라는 걸 해본 적이 없는데 단과대가 공과대학이라는 이유만으로 일률적으로 이수를 강요하고 있다. 에브리타임에 옛날 옛적 누군가 만들어놓은 스킵코드가 존재하기는 하지만 이런 건 직접 해봐야 직성이 풀리지 않겠는가.

연구실 안전교육을 이수하는데 한 사람당 소요되는 시간이 어림잡아 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 이렇게 의문의 파라미터값들이 존재하는 영상이 있다.

1. 파라미터에 영상 번호만 존재하는 영상


개발자 도구로 확인해보면, 이 유형의 영상은 일정 주기마다 스크립트를 통해 서버에게 현재 진행 상태를 전송한다.

페이로드는 다음과 같은데 이 중 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초 정도의 지연이 발생한다.

2. 의문의 파라미터가 존재하는 영상

파라미터 관찰하기

이 분류의 영상은 하나밖에 없지만 필수로 분류되기 때문에 반드시 수강하여야 한다. 어떻게 exploit 할지 찾아보자.

우선 URL은 다음과 같았다. https://safety.konkuk.ac.kr/resources/contents/IMGT2024/1/02.html?passedPage=1&checkurl=2&smProgressNo=828954&smMemberNo=948504

여기서 눈여겨본 파라미터는 passedPagecheckurl이다. 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가 발생하며 수강이 기록되지는 않았다.

이렇게 서버 에러 로직이 클라이언트에게 직접 전달되는 거 정말 안 좋다고 생각합니다..

fetch/xhr 확인하기

오류가 발생했다면 서버쪽으로 전송하는 정보가 있을 것이라고 판단해 처음 두 개의 영상이 종료됐을 때 어떤 페이로드들이 주고받아지는지 확인해봤다.

// 첫 번쨰 영상
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 할 수 있었다. 👍

3. 평가하기..?

최종적으로 연구실 안전교육을 이수하려면 다음과 같은 문제를 풀어야한다.

중학교 도덕시간의 감성으로 풀었더니 통과기준인 60점이 나왔다. 응시에 제한이 있는 건 아니기 때문에 모든 사람들이 brute force하면 될 것 같다.

여담으로 문제를 분석해봤는데 페이로드에 답과 문항 정보 그리고 점수가 있는 점과 이게 유일한 HTTP 요청이었던 걸로 봐서는 클라이언트에 답안이 존재하는 것 같다.

하지만 한 번 시험에 합격한 뒤에는 문제에 대한 정보를 볼 수가 없어서 해당 부분은 자동화에 실패했다.

결론

종강도 했겠다 심심하던 차에, 콘텐츠 제공을 해준 우리 학교 연구실안전관리 시스템에게 감사를 표한다.

실험 가운을 아무도 사지 않는 과에게 연구실안전관리시스템 이수를 성적을 빌미로 강요하는 학교가 탐탁치는 않지만...

더 나아가서 비개발자분들이 사용하기 편하도록 URI에 따라 다른 함수가 실행되도록 해도 될 것 같지만 다른 멋진 분이 해주시리라 믿고 이만 마친다. 레포에 대한 링크는 여기에!

profile
웹, 인프라에 관심있는 대학생 개발자입니다.

4개의 댓글

comment-user-thumbnail
2025년 6월 27일

훌륭하십니다

답글 달기
comment-user-thumbnail
2025년 6월 29일

재밌게 읽었습니다 ! 근데 맥북은 그냥 영상 틀고 상태바에서 재생되고 있는 영상 재생바 조정해서 끝낼 수 있더라구요

답글 달기
comment-user-thumbnail
2025년 6월 30일

so attractive

답글 달기
comment-user-thumbnail
2025년 7월 1일

Thật tuyệt vời

답글 달기