Dreamhack : BypassIF 문제를 통해 보는 시스템 쉘(shell) 호출의 위험성
Dreamhack : BypassIF 문제를 통해 보는 시스템 쉘(shell) 호출의 위험성
오늘은 Dreamhack 사이트의 BypassIf 문제를 통해 시스템 쉘(shell) 호출의 위험성에 대해 분석하고 어떻게 방어할 수 있는지에 대해 분석한 내용을 소개하려고 합니다.
1. 문제 접근 방법
먼저 제가 어떤 방법으로 이 문제를 접근해서 풀었는지 궁금하실 수도 있으시니 빠르고 간단하게 설명해 보겠습니다!
- 시스템이 어떻게 작동하는지 이해하려고 노력합니다.
- 저는 항상 전체 코드를 분석하고 가설을 설정하는데 이때 목표지점부터 출발합니다.
(다음 목차에서 가설 설정하는 방법을 더 자세히 보여드릴게요!)
- github Codespaces를 통해 가상 서버에서 2단계를 시도합니다.
제가 가상 서버에서 먼저 공격을 시도하는 이유는 가상환경을 만들면서 “어! 이러한 설정 오류는 xss와 같은 보안 취약점으로 이어질 수 있을 것 같은데” 와 같이 설정 오류가 어떠한 보안 사고를 일으킬 수 있는지 공부할 수 있어서입니다!
- 검증된 가설을 실제 서버에 적용합니다.
2. Bypassif 가설 설정 : 역추적을 통한 공격 설계
1. 목표 : flag 구하기
2-1. 목표: cmd_input 값 비우기
2-2. 목표: 관리자 키 구하기
3-1. 목표: cmd_input값 받지 않기
3-2. 목표: timeout 발생시키기 & cmd_input 값에 명령어 입력하기
저는 전체 코드를 분석하여 시스템이 어떻게 작동되는지 이해한 후 가설을 설정하여 문제에 접근합니다!
3. 취약점 코드 분석 : 시스템 쉘의 위험성!
@app.route('/flag', methods=['POST'])
def flag():
#중간 코드 생략
if cmd != '' or key == KEY:
#cmd 명령을 입력했거나 key를 찾으면 통과
if not filter_cmd(cmd):
#cmd 명령어가 필터링에 걸리지 않으면 통과
try:
output = subprocess.check_output(['/bin/sh', '-c', cmd], timeout=5)
#/bin/sh와 같은 시스템 쉘을 직접 호출
return render_template('flag.html', txt=output.decode('utf-8'))
except subprocess.TimeoutExpired:
return render_template('flag.html', txt=f'Timeout! Your key: {KEY}')
#timeout이 발생하면 key 값을 얻을 수 있다!
핵심 코드 스니펫
output = subprocess.check_output(['/bin/sh', '-c', cmd]
이 코드가 바로 제가 소개하고 싶은 시스템 쉘 호출 코드입니다! 우선 이 코드가 어떻게 작동하는지 알려드리겠습니다.
작동원리
그럼 어떻게 될까요?
그렇다면 필터링되지 않은 ls 명령어를 보면 어떻게 생각할까요?
shell은 "아! 현재 디렉터리의 파일과 폴더를 보여줘야지"라고 이해하게 됩니다!
왜냐하면 이것이 바로 shell 의 역할이기 때문입니다.
가상 서버 실습: 쉘 호출 취약점 시나리오
이해를 위해 쉘 호출 취약점에 대한 예시 공격 상황을 소개해 드리겠습니다.
공격을 시도하기 전, GitHub Codespaces를 활용해 실제 환경과 유사한 실습 환경을 구축했습니다.
가상 서버 환경
먼저 서버 내부에 우리가 보호해야 할 flag.txt가 정상적으로 존재하는지 확인합니다
[위 사진은 저의 가상 서버에 flag.txt 파일이 있는 모습입니다]
공격 시나리오
사용자 입력: ls
쉘의 동작: 입력받은 문자열을 명령어로 인식하여 파일 목록을 출력!
결과 분석: 아래 이미지처럼 flag.txt 파일의 존재가 노출됩니다. 공격자는 이를 통해 서버 내부에 어떤 민감한 데이터가 있는지 파악할 수 있습니다.
ls 명령어 실행 결과
[위 사진은 ls 명령어를 입력한 모습입니다]
시스템 쉘 직접 호출의 위험을 보여주는 또 다른 예시
[touch 명령어를 주입하여 script 파일을 생성하는 과정]
[가상 서버 내부에 script 파일이 성공적으로 생성된 결과]
이번 실습에서는 취약점의 핵심을 직관적으로 보여드리기 위해 필터링 우회 없이 ls와 touch라는 두 가지 기본 명령어만 사용했습니다.
하지만 실제 공격 상황에서는 이보다 훨씬 치명적인 시나리오가 가능합니다.
만약 악의적인 사용자가 필터링을 우회하여 쉘 메타 문자를 자유롭게 주입할 수 있다면, touch 명령의 -d나 -t 옵션을 활용하여 파일의 접근 및 수정 시간을 과거로 조작할 수도 있습니다.
4. 대응 방안: Suri code
가장 확실한 방법은 ‘쉘’이라는 중간 관리자를 배제하는 것입니다!
직접 구성한 Codespaces 환경에서 테스트해 본 안전한 코드를 소개해 보겠습니다.
기존 코드에서 변경된 로직
1. 블랙리스트 -> 화이트 리스트 방식으로 변경
저는 크게 2가지의 로직을 변경했습니다. 그중 첫 번째는 화이트리스트를 기반으로 명령어를 필터링하여 알려지지 않은 위협으로부터 보호할 수 있도록 하였습니다.
def filter_cmd(cmd):
# 허용할 명령어 목록 (화이트리스트 방식으로 변경)
allowed_commands = ['ls', 'id', 'pwd', 'whoami']
2. 쉘을 거치치 않고 직접 실행(shell=False)
if cmd != '':
if not filter_cmd(cmd):
try:
# [핵심 수정] shell=False를 사용하고 리스트 형태로 전달
# 이렇게 하면 세미콜론(;) 등을 이용한 추가 명령 실행이 불가능
args = cmd.split()
output = subprocess.check_output(args, shell=False, timeout=5)
return render_template('flag.html', txt=output.decode('utf-8'))
except subprocess.TimeoutExpired:
# [핵심 수정] 타임아웃 발생 시 KEY를 노출하지 않음
return render_template('flag.html', txt="Error: Request Timeout")
보안을 강화하기 위한 두 번째 핵심 조치로 shell=False 옵션을 활용했습니다.
기존 방식은 사용자의 입력을 쉘이 직접 해석하기 때문에 위험했었습니다.
하지만 수정된 코드에서는 python args = cmd.split()을 통해 입력값을 리스트 형태로 나눕니다.
즉 시스템은 첫 번째 요소인 ls만 실행 파일로 인식하고, ‘;’, ‘rm’ 등은 단순한 파일 이름으로 취급합니다. 결과적으로 시스템 인젝션 공격은 무력화되며, 시스템은 “그런 이름의 파일은 없다” 는 에러를 내뱉으며 안전하게 보호됩니다.
5. 마치며
이번 BypassIF 문제를 풀면서, 저는 스스로가 이제 출발선에 섰다는 것을 다시 한번 겸손하게 느낄 수 있었습니다. 코드를 해석하는 데 생각보다 많은 시간이 소요되었고, 생소한 모듈들을 마주할 때마다 하나하나 검색하며 파악해야 했습니다.
하지만 1시간, 2시간 그 후로 서버를 몇 번이고 재 생성했는지 모를 정도로 끈기 있게 코드를 분석하고, 저만의 가설을 세워 마침내 플래그를 획득했을 때의 기쁨은 마치 군 생활 중 첫 휴가를 나왔을 때만큼이나 짜릿하고 행복했습니다.
문제를 해결한 뒤 그대로 넘어가면 공부한 내용을 금방 잊을 것 같아, GitHub Codespaces를 활용해 “어떻게 하면 이 사이트가 정상 구동되면서도 내가 발견한 취약점을 완벽히 막을 수 있을까?”를 치열하게 고민해 보았습니다.
물론 아직은 필터링 우회 기법이나 복잡한 쉘과 관련된 문법에 서툴러 더 다양한 공격 시나리오를 구성하는 데 한계가 있었지만, 방어자의 입장에서 코드를 다시 써본 경험은 더 넓은 시야를 가지게 된 무엇과도 바꿀 수 없는 소중한 자산이었습니다.
피드백은 언제나 환영입니다!
부족한 글이지만 끝까지 읽어주셔서 감사합니다.
포스팅 내용 중 잘못된 정보가 있거나, 더 효율적인 방어 대책이 있다면 언제든지 댓글을 활용해 피드백 부탁드립니다.
여러분의 소중한 의견이 저에게는 큰 배움의 기회가 됩니다!
댓글 남기기