SKYX LAB 모의해킹 — 웹 진입부터 root 권한까지

이번엔 현직자 특강에서 진행하였던 모의해킹을 리뷰해보려고 한다.
페이지가 생각보다 엄청 깔끔하고 고퀄이라서 놀랐다.
주의: 본 글은 인가된 교육용 CTF/모의침투 실습 환경에서 진행한 내용을 정리한 것이다. 허가받지 않은 시스템에 같은 방법을 시도하면 안 된다.
STEP 0. 정찰
일단 처음에 접속했는데 로그인 페이지도 안 보이고 버튼도 없었다.
그나마 단서라고 한다면 랜딩 문구에 “멤버십 도메인으로 접속해주세요” 정도였다.
이 문장을 보고 지금 보이는 정문 페이지는 껍데기이고, 실제 서비스로 들어갈 수 있는 다른 서브도메인 또는 가상 호스트가 존재할 것이라고 생각했다.
이 서버는 같은 IP로 들어온 요청이라도 HTTP의 Host 헤더 값에 따라 서로 다른 사이트를 보여주는 이름 기반 가상 호스팅(Name-based Virtual Hosting) 구조일 수 있다. 그래서 후보 이름을 바꿔가며 요청을 보내고, 상태 코드와 응답 크기가 다른 대상을 찾아보았다.
for h in portal member vip prive my api admin app booking; do
printf "%-16s -> " "$h.skyx.lab"
curl -s -o /dev/null -w "%{http_code} %{size_download}\n" \
-H "Host: $h.skyx.lab" http://192.168.10.101/
done
이 방식은 ffuf 같은 도구로 Host 헤더를 퍼징하는 것과 같은 원리다. 다만 이번에는 후보 목록이 짧아서 반복문으로 직접 확인했다.
결과적으로 vip.skyx.lab에서 다른 응답과 함께 200 OK가 나왔고, 해당 주소로 접속했더니 로그인 페이지가 나타났다. 로컬 실습 환경이라면 도메인이 해당 실습 서버 IP를 가리키도록 hosts 설정이 필요할 수도 있다.

STEP 1. 로그인
이제 로그인을 하기 위해 계정 정보를 찾아야 한다.
바로 SQL Injection부터 시도하기 전에 브라우저로 내려오는 JavaScript 파일을 먼저 확인해보았다. 클라이언트에 전달되는 JavaScript는 누구나 읽을 수 있기 때문에, 여기에 비밀번호나 API 키 같은 민감정보를 적어두면 주석 처리 여부와 상관없이 그대로 노출된다.
그런데 실제로 다음과 같은 주석이 남아 있었다.
// 임시 운영자 계정(삭제필요) - binary9 / P@ssw0rd
주석으로 막아두었다고 해서 사용자에게 보이지 않는 것이 아니다. 빌드 결과물에 포함된 순간 개발자 도구나 정적 파일을 통해 누구나 확인할 수 있다. 운영 계정을 프런트엔드 코드에 하드코딩해둔 것이 이번 취약점의 핵심이었다.
이 계정으로 첫 번째 FLAG를 발견하고 로그인에도 성공하였다.

STEP 2. SQL Injection
접속하자마자 항공편 검색창이 보였고, 입력값이 서버의 SQL 쿼리에 안전하게 처리되는지 확인해보았다.
따옴표 하나를 입력했더니 다음과 같이 데이터베이스 오류와 쿼리 원문 일부가 그대로 노출되었다.

