
패치에서 RCE까지: CVE-2010-4221(ProFTPD TELNET IAC 스택 오버플로)용 수제 익스플로잇, 실패 중심의 전체 과정 문서화 포함
완전히 재현 가능한 실습 환경과 CVE-2010-4221 — ProFTPD의 pr_netio_telnet_gets()에서 발생하는 인증 전 스택 버퍼 오버플로우 — 를 위한 직접 작성한(raw-socket) 익스플로잇입니다. 취약점 연구와 익스플로잇 개발 학습 목적으로 제작되었습니다.
모든 실패는 기록되어 있습니다. 성공 경로는 거짓이며, 우회로가 곧 교훈입니다.
이 저장소는 교육용 산출물입니다. 멘토나 교육을 받을 여유가 없는 사람들이 메모리 손상 익스플로잇이 실제로 어떻게 탄생하는지 — 패치에서 시작해 실패를 거쳐 자신이 소유한 실습 환경 안에서 동작하는 개념 증명에 이르기까지 — 배울 수 있도록 존재합니다.
레드팀 작업 — 실제 전문적인 공격 보안 — 은 한 단어로 정의됩니다: 승인(authorization). 전문가가 하는 모든 일은 서면 계약 안에서 이루어집니다: 범위, 대상, 허용된 기법, 시간 창, 승인한 사람들을 명시한 서명된 Rules of Engagement 문서 말입니다. 그 서류가 없다면, 동일한 키 입력은 직업이 아니라 — 지구상의 사실상 모든 관할권에서 범죄입니다.
따라서 이 저장소에 대한 계약은 다음과 같으며, 협상 불가입니다:
이 기술은 배울 가치가 있습니다. 이 기술은 그에 따르는 규율이 있을 때만 가치가 있습니다.
ProFTPD는 FTP 제어 채널에서 TELNET 이스케이프 시퀀스를 처리합니다. TELNET에서 0xFF(IAC, "Interpret As Command")는 이스케이프 바이트이며, 리터럴 는 로 전송됩니다.
0xFF0xFF 0xFFpr_netio_telnet_gets()는 클라이언트 바이트를 스택 버퍼(pr_cmd_read의 char buf[PR_DEFAULT_CMD_BUFSZ+1], glibc의 MAXPATHLEN=4096 기준 4104바이트)에 복사하면서 남은 공간을 buflen — size_t, 부호 없는(unsigned) — 으로 추적합니다.
취약한 경로(1.3.3a, netio.c):
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // 쓰기 #1
buflen--; // 감소 #1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // 쓰기 #2
buflen--; // 감소 #2 <-- 사이에 검사 없음
두 번의 쓰기, 두 번의 감소, 그 사이에 0 검사가 없습니다. buflen이 정확히 1일 때, 이 쌍은 이를 0으로 감소시킨 후 SIZE_MAX(1800경)로 언더플로우됩니다. 이제 루프는 버퍼가 무한하다고 믿고 공격자가 제어하는 바이트를 스택 위로 계속 씁니다 — 저장된 레지스터, 저장된 RBP, 반환 주소를 덮어씁니다.
인증 전(pre-auth). 이 함수는 USER/PASS가 처리되기 전에 실행됩니다.
수정(커밋
3cc69b8388,
"Bug#3521 - Telnet IAC processing stack overflow", 1.3.3c에서 릴리스)은
12줄입니다. 전체 보안 경계는 다음과 같습니다:
if (buflen == 0) {
break;
}
patch.diff를 참조하십시오. 패치를 읽으면 상처가 어디 있었는지 알 수 있습니다 — 그것이 기술입니다.
Dockerfile은 역사적인 Debian 스냅샷 소스에서 ProFTPD 1.3.3a를 컴파일하며, 의도적으로 안전하지 않게 만듭니다(이것이 2010년의 모습입니다):
-fno-stack-protector — 카나리 없음-z execstack — 실행 가능한 스택(NX 없음)-no-pie — 고정된 바이너리 주소gdb 아래에서 실행 — 기본적으로 ASLR을 비활성화 → 결정적 스택docker build -t proftpd-133a .
docker rm -f lab133 2>/dev/null
docker run -d --name lab133 --cap-add SYS_PTRACE \
--security-opt seccomp=unconfined -p 127.0.0.1:2122:21 \
proftpd-133a sh -c 'gdb -batch -ex "set follow-fork-mode child" \
-ex "run" -ex "continue" --args /usr/local/sbin/proftpd -n -d1 \
> /tmp/gdb.txt 2>&1; sleep 600'
python3 exploit.py
예상 출력:
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(셸코드는 fd 0, 1, 2에 씁니다. 어느 것이 제어 채널을 담당하는지 알 필요가 없도록 하기 위함입니다 — 그중 두 개가 응답합니다.)
"SITE " + NOP 슬레드 + 셸코드 + [\xff\xff 플러드] + 패딩 + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
셸코드는 명령 버퍼 내부에 존재 — 오버플로우 꼬리는 단 하나의
아무도 건드리지 않는 영역 주소만 전달
buf[4102]만 건드려집니다(잘림 NUL). 프레임의 활성 로컬 변수 아래는 모두 조용합니다.buflen을 언더플로우로 몰아갑니다(패리티 문제는 "여정" 참조).pr_cmd_read가 파싱 후 return 0에 도달하면 CPU는 슬레드에 착지하여 셸코드로 미끄러져 들어갑니다.이 역전된 구조 — 페이로드 먼저, 플러드 둘째, 주소 마지막 — 은 동일한 레이아웃을 사용하는 표준 Metasploit 모듈(proftp_telnet_iac)에 대해 검증되었습니다. 해당 타깃은 NX가 있었으므로 res 포인터의 "4중 역참조"를 가진 ROP 체인이 필요했습니다. 우리 실습 환경은 실행 가능한 스택이 있으므로 단일 직접 반환으로 충분합니다.
최종 익스플로잇은 60줄입니다. 그 비용은 다음과 같습니다:
맹목적인 \xff 플러드 → 아무 일도 없음. 서버가 정중하게 세션을 종료했습니다. 근본 원인: buflen은 4102(짝수)에서 시작하고 각 IAC 쌍은 2씩 감소하므로 — 0에 깨끗하게 도달하며, 결코 1에 도달하지 않습니다. 언더플로우는 패리티가 깨져야 합니다. 교훈: 무작위 전송보다 상태 머신을 읽는 것이 낫습니다.
잘못된 버퍼 크기. 첫 번째 보정 시도는 1024바이트 버퍼를 가정했습니다. 실제는 Linux/glibc에서 MAXPATHLEN+8 = 4104입니다. 플러드는 타깃에서 3KB 부족하게 멈췄습니다. 교훈: 타깃을 측정하십시오. 타깃을 가정하지 마십시오.
첫 번째 SIGSEGV. 순환 de Bruijn 패턴(Aa0Aa1...)이 ret 슬롯을 buf+4152에 배치했으며, 두 번 교차 검증되었습니다(프레임 계산 + 패턴 오프셋). 교훈: 순환 패턴은 줄자이지 익스플로잇이 아닙니다.
RIP 제어. 슬롯을 0x4141414141414141로 설정하면 ret 명령 자체가 충돌했습니다 — x86-64는 비정규(non-canonical) 주소를 거부하며, 결함은 ret 위에 발생하고 우리 값은 백트레이스에 남습니다. 교훈: 프레임에 우리 값이 있는 상태에서 ret에서 충돌 = 제어권 확보.
ret 슬롯 위의 셸코드 → 덮어써짐. 힙 포인터(0x4d7838 — 이후 cmd_rec 풀 할당으로 식별)에 의해 8바이트가 덮어써졌습니다. 슬롯 위의 스택 프레임은 착지와 하이재킹 사이에 계속 작동하는 함수들에 속합니다. 교훈: 오버플로우가 마지막 쓰기가 아닙니다. 프로그램은 당신이 훼손한 스택 위에서 계속 살아있습니다.
슬롯 아래의 "죽은 영역" → 역시 훼손됨. pr_cmd_read의 자체 로컬 변수(cmd, buflen, cp)가 바로 거기에 있으며 파싱 중 계속 저장됩니다.
하드웨어 워치포인트 포렌식. gdb에서 watch *(long*)ADDR는 미스터리를 카메라로 바꿨습니다: 덮어써진 주소에 대한 모든 쓰기와 백트레이스가 순서대로 기록됩니다. 교훈: "누가 이 메모리를 썼는가?"라는 질문에 대한 답은 gdb 명령 하나면 충분합니다.
참고 자료를 읽되, 먼저 이해하십시오. 표준 모듈이 역전된 구조를 확인해 주었습니다. 자신의 정신적 모델을 구축한 후에 다른 익스플로잇을 읽는 것은 학습입니다. 그 전에 읽는 것은 복사입니다.
size_t는 음수가 되지 않습니다 — 엄청나게 커질 뿐입니다. 남은 공간 카운터의 정수 언더플로우는 단계만 추가된 스택 오버플로우입니다.\x0a(읽기를 종료)를 피하고 설계상 \xff(TELNET 이스케이프)를 견딥니다.argv[0]의 11바이트 차이(빌드 트리 바이너리 vs 설치된 바이너리)는 전체 스택을 0x40만큼 이동시켜 완벽한 익스플로잇을 조용히 무효화했습니다.modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)diegslva가 공개적으로 학습하며 제작 — "익스플로잇을 한 번도 작성해 본 적 없음"에서 손수 제작한 셸코드를 사용한 인증 전 RCE까지, 기록된 하루 만에. 이 저장소가 당신에게 무언가를 가르쳐 주었다면, 그 지식을 나누어 주십시오.