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. 대응 방안: Secure Coding
가장 확실한 방법은 ‘쉘’ 이라는 중간 관리자를 배재하는 것 입니다!
직접 구성한 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를 활용해 “어떻게 하면 이 사이트가 정상 구동되면서도 내가 발견한 취약점을 완벽히 막을 수 있을까?”를 치열하게 고민해 보았습니다.
물론 아직은 필터링 우회 기법이나 복잡한 쉘과 관련된 문법에 서툴러 더 다양한 공격 시나리오를 구성하는 데 한계가 있었지만, 방어자의 입장에서 코드를 다시 써본 경험은 더 넓은 시야를 가지게 된 무엇과도 바꿀 수 없는 소중한 자산이 었습니다.
피드백은 언제나 환영입니다!
부족한 글이지만 끝까지 읽어주셔서 감사합니다.
포스팅 내용 중 잘못된 정보가 있거나, 더 효율적인 방어 대책이 있다면 언제든지 댓글를 활용해 피드백 부탁드립니다.
여러분의 소중한 의견이 저에게는 큰 배움의 기회가 됩니다!
댓글 남기기