오류 메시지가 보인다는 사실만으로 SQL Injection이 확정되는 것은 아니지만, 사용자 입력 때문에 SQL 문법 오류가 발생했고 이후 ORDER BY와 UNION SELECT 구문이 실제 쿼리 결과에 반영되는 것을 확인하면서 SQL Injection이 가능하다는 것을 검증할 수 있었다.
먼저 ORDER BY 등을 이용해 기존 조회 쿼리의 컬럼 개수를 확인하고, 그 개수에 맞춰 UNION SELECT를 구성했다. UNION으로 두 SELECT 결과를 합치려면 양쪽의 컬럼 개수가 같아야 하고, 대응하는 데이터 형식도 호환되어야 한다.
컬럼개수는 4개라는걸 확인하였다.
그 후 information_schema를 조회하여 현재 데이터베이스의 테이블 목록을 뽑았다.
zzz' UNION SELECT table_name,2,3,4
FROM information_schema.tables
WHERE table_schema = database() #

확인 결과 bookings, flights, member_profiles, users 테이블이 보였다.
information_schema는 MySQL 계열 데이터베이스에서 데이터베이스, 테이블, 컬럼 같은 메타데이터를 제공하는 시스템 스키마다. 여기서 information_schema.tables를 조회하면 테이블 이름을, information_schema.columns를 조회하면 각 테이블의 컬럼 이름을 확인할 수 있다.
예를 들어 users 테이블의 컬럼은 다음과 같은 방식으로 확인할 수 있다.
zzz' UNION SELECT column_name,2,3,4
FROM information_schema.columns
WHERE table_schema = database()
AND table_name = 'users' #

이를 통해 id, username, password, role이라는 컬럼 이름을 알아냈고, 이후 회원 정보와 비밀번호 데이터를 조회할 수 있었다.



또한 일반 검색 화면에는 나오지 않던 숨겨진 항공편도 발견할 수 있었다.

3000만 원짜리 티켓…
이 취약점은 서버가 입력값을 문자열로 이어 붙여 SQL을 만들었기 때문에 발생한 것으로 보인다. 방어할 때는 Prepared Statement/Parameterized Query를 사용하고, 운영 환경에서는 상세한 DB 오류를 사용자에게 그대로 보여주지 않아야 한다. 비밀번호도 평문이 아닌 안전한 비밀번호 해시로 저장해야 한다.
STEP 3. 프로필 IDOR
항공편 검색에서 웬만한 건 다 알아냈으니 마이페이지에 들어가보았다.

여기서 내 프로필 조회를 눌렀더니 네트워크 요청의 POST body에 내 username이 들어가는 것을 확인했다.
그래서 “내 아이디를 요청 body에 담네? 그럼 이 값을 admin으로 바꿔서 보내면 다른 사람의 정보가 나올까?”라는 의문을 갖고 요청을 수정해보았다.
그 결과 서버가 로그인한 사용자의 권한을 기준으로 조회 대상을 검증하지 않고, 클라이언트가 보낸 username만 믿고 다른 사용자의 정보를 반환했다.

이런 식으로 FLAG 두 개가 발견되었다.
이 취약점이 **IDOR(Insecure Direct Object Reference)**이다. 단순히 요청 파라미터를 숨기거나 버튼을 안 보이게 하는 것으로는 막을 수 없다. 서버가 세션이나 토큰에서 확인한 현재 사용자와 요청한 리소스의 소유자·권한을 매번 비교해야 한다.
STEP 4. 예약 가격 파라미터 변조
그다음으로는 예매 페이지에 들어가 아무 티켓이나 예매해보았다.

그랬더니 다음과 같이 요청 body에 항공편 정보와 결제 가격인 fare 값이 함께 전송되고 있었다.

클라이언트가 보내는 값은 사용자가 언제든 바꿀 수 있으므로 서버가 신뢰하면 안 된다. 그런데 이 서버는 클라이언트가 보낸 가격을 그대로 결제 금액으로 사용하고 있었다.
그래서 fare: 1로 가격을 위조하여 요청을 보냈고, 정상 가격이 아닌 1원으로 예약이 처리되면서 FLAG를 발견할 수 있었다.
FLAG{가격변조_결제금액조작}

또한 결제 금액을 음수로도 요청해보았다.

