Step 108. 환경변수와 PATH 인젝션
Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★☆☆ | 예상 소요 시간 2.5시간
전제: Step 23~24의 권한과 sudo, Step 106의 setuid, Step 28의 셸 스크립트 기초.
- 준비물: 리눅스 터미널(우분투/WSL). 오늘 실험은 전부
/tmp아래와 터미널 세션 안에서 일어나며, 창을 닫으면 원상태로 돌아갑니다. - ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다.
- 주의: 오늘 만드는 "가짜 명령어"는 무해한
echo스크립트이고, 실험이 끝나면 PATH를 원복하고 파일을 지웁니다. 이 기법을 다른 사람의 세션이나 서버에 적용하는 것은 명백한 공격입니다.
여러분이 ls라고 칠 때 컴퓨터는 정확히 무엇을 실행할까요? 정답은 "때에 따라 다릅니다"입니다. 셸은 PATH라는 환경변수에 적힌 폴더들을 앞에서부터 뒤져 처음 발견한 ls를 실행합니다. 즉 "무엇이 실행되는가"는 변수 하나가 결정합니다.
그렇다면 이 질문이 생깁니다 — 내가 쓸 수 있는 폴더가 PATH 맨 앞에 오면 어떻게 될까? 그리고 어떤 높은 권한의 스크립트가 경로 없이 ls를 호출하면? 내 가짜 ls가 그 권한으로 대신 실행됩니다. 이것이 PATH 인젝션입니다. 오늘은 환경변수의 원리부터 시작해, 이 공격을 직접 재현하고, 방어법으로 닫습니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 환경변수가 무엇이며
export와$변수로 읽고 쓴다 echo $PATH와which·type으로 명령어의 검색 경로와 실제 위치를 확인한다- 가짜 명령어를 만들어 PATH 조작으로 끼워 넣는 실험을 재현한다
- 절대경로 호출이 왜 이 공격을 무력화하는지 설명한다
- PATH 인젝션의 공격 시나리오와 방어법(절대경로, PATH 초기화, sudo secure_path)을 정리한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | 리눅스 셸(bash), 실험 폴더는 /tmp/fake |
| 오늘의 명령어 | echo $PATH, export PATH=..., which, type, env, ${PATH#접두사}(원복) |
| 필요한 개념 | 환경변수, PATH 검색 순서(앞이 우선), 절대경로 vs 이름 호출, PATH 인젝션, sudo의 secure_path |
| 오늘의 산출물 | PATH 인젝션 재현 기록 + 공격 시나리오·방어법 메모 |
2-1. 환경변수 — 모든 프로그램에게 열린 게시판
환경변수(environment variable)는 프로세스에 붙어 다니는 "이름=값" 목록입니다. 셸에서 프로그램을 실행하면 환경변수가 그대로 자식에게 전달됩니다. 전체 목록은 env로 볼 수 있고, 하나는 echo $이름으로 읽습니다.
대표적으로 HOME(내 홈 폴더), USER(내 이름), LANG(언어 설정)이 있고, 오늘의 주인공 PATH가 있습니다. 셸에서 바꾸면 그 셸과 그 이후 실행되는 프로그램에만 적용됩니다 — 터미널을 닫으면 사라집니다. 오늘의 실험이 안전한 이유입니다.
2-2. PATH — 명령어 탐정의 수색 순서
PATH는 콜론(:)으로 구분된 폴더 목록입니다. 예:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:...
ls를 치면 셸은 이 목록을 왼쪽부터 뒤집니다. /usr/local/sbin/ls가 있나? 없음. … /usr/bin/ls 발견 → 실행. 핵심 규칙은 하나입니다 — 앞에 있는 폴더가 이깁니다. 같은 이름의 프로그램이 두 폴더에 있으면, PATH에서 앞선 것이 실행됩니다.
반대로 ./run.sh처럼 /가 포함된 호출은 PATH를 거치지 않습니다. 경로가 명시되었으니까요. 이 대비가 오늘 공격과 방어의 축입니다.
2-3. PATH 인젝션 — 수색 순서를 오염시키기
공격의 구조는 이렇습니다.
① 공격자가 쓸 수 있는 폴더(예: /tmp/fake)에 가짜 ls를 놓는다
② 어떤 스크립트가 PATH 맨 앞이 오염된 환경에서 "ls"를 이름으로 호출한다
③ 셸의 수색이 /tmp/fake를 먼저 뒤져 가짜가 실행된다
④ 그 스크립트가 setuid나 sudo로 높은 권한이라면 → 가짜도 높은 권한으로 실행
조건이 두 개임에 주의하세요 — PATH가 오염되어 있고, 호출하는 쪽이 경로 없이 이름만 쓸 것. 둘 중 하나라도 깨지면 성립하지 않습니다. 그래서 방어법도 정확히 그 둘을 겨냥합니다.
2-4. sudo는 PATH를 믿지 않는다
이 공격이 역사적으로 유명해서 방어가 이미 곳곳에 박혀 있습니다. 그 대표가 sudo의 secure_path 설정 — sudo로 명령을 실행하면 사용자의 PATH를 버리고 /etc/sudoers에 적힌 안전한 목록으로 교체합니다. 그래서 현대 시스템에서 PATH 인젝션이 먹히는 문은 좁습니다 — sudo 설정 미스(env_keep으로 PATH 유지), 또는 스스로 PATH를 초기화하지 않는 setuid 스크립트·cron 작업이 주요 사냥감입니다.
3. 따라 하기
3-1. PATH 읽기 — 수색 순서 확인 (실측)
echo $PATH
which ls
type ls
출력 (2026-09-09 실측, WSL — 뒷부분은 환경별 경로라 생략·가공):
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:...
/usr/bin/ls
ls is /usr/bin/ls
읽는 법: 콜론으로 나뉜 목록의 왼쪽이 먼저 뒤져지는 폴더입니다. which는 "이름으로 치면 실제 어디가 실행되는가"를 보여 주고, type은 셸이 그 이름을 어떻게 해석하는지(외부 명령인지, 셸 내장인지, 별명인지)를 보여 줍니다. 지금은 /usr/bin/ls — 정상입니다. 이 두 명령이 오늘의 "전/후 대조" 도구입니다.
3-2. 가짜 명령어 만들기 (실측)
mkdir -p /tmp/fake
printf '#!/bin/bash\necho "속았지! 나는 가짜 ls다"\n' > /tmp/fake/ls
chmod +x /tmp/fake/ls
cat /tmp/fake/ls
출력 (2026-09-09 실측):
#!/bin/bash
echo "속았지! 나는 가짜 ls다"
읽는 법: 파일명이 ls일 뿐 내용은 무해한 echo입니다. chmod +x가 없으면 실행 파일로 취급되지 않아 공격이 성립하지 않습니다 — Step 23의 x 권한이 여기서 다시 나옵니다.
3-3. PATH 오염 — 가짜가 이기는 순간 (실측)
export PATH=/tmp/fake:$PATH
which ls
ls
출력 (2026-09-09 실측):
/tmp/fake/ls
속았지! 나는 가짜 ls다
읽는 법: which의 답이 바뀌었고, 실제로 ls가 가짜를 실행했습니다. $PATH 앞에 /tmp/fake:를 붙인 것이 전부입니다 — 수색 순서의 맨 앞을 장악한 것. 같은 두 글자 ls가 전혀 다른 프로그램이 되는 것을 직접 목격했습니다.
3-4. 절대경로는 흔들리지 않는다 (실측)
같은 오염 상태에서 경로를 붙여 호출해 봅니다.
/bin/ls /tmp/fake
출력 (2026-09-09 실측):
ls
읽는 법: 진짜 ls가 실행되어 /tmp/fake 안의 파일 목록(가짜 ls 하나)을 정상 출력했습니다. /가 들어간 호출은 PATH를 거치지 않으므로 오염과 무관합니다. 이것이 곧 방어법 1번입니다.
3-5. 취약한 스크립트 시나리오 재현 (실측)
이제 "높은 권한으로 돌아가는 스크립트" 역할을 만들어 공격을 완성해 봅니다. 관리자가 만든 점검 스크립트가 경로 없이 ls를 호출한다고 가정합니다.
printf '#!/bin/bash\nls /tmp/fake\n' > /tmp/fake/backup_check.sh
chmod +x /tmp/fake/backup_check.sh
echo "--- 정상 PATH ---"
/tmp/fake/backup_check.sh
echo "--- 오염된 PATH ---"
export PATH=/tmp/fake:$PATH
/tmp/fake/backup_check.sh
출력 (2026-09-09 실측):
--- 정상 PATH ---
backup_check.sh
ls
--- 오염된 PATH ---
속았지! 나는 가짜 ls다
읽는 법: 스크립트는 한 글자도 바꾸지 않았는데 결과가 갈렸습니다. 변수 하나가 프로그램의 행동을 바꿨습니다. 이 스크립트가 setuid이거나 sudo 규칙으로 실행되는 것이었다면, 속았지! 자리에는 공격자가 원하는 어떤 명령이라도 들어갈 수 있었습니다 — 그것이 PATH 인젝션의 위력입니다.
3-6. 원복 — 실험의 끝 (실측)
export PATH=${PATH#/tmp/fake:}
which ls
출력 (2026-09-09 실측):
/usr/bin/ls
읽는 법: ${PATH#/tmp/fake:}는 "PATH 앞의 /tmp/fake:를 떼어 내라"는 셸 문법입니다. 더 간단한 원복은 터미널을 닫고 여는 것 — export는 세션 안에서만 유효하니까요. 마지막으로 가짜도 지웁니다: rm -rf /tmp/fake (실행 전 경로 확인 — rm -rf는 되돌릴 수 없습니다).
3-7. sudo의 방어 확인 (실측과 설정 읽기)
grep -r secure_path /etc/sudoers /etc/sudoers.d/
출력 (2026-09-09 실측):
/etc/sudoers:Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
읽는 법: sudo는 사용자의 오염된 PATH를 버리고 이 목록으로 교체합니다. 참고로 실측에서 root가 sudo sh -c 'echo $PATH'를 실행하니 PATH가 유지되었습니다 — root는 이 초기화의 예외 대상입니다. 일반 사용자의 sudo에는 적용됩니다. 그래서 PATH 인젝션의 실전 표적은 sudo가 아니라 스스로 PATH를 정리하지 않는 setuid 스크립트와 cron 작업으로 좁혀집니다.
3-8. 공격 시나리오와 방어법 정리
오늘 실험을 공방 문서로 접습니다. 위키에 이렇게 적습니다.
### PATH 인젝션
- 공격 시나리오: 쓰기 가능 폴더에 가짜 명령 제작 → PATH 앞부분 오염 →
이름으로 명령을 호출하는 고권한 스크립트(setuid, cron)가 가짜를 실행
- 성립 조건: ① PATH 오염 ② 경로 없는 호출 — 둘 다 필요
- 방어법:
1. 스크립트에서는 항상 절대경로 (/bin/ls)
2. 스크립트 첫머리에 PATH를 안전한 값으로 초기화 (export PATH=/usr/bin:/bin)
3. sudo는 secure_path 유지, env_keep에 PATH를 넣지 않기
4. /tmp처럼 누구나 쓰는 폴더를 PATH에 넣지 않기 (특히 맨 앞에)
4. 미션과 연습문제
미션 — PATH 인젝션 전 과정 재현과 문서화
- 3-2~3-5를 재현합니다 — 가짜 명령 제작, PATH 오염,
which전후 대조, 취약 스크립트 실행 - 가짜 명령어를
ls가 아닌cat으로 한 번 더 만들어 같은 실험을 반복합니다 - 3-6으로 원복하고
/tmp/fake를 삭제합니다 - 3-8 형식의 공격·방어 정리 문서를 위키에 작성합니다 — 단, 방어법 2(PATH 초기화)는 실제로 취약 스크립트에 적용해 "오염 상태에서도 안전함"을 실험으로 확인한 뒤 적습니다
연습문제
문제 1. ls와 ./ls의 차이를 PATH 검색 관점에서 설명하고, 둘 중 PATH 인젝션에 취약한 쪽을 골라 보세요.
문제 2. export PATH=/tmp/fake:$PATH에서 $PATH를 뒤에 쓰지 않고 export PATH=/tmp/fake로만 설정하면 어떤 일이 벌어지나요?
문제 3. PATH 인젝션의 두 성립 조건을 말하고, 각 조건을 깨는 방어법을 하나씩 연결해 보세요.
문제 4. sudo의 secure_path가 있는데도 PATH 인젝션이 여전히 위협인 대상을 두 가지 말해 보세요.
5. 모범 답안과 완료 기준
미션 모범 답안
재현의 핵심 대조 (2026-09-09 실측 기준):
오염 전 which ls → /usr/bin/ls
오염 후 which ls → /tmp/fake/ls
취약 스크립트(오염 환경) → 가짜 실행
취약 스크립트 + 절대경로 수정(/bin/ls) → 오염 환경에서도 정상 실행
검증하는 법: ① which의 전후가 달라졌다가 원복 후 돌아오는가. ② 가짜 cat 실험도 성공했는가 — 기법이 ls에 특화된 것이 아님을 확인하는 절차입니다. ③ 방어 실험: 스크립트를 /bin/ls로 고치거나 첫 줄에 export PATH=/usr/bin:/bin을 넣은 뒤, 오염된 터미널에서 실행해도 가짜가 안 뜨는가. ④ /tmp/fake가 삭제됐는가.
연습문제 해답
문제 1 해답. ls는 PATH 목록을 앞에서부터 수색해 처음 발견된 것을 실행하므로 오염에 취약합니다. ./ls는 "현재 폴더의 ls"라는 경로 지정이라 PATH를 거치지 않아 인젝션과 무관합니다. 취약한 쪽은 ls(이름 호출)입니다.
문제 2 해답. PATH가 /tmp/fake 하나뿐이 되어 /usr/bin 등을 못 찾습니다 — ls, grep 같은 거의 모든 외부 명령이 command not found가 됩니다. 터미널이 마비되니, PATH를 바꿀 때는 항상 기존 값($PATH)을 이어 붙이는 형태여야 합니다. (마비되면 터미널을 닫고 새로 여세요 — 세션 한정이므로 복구됩니다.)
문제 3 해답. 조건 ① PATH 오염 → 방어: 스크립트 첫머리에서 PATH 초기화, sudo secure_path. 조건 ② 경로 없는 호출 → 방어: 스크립트에서 항상 절대경로(/bin/ls)로 호출. 둘 중 하나만 깨도 공격은 실패합니다.
문제 4 해답. ① 스스로 PATH를 초기화하지 않는 setuid 스크립트·프로그램, ② 사용자 환경을 물려받아 실행되는 cron 작업(특히 사용자 crontab). 둘 다 sudo를 거치지 않으므로 secure_path의 보호 밖에 있습니다.
완료 기준 체크리스트
- [ ] 환경변수를
export와echo $이름으로 쓰고 읽을 수 있다 - [ ] PATH가 "왼쪽이 우선"인 검색 목록임을 설명할 수 있다
- [ ]
which와type의 차이를 안다 - [ ] 가짜 명령어를 만들어 PATH 조작으로 끼워 넣는 실험을 재현했다
- [ ]
${PATH#접두사}원복 또는 세션 재시작으로 돌아올 수 있다 - [ ] 절대경로 호출이 PATH 인젝션을 무력화함을 실험으로 확인했다
- [ ] 미션: 공격·방어 정리 문서를 위키에 완성했다
6. 흔한 실수와 해결
벽 1. 가짜를 만들었는데 진짜가 실행된다
증상: PATH를 바꿨는데도 정상 ls가 뜹니다.
원인: ① 가짜에 chmod +x를 안 했거나(실행 불가라 건너뜀), ② /tmp/fake를 PATH 맨 앞이 아니라 뒤에 붙였거나, ③ export한 터미널과 실행하는 터미널이 다릅니다.
해결: which ls가 /tmp/fake/ls를 가리키는지부터 보세요. 가리키는데도 진짜가 뜨면 실행 권한 문제입니다 — 셸은 실행 불가능한 후보를 조용히 건너뜁니다.
벽 2. PATH를 통째로 덮어써서 명령이 전부 죽었다
증상 (출력 예시):
bash: ls: command not found
원인: export PATH=/tmp/fake처럼 기존 값을 이어 붙이지 않았습니다.
해결: 당황하지 마세요 — export는 세션 안에서만 유효합니다. 터미널을 닫고 새로 열면 끝입니다. 같은 창에서 고치려면 절대경로로 셸 문법을 쓸 수 있습니다: /usr/bin/env는 살아 있습니다.
벽 3. export했는데 다른 창에서는 안 통한다
증상: 옆 터미널에서 which ls를 하면 정상입니다.
원인: 환경변수는 프로세스(그 셸)와 그 자식에게만 전달됩니다. 다른 터미널은 다른 프로세스입니다.
해결: 버그가 아니라 환경변수의 정체입니다. "환경변수의 범위 = 그 프로세스와 자손"이라고 외워 두세요 — 이 성질이 오늘 실험을 안전하게 만든 장치이기도 합니다.
벽 4. 취약 스크립트가 오염 환경에서도 정상 동작한다
증상: 가짜가 안 뜨고 진짜 결과가 나옵니다.
원인: 스크립트 안의 호출이 /bin/ls 같은 절대경로이거나, 스크립트가 자체적으로 PATH를 초기화합니다.
해결: 그 스크립트는 이미 방어된 것입니다 — 3-5의 재현은 "경로 없는 호출"인지를 먼저 확인하세요. 실전에서도 PATH 인젝션의 첫 절차는 대상 스크립트의 호출 방식을 읽는 것입니다.
벽 5. 실험 후 정리를 잊는다
증상: 다음 날에도 /tmp/fake와 오염된 PATH 습관이 남아 있습니다.
원인: 3-6 건너뛰기.
해결: 실험의 마지막 명령은 항상 원복과 삭제입니다. 특히 PATH 오염을 .bashrc 같은 시작 파일에 써 두는 실험은 하지 마세요 — 매 로그인마다 가짜가 뜨는 함정을 스스로에게 설치하는 것입니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 환경변수 | 프로세스에 붙어 자식에게 전달되는 이름=값 목록 |
| PATH | 명령어 이름을 찾아 헤매는 폴더 목록 — 왼쪽이 우선 |
| 이름 호출 vs 경로 호출 | ls는 PATH 수색, /bin/ls·./ls는 직접 지정 |
| PATH 인젝션 | PATH 앞부분을 오염시켜 가짜 명령을 끼워 넣는 공격 |
| 성립 조건 | ① PATH 오염 + ② 경로 없는 호출 — 둘 다 필요 |
| secure_path | sudo가 사용자 PATH를 버리고 쓰는 안전 목록 |
| 세션 한정 | export는 그 터미널과 자식에게만 — 창을 닫으면 원상태 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
echo $PATH |
검색 순서 보기 |
export PATH=/tmp/fake:$PATH |
PATH 맨 앞에 폴더 끼우기 (실험용) |
export PATH=${PATH#/tmp/fake:} |
끼운 폴더 떼어 내기 (원복) |
which 명령 |
이름으로 치면 실제 뭐가 실행되는지 |
type 명령 |
셸이 그 이름을 어떻게 해석하는지 |
env |
환경변수 전체 목록 |
grep secure_path /etc/sudoers |
sudo의 PATH 방어 설정 확인 |
명령어보다 중요한 감각
오늘의 교훈은 "환경이 곧 행동"이라는 한 줄입니다. 같은 스크립트가 환경변수 하나에 다른 프로그램이 됩니다 — 공격자가 시스템을 볼 때 파일만 보는 것이 아니라 설정과 환경을 함께 보는 이유입니다. 방어 쪽의 감각은 더 간단합니다. 스크립트를 쓸 때 명령 앞에 경로를 붙이는 손가락 — /bin/ls. 그리고 다른 사람의 스크립트를 읽을 때 "이 호출이 이름인가, 경로인가"를 자동으로 체크하는 눈. Step 106의 setuid와 오늘의 PATH가 만나면 권한 상승의 고전 조합이 됩니다. 다음에 setuid 파일이나 cron 스크립트를 볼 때, 첫 질문은 이것이어야 합니다 — "이 안의 명령들, 경로로 쓰여 있는가?"
전부 체크되면 Step 108 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.