안녕하세요! 오늘은 드림핵(Dreamhack)의 Simple Note Manager 문제를 풀어보며, 수많은 삽질 끝에 깊게 공부하게 된 리눅스 쉘의 명령어 치환 기법과 이를 제어하기 위한 백슬래시(\) 이스케이프의 핵심 원리를 공유해 보려고 합니다.
1. 주요 소스코드 분석
① 파일 백업 처리 함수
def backup_notes(timestamp):
with lock:
#쓰기 모드로 실행
with open('./tmp/notes.tmp', 'w') as f:
f.write(repr(notes))
# 취약한 부분: {timestamp} 자리에 내가 넣은 값이 그대로 들어감!
subprocess.Popen(f'cp ./tmp/notes.tmp /tmp/{timestamp}', shell=True)
backup_notes 함수는 시스템 명령어를 실행하기 위해 subprocess.Popen을 호출합니다. 이때 인자로 들어가는 timestamp 변수가 포맷 스트링(f’…‘)을 통해 그대로 결합되는데, 결정적으로shell=True 옵션이 켜져 있습니다. 즉, timestamp 내부에 리눅스 메타문자가 섞여 들어오면 그대로 OS 명령어로 실행되는 구조입니다.
@app.route('/backup_notes', methods=['GET']) # GET메소드
def get_backup_notes():
print(len(notes), flush=True)
if len(notes) == 0:
abort(404)
page = render_template('backup_notes.html')
resp = make_response(page)
resp.set_cookie('backup-timestamp', f'{time.time()}') #현재시간의cookie가져옴
return resp
@app.route('/backup_notes', methods=['POST'])
def post_backup_notes():
if len(notes) == 0:
abort(404)
backup_timestamp = request.cookies.get('backup-timestamp', f'{time.time()}') #백업 버튼을 누르면 가져온 쿠기값을 backup_timestamp에 저장
if not isinstance(backup_timestamp, str):
abort(400)
backup_notes(backup_timestamp)# backup_timestamp 값을 가지고 backup_notes실행, 즉 backup_timestamp에 스크립트 삽입
return redirect(url_for('get_index'))
/backup_notes 경로에 POST 요청을 보낼 때, 서버는 사용자가 전송한 backup-timestamp 쿠키 값을 그대로 가져와 backup_notes() 함수의 인자로 전달합니다.
즉, 우리가 변조해야 할 핵심 타깃(공격 벡터)은 일반적인 Form 데이터나 URL 파라미터가 아니라 ‘쿠키(Cookie) 값’이라는 사실을 알 수 있습니다. 단, 백업을 진행하려면 메모(notes)가 최소 1개 이상 존재해야 하므로 사전에 선행 작업이 필요합니다.
2. 가설 설정
1. 사전 조건 충족: 메모 생성 엔드포인트를 호출하여 len(notes) == 0 조건을 우회합니다.
2. 쿠키 변조 공격: backup-timestamp 쿠키에 원격 명령 실행(RCE) 구문을 삽입하여 POST 요청을 전송합니다.
3. 데이터 추출: curl 명령어를 활용하여 시스템 내부 명령의 결과값(cat flag)을 외부 RequestBin URL의 Body 데이터로 실어 보냅니다.
사용하는 curl 옵션:
-X: POST 메소드를 지정하고 요청할 URL을 지정하기 위한 옵션 입니다.
-H: 전송할 헤더를 지정 할 수 있는 옵션입니다. (이 문제에서는 400 오류를 피하기 위해 사용하였습니다.)
-d: body 데이터를 기재하는 옵션입니다.
(뒤에 따옴표 없이 특수문자(중괄호와 백틱)가 오면 공백( )을 기준으로 인자(Argument)를 쪼개니 주의 하셔야 합니다.)
3. 핵심 원리: 명령어 치환과 로컬 PC 쉘의 함정
이 문제를 풀 때 가장 많은 고전과 시행착오를 겪게 만드는 주범은 바로 “명령어 치환” 메커니즘입니다. 이 문제의 스크립트를 만들기 위해 저는 다음과 같은 문법에 대해 찾아보며 공부하였습니다.
-
$(): 명령어 치환이라는 특수 기능을 가지고 있는 문자입니다. -
` 백틱(``) ` : 백틱은 처번 문제에서 소개한바와 같이 명령어 치환이라는 특수기능을 가지고 있습니다.
-
제가 겪은 거대한 삽질과 함정입니다.
실패하는 명령어 예시
curl -X POST -H "Cookie: $( 헤더지정 -d {``})"
저는 위와 같은 구조로 스크립트를 작성하였습니다. 그 결과 서버 설정 까지는 명령이 잘 들어 갔지만 가져오는 값은 제가 생각한 값들이 아니었습니다.
출력: {cat
이유는 공격 대상인 문제 서버가 아니라 ‘내 로직을 입력한 내 로컬 PC’가 이 명령어를 먼저 읽어 실행했기 때문이었습니다. 내 PC의 쉘이 전처리 과정에서 $()와 백틱을 먼저 실행해 버린 것입니다. 내 PC에는 flag 파일이 없으므로 빈 값({}) 처리되어 최종적으로 문제 서버에는 아무런 명령도 없는 껍데기 요청만 전달된 것 입니다.
4. 해결 열쇠: 백슬래시() 이스케이프 (Escape)
백 슬래쉬(\): 이 친구를 찾기 참 힘들었습니다. 이 함정을 파쇄하기 위해 필요한 것이 바로 백슬래시()입니다. 흔히 줄 바꿈이나 경로 구분자로 쓰이지만, 쉘 환경에서는 뒤에 오는 특수 메타문자의 기능을 일시적으로 증발시키고 단순한 ‘문자열’로 격하시키는 이스케이프 기능을 수행합니다.
이스케이프 작동 원리 예시
echo date # 출력: 2026. 06. 17. (명령어 실행됨)
echo `date` # 출력: date (단순 문자열로 전달됨)
내 PC의 쉘이 명령어를 가로채지 못하도록 치환 문자 앞에 백슬래시()를 전부 붙여서 전송해야, 비로소 그 기호들이 훼손되지 않고 원본 그대로 드림핵 문제 서버 내부 subprocess.Popen(shell=True) 까지 도달하게 됩니다!
댓글 남기기