음수 값까지 허용되면서 돈이 차감되는 대신 잔액이 증가했고, 또 다른 FLAG를 발견했다.
FLAG{음수결제_잔액증가}
이것은 단순한 화면 조작 문제가 아니라 서버 측 비즈니스 로직 검증이 빠진 취약점이다. 서버는 항공편 ID를 기준으로 실제 가격을 DB에서 다시 조회해야 하고, 금액이 허용 범위의 양수인지와 결제·잔액 계산이 올바른지도 검증해야 한다.
이뿐만이 아니었다. 내 예약 정보를 조회했을 때 응답에서 예약 ID가 1001인 것을 보고, 요청의 값을 1002로 바꿔보았다. 그랬더니 다른 사람의 예약 정보가 그대로 조회되었다.

FLAG{IDOR_타인_예약_열람}
이것도 앞의 프로필 조회와 같은 IDOR 문제다. 예약 번호가 연속적인 것 자체가 취약점의 본질은 아니며, 서버가 “현재 로그인한 사용자가 이 예약을 볼 권한이 있는가?”를 검사하지 않은 것이 핵심이다.
STEP 5. LFI / Path Traversal
이번엔 e-티켓 메뉴로 가서 티켓 한 장을 읽어보았다.

그랬더니 URL이 다음과 같은 형태로 되어 있었다.
/api/v1/tickets/1001/boarding-pass?file=e-ticket.pdf
file 파라미터로 서버에서 읽을 파일 이름을 직접 전달하고 있었기 때문에, ../를 이용해 기준 디렉터리 밖의 파일까지 접근할 수 있는지 확인해보았다.
문제는 처음에는 웹 서버 내부의 기준 경로를 모른다는 것이었다. 그래서 존재하지 않는 파일 이름을 넣어 오류 메시지를 확인했다.
?file=nonono
→ {"error":"... '/app/tickets/nonono'"}
오류 응답을 통해 애플리케이션이 /app/tickets/를 기준으로 파일을 찾고 있다는 사실이 노출되었다. 이런 상세 경로 노출도 공격자에게 내부 구조를 알려주는 정보 노출 취약점이 될 수 있다.
그 후 여러 단계의 ../를 사용해 상위 디렉터리로 이동한 뒤 /etc/passwd를 읽어보았다.

응답에 시스템 계정과 경로 정보가 나타났고 FLAG도 찾을 수 있었다.
FLAG{LFI_경로조작_파일읽기}
이번 경우의 더 정확한 동작은 Path Traversal(Directory Traversal)을 통한 임의 파일 읽기다. 흔히 LFI라고도 부르지만, LFI는 서버가 공격자가 지정한 로컬 파일을 애플리케이션 코드에 포함해 실행하거나 해석하는 문맥에서 주로 쓰인다. 여기서는 다운로드/조회 기능의 경로 검증이 없어 서버 로컬 파일 내용이 반환된 경우에 가깝다.
여기서 그치지 않고 애플리케이션 소스코드처럼 서비스 계정이 읽을 수 있는 파일을 확인하면 다른 취약점의 단서도 얻을 수 있다.
방어할 때는 사용자가 보낸 파일 경로를 그대로 이어 붙이지 말고, 서버가 발급한 파일 ID를 허용 목록과 매핑하는 방식이 안전하다. 부득이하게 경로를 처리한다면 정규화한 최종 경로가 허용된 기준 디렉터리 안에 있는지도 반드시 검사해야 한다.
STEP 6. JWT 권한 변조
여기는 강사님께서 힌트를 주셨다.
로그인 토큰에 들어 있는 일반 사용자 권한을 관리자 권한으로 바꾸고, 서버가 이를 유효한 토큰으로 받아들이게 만드는 것이 목표였다.
JWT는 보통 다음과 같이 점(.)으로 구분된 세 부분으로 구성된다.
header.payload.signature
header: 토큰 유형과 서명 알고리즘 정보payload:uid,username,role같은 클레임(데이터)signature: 앞의 두 부분이 발급 후 변조되지 않았는지 서버가 검증하기 위한 서명값
Header와 Payload는 일반적으로 Base64URL로 인코딩되어 있을 뿐 암호화된 것이 아니므로 누구나 디코딩해 내용을 볼 수 있다. 하지만 내용을 수정하면 기존 Signature와 맞지 않기 때문에, 정상적인 서버라면 변조된 토큰을 거부해야 한다.
가지고 있던 토큰을 JWT 분석 도구에 넣어 디코딩해보니 다음과 같이 Header, Payload, Signature 세 부분을 확인할 수 있었다.

여기서 중요한 점은 JWT를 디코딩했다고 해서 서명 비밀키가 그대로 보이는 것은 아니라는 것이다. 이번 실습에서는 서명키가 skyx5555처럼 짧고 추측하기 쉬운 값이어서, 강사님이 알려주신 도구의 사전 대입/검증 기능으로 약한 키를 찾아낼 수 있었다. 즉 화면에 보이는 Signature 문자열 자체가 skyx5555였던 것이 아니라, 그 Signature를 만들어낸 비밀키가 skyx5555였던 것이다.
처음에는 디코더 화면에서 username과 role을 admin으로 바꾸기만 했는데, 이것만으로는 유효한 토큰이 만들어지지 않는다.

인코더에서 username과 role을 admin으로, uid를 관리자 계정에 해당하는 2로 바꾼 뒤, 알아낸 키를 사용해 토큰을 다시 서명해야 했다. 이렇게 새 JWT를 만든 다음 기존 로그인 토큰을 교체하고 관리자 페이지를 새로 불러왔다.

그 결과 실제 관리자 권한을 사용할 수 있는 SKYX Admin Console로 이동하였다.
앞에서 IDOR로 admin 프로필을 조회한 것은 다른 사용자의 정보만 열람한 것이고, 이번에는 서버가 관리자로 신뢰하는 유효한 JWT를 위조하여 실제 관리자 기능에 접근한 것이므로 둘은 차이가 있다.
여기서 한 번 막혔던 지점도 있었다. 관리자 페이지를 먼저 로드한 후 토큰만 바꾸면 화면이 바로 갱신되지 않았다. 이 애플리케이션은 페이지가 처음 로드될 때 저장소나 쿠키의 토큰을 읽어 인증 상태와 관리자 화면을 구성하는 것으로 보였다. 따라서 위조한 토큰을 먼저 저장한 다음 관리자 페이지를 새로고침해야 Admin Console로 진입할 수 있었다.
다만 이것은 프런트엔드의 토큰 적용 시점에 관한 이야기다. 진짜 보안 검사는 화면을 숨기고 보여주는 데서 끝나면 안 되고, 서버가 모든 관리자 API 요청마다 JWT의 서명, 만료 시간, 발급자 같은 조건과 실제 권한을 검증해야 한다.
STEP 7. Command Injection / RCE
관리자 콘솔을 보면 통합 관리 옆에 네트워크 진단 메뉴가 보인다.
들어가보니 IP 주소를 입력하는 칸과 결과를 출력하는 칸이 있었다. 서버가 입력받은 주소를 ping 같은 시스템 명령어 문자열에 그대로 이어 붙일 가능성이 있다고 생각해, 실습 환경에서 명령 구분자를 이용해 추가 명령이 실행되는지 확인했다.
; ls -la를 입력하자 원래 네트워크 진단 명령 뒤에 파일 목록 조회 명령까지 실행되었다.

파일 목록에서 user.txt가 보였다.

FLAG{user_skyx_foothold}
발견.
이 취약점은 OS Command Injection이다. 웹 입력값을 셸 명령 문자열에 그대로 연결하면서 ; 같은 메타문자가 명령 구분자로 해석된 것이다. 이로 인해 웹 서버 프로세스의 권한 범위에서 임의 명령 실행이 가능해졌고, 영향 측면에서는 RCE(Remote Code Execution)로 이어진다.
방어할 때는 가능하면 셸 명령 자체를 호출하지 말고 전용 라이브러리를 사용해야 한다. 꼭 외부 프로그램을 실행해야 한다면 셸을 거치지 않는 인자 배열 방식으로 실행하고, IP 주소처럼 허용되는 입력 형식도 서버에서 엄격하게 검증해야 한다.
STEP 8. sudo 권한 상승
결국 이 모의해킹의 최종 목적은 root 권한을 얻는 것이다.
Command Injection으로 얻은 권한은 웹 애플리케이션을 실행하는 일반 사용자 권한이었다. 여기서 시스템의 잘못된 sudo 설정을 이용해 root 권한으로 올라갈 수 있는지 확인했다.
먼저 현재 사용자가 어떤 명령을 sudo로 실행할 수 있는지 확인하였다.
sudo -n -l
sudo -l은 현재 사용자에게 허용된 sudo 규칙을 보여주고, -n은 비밀번호 입력 프롬프트를 띄우지 않는 비대화형 옵션이다. 따라서 비밀번호가 필요한 상황이면 기다리지 않고 실패하며, 웹 명령 실행처럼 대화형 입력이 어려운 환경에서 확인하기 편하다.
결과는 다음과 같았다.
(ALL) NOPASSWD: /usr/bin/env
이 규칙은 현재 사용자가 비밀번호 없이 sudo를 통해 /usr/bin/env를 모든 사용자, 사실상 기본값인 root로 실행할 수 있다는 뜻이다.
문제는 /usr/bin/env가 환경 변수만 출력하는 단순한 프로그램이 아니라, 뒤에 전달한 다른 프로그램을 실행할 수도 있다는 것이다. 인자 제한 없이 env 전체를 NOPASSWD로 허용했기 때문에 원하는 명령을 root 권한으로 실행할 수 있었다.
플래그만 확인하려면 다음과 같이 root 권한으로 파일을 읽을 수 있다.
sudo /usr/bin/env cat /root/root.txt

실습 터미널처럼 대화형 셸을 사용할 수 있는 환경이라면 다음과 같이 root 셸을 실행할 수도 있다.
sudo /usr/bin/env /bin/bash

FLAG{root_skyx_pwned} — root 완전 장악
방어 측면에서는 NOPASSWD 대상을 단순히 실행 파일 경로만 보고 정하면 안 된다. env, 편집기, 인터프리터처럼 다른 프로그램이나 셸을 실행할 수 있는 도구는 사실상 임의 명령 실행 통로가 될 수 있다. 정말 필요한 명령만 고정된 인자와 함께 최소 권한으로 허용해야 한다.
탈취 순서 요약

정리하면 이번 실습의 흐름은 다음과 같았다.
- Host 헤더를 바꿔 숨겨진 가상 호스트
vip.skyx.lab발견 - 프런트엔드 JavaScript에 노출된 운영자 계정으로 로그인
- 항공편 검색 SQL Injection으로 DB 구조와 데이터 확인
- 프로필·예약 조회 IDOR로 다른 사용자의 정보 열람
- 예약 가격 파라미터 변조로 결제 금액 및 잔액 로직 악용
- Path Traversal로 서버의 로컬 파일 읽기
- 약한 JWT 서명키를 찾아 관리자 토큰 위조
- 관리자 네트워크 진단 기능의 Command Injection으로 명령 실행
- 잘못된 sudo 설정(
NOPASSWD: /usr/bin/env)으로 root 권한 상승
각 취약점 하나만 보면 별개의 문제처럼 보일 수 있지만, 실제로는 앞 단계에서 얻은 정보와 권한이 다음 공격의 발판이 되었다. 특히 클라이언트 입력을 신뢰한 문제, 서버 측 권한 검증 누락, 민감정보 노출, 과도한 시스템 권한이 연결되면서 최종적으로 root 권한까지 이어졌다.
본 리포트는 인가된 교육용 CTF/모의침투 실습 결과입니다.
원본 · velog.io