본문 바로가기
CTF, 워게임 문제 풀이/Try Hack Me

THM: Race Conditions

by secumark 2025. 6. 11.
728x90

 

Jr Penetration Tester > Introduction to Web Hacking > Race Conditions

 

Race Condition 공격도 보안기사에 자주 등장하는 용어 중 하나다. 

 

웹 애플리케이션 취약점분석을 하면서 10달러 짜리 기프트 카드 하나를 10번 연속으로 쓸 수 있다거나 하는 취약점을 발견할 수도 있다. 

 

컴퓨터 프로그램에서 이벤트가 발생하는 timing의 차이로 프로그램의 동작이나 결과가 달라지는 것을 레이스컨디션이라고 부르며, 일반적으로 하나의 변수에 여러 threads가 동시에 접근, 수정하려고 할 때 발생한다. 이 스레드간 적절한 lock mechanism, synchronization이 없으면 공격자가 이를 악용해 여러 번의 할인을 적용하는 등의 조작이 가능해짐

 

이번 레이스컨디션 취약점에서 배울 것

1. Burp Suite의 Repeater로 race condition 공격하는 법

2. 스레드와 멀티 스레딩

3. State Diagrams에 대해 배운다.
(State Diagram은 처음 들어봄)

 

멀티스레딩

프로그램과 프로세스의 정의를 커피 레시피에 비유하여 설명함..

 

프로그램: 특정 작업을 수행하기 위한 명령어들의 집합. 원하는 작업을 하려면 그 프로그램을 실행해야 하는데 실행하지 않으면 프로그램은 아무것도 하지 않고, 정적인 명령어의 집합으로만 남아있음

프로세스: 레시피에 따라서 커피를 만드는 그 단계 하나하나, 과정을 말함. instructions에 따라 executing을 하면서 중단이 될 수도 있고, 다른 일을 할 수도 있음 (흔함) 예시의 Flask 코드를 실행하면 하나의 프로세스가 생성되고, 8080번 포트에서 외부 연결을 기다리는 상태가 된다. 대부분은 waiting 상태에 머무르고 있음. 사용자가 http get 요청을 보내면 프로세스가 ready 상태로 전환되어 cpu 스케줄링에 따라 실행을 기다림. CPU가 해당 프로세스에 시간을 할당하면 Running 상태가 되어 HTML 페이지를 사용자에게 전송한 후 다시 Waiting 상태로 돌아감. (waiting - ready - running ~ 반복)

 

** 서버의 관점에서 볼 때, 이 애플리케이션은 클라이언트 요청을 순차적으로 처리한다. 한 번에 하나의 요청만 처리한다는 뜻. 

(위에서 flask 예시가 있었는데, flask는 버전 1.0부터는 멀티스레드로 동작하나 --without-threads 인자를 사용해 일부러 single-threaded 방식으로 실행되도록 함)

 

스레드: 두 개의 포타필터가 있을 때 (원두 담을 때 쓰는 도구) 에스프레소 주문이 들어오면 하나만 사용해서 추출을 하는데, 또 다른 손님이 동시에 주문하면 나머지 한개의 포타필터도 쓰게 된다. 이때 예열된 에스프레소 머신이 하나의 프로세스, 이 프로세스 내에서 작동하는 포타필터를 스레드라고 칭한다. = 작업 단위로 동시에 처리가 가능해지고 프로세스와 여러 메모리 영역, 명령어를 공유함 즉 자신이 속한 프로세스의 코드, 데이터, 파일 디스크립터 같은 것들을 공유하다보니 빠르고 가벼운데 메모리 충돌, 경쟁 조건(race condition) 문제가 생길 수 있음

 

수천 명의 사용자에게 동일한 페이지 또는 맞춤형 페이지를 제공하는 웹 서버의 경우 1. 직렬처리:  하나의 프로세스만 실행되며, 한 명의 사용자 요청을 순차적으로 처리해 새 요청은 대기열에 쌓임 2. 병렬처리: 하나의 프로세스가 실행 중이고 새로운 사용자 요청마다 스레드를 생성하여 처리함. 실행 중인 스레드 수가 최대치에 도달했을 때만 요청이 대기열에 쌓임 (자원 관리 중요)

 

이전 앱은 python용 웹서버인 gunicorn을 사용해 4개의 스레드로 실행될 수 있다. (WSGI HTTP서버로 웹 서버랑 Python 웹앱을 연결해주는 인터페이스임, 예를 들어 아파치와 플라스크 간 연결) 특히 Gunicorn은 여러 개의 worker processes를 생성해 동시에 여러 요청 처리 가능. gunicorn을 실행하면서 --workers=4 옵션을 주면 4개의 worker processes를 사용하겠다!라는 뜻이다. 즉 클라이언트 요청을 병렬로 처리할 수 있게 된다. --threads=2는 각 worker processes가 2개의 스레드를 생성할 수 있다는 뜻으로,

4 workers * 2 threads = 8개의 동시 요청 처리가 가능해진다.

source: tryhackme.com

예시는 다음과 같다.

 

즉 이 프로세스는 tcp 포트 8080에 바인딩되어 동일한 프로세스를 두 개 실행하는 것은 불가능하다 (하나의 tcp 포트는 한 번에 하나의 프로세스만 사용. localhost:8080에 프로세스 a가 바인딩되어 있으면 프로세스 b는 같은 포트 사용 x)

 

또한 tcp, udp 포트는 하나의 프로세스에만 연결될 수 있어 포트 하나에 여러 프로그램이 동시에 bind 되진 않는다 (포트 충돌)

마지막으로 하나의 프로세스는 원하는 만큼의 threads를 구성할 수 있고, 8080포트로 들어오는 HTTP 요청들은 각각의 스레드로 분배된다 (예, 1개의 웹 서버 프로세스가 10개의 threads를 가지고 있음. 이때 10명의 사용자가 동시에 요청해도 각 요청이 다른 스레드에서 병렬로 처리됨) 

결론 : 포트 하나당 하나의 프로세스만 사용할 수 있지만, 그 프로세스 안에서는 여러 개의 스레드가 요청을 나눠 처리할 수 있다.

 

 

Real World Analogy (실생활에서 비유해보기)
- TOCTOU (Time-of-check to time-of-use)

일반적으로 경쟁 상태의 가장 흔한 원인은 ‘공유 자원(shared resources)’에 있다. 여러 스레드가 동시에 같은 공유 데이터를 접근하고 수정할 때가 대표적인 예. 공유 데이터의 예로는 데이터베이스의 레코드, 메모리에 있는 데이터 구조 등이 있다.

 

경쟁상태의 주요 원인 3가지

1. 병렬 실행 : 웹 서버가 동시에 여러 사용자의 요청을 처리하기 위해 병렬로 요청들을 실행할 수 있음. (적절한 동기화 없이..)
2. DB 운영: 읽기-수정-쓰기(read-modify-write) 같은 작업을 동시에 수행할 때 (이때 해결책은 lock mechanism, transaction isloation)
3. 서드파티 라이브러리 & 서비스 : 오늘날 웹 앱은 third-party libraries, APIs, 그리고 외부 서비스와 자주 통합됨. 이 외부 구성요소들이 동시 접근을 제대로 처리하지 못한다면, 여러 요청이나 작업이 동시에 작동할 때 경쟁 상태가 발생할 수 있다.

 

Web Application Architecture

Client-Server Model (클라이언트-서버 모델) - 웹 애플리케이션이 따르는 모델
- Client (클라이언트): 서비스를 요청하는 프로그램, 애플리케이션 (웹 페이지를 보려면 웹 브라우저가 웹 서버에 해당 페이지(파일)를 요청함)
- Server (서버): 이런 요청을 받아서 서비스를 제공하는 프로그램 또는 시스템.

예) 웹 브라우저(클라이언트)에서 웹 서버로 HTTP GET 요청을 보내면 요청한 HTML 페이지를 전송함. 

이러한 클라이언트-서버 모델은 네트워크를 통해 동작한다. 클라이언트가 네트워크를 통해 요청을 보내면 서버에서 요청을 받고 처리해서 필요한 리소스를 다시 보냄

 

웹 애플리케이션의 전형적인 구조는 다중 계층 구조. 논리를 여러계층으로 분리하는.. 일반적으로는 표현 계층, 응용 계층, 데이터 계층 세 가지로 나뉜다.

1. 표현 계층: 이 계층이 클라이언트 측의 웹 브라우저에 해당. (HTML, CSS, Java Script 코드를 해석하고, 화면에 렌더링)
2. 응용 계층: 웹앱의 핵심 로직, 기능이 들어있음 (데이터 계층과 상호 작용)

3. 데이터 계층: 애플리케이션의 데이터를 저장하고 조작함 (db 작업, 데이터 생성, 수정 삭제) 보통 MySQL이나 PostgreSQL 같은 데이터베이스 관리 시스템(DBMS)을 사용하여 구현됨


결국 웹 애플리케이션은 클라이언트 → 서버 → 데이터베이스의 흐름으로 작동하는데, 이 과정에서 여러 사용자의 요청이 동시에 발생하고, 서버가 이를 적절히 관리하지 못하면!? 경쟁 상태 같은 보안 취약점이 생길 수 있다는 것임

 

States

해당 교육에서는 세 가지 예시에 대해서 다룸 
- 돈 이체를 검증하고 수행하는 과정
- 쿠폰 코드를 검증하고 할인 적용하는 과정
- 돈 이체 검증 및 실행
(생략함)

 

Burp Suite를 사용해서 공격 진행해보기

이동통신사에 속한 한 웹 애플리케이션에 휴대폰 요금 이체 기능이 들어있음. 이 웹앱이 race condition 취약점이 존재하는지 확인(잔액보다 더 많은 돈을 이체해 악용할 수 있는지)


1. 먼저, 대상 웹 애플리케이션이 HTTP 요청을 어떻게 받고, 어떤 방식으로 응답하는지를 분석해야 함. 이때 burp suite의 proxy를 이용한다. 현재 사이트에서는 post 요청을 사용하고 있어서 요청과 응답이 어떻게 처리되는지 살펴볼 수 있음.


HTTP History 탭에는 모든 요청, 응답 코드가 상세하게 기록되어 있음.

 

2. 제시된 두 계정 중 하나의 계정으로 로그인 해서 Pay & Recharge 버튼을 클릭


금액을 다양하게 입력하면서 시스템이 각 상황에 어떻게 응답하는지 확인해본다. 


10달러를 입력했더니 이런 오류가 뜬다. 

 

3. 현재 밸런스보다 적은 금액을 이체하면 다음과 같이 성공적인 것을 확인할 수 있다.

 

4. 그럼 이제 request 부분의 POST 요청을 Repeater로 보내보겠다.

 

 

1이 방금 만든 request인데, 이를 포함한 그룹 하나를 생성하라고 한다. 

 

그리고 duplicate tab을 해서 20번 추가로 복제하라고 함 (총 21개 만들어짐)

 

Request Group 전송 방식
1. Send group in sequence (single connection): 한 개의 TCP 연결을 통해 순차적으로 모든 요청을 전하고, 마지막 요청 후 연결을 종료함. 클라이언트 측 desync(비동기 동기화 오류) 취약점 테스트에 유용함... 모든 요청이 같은 연결을 공유하기 때문에 서버 입장에서 상태가 연속되는 요청으로 보일 수도 있음

2. Send group in sequence (separate connections)
요청 하나 보낼 때마다 새로운 TCP 연결을 생성, 요청 전송 후 연결 종료하고 그 다음 요청도 새로운 연결로 동일한 방식 반복. 이 방법은 일반적인 동작 방식과 유사하며, 레이스 컨디션(race condition) 테스트에 자주 사용됨.

 

3. Send group in parallel

여러 요청을 동시에 병렬로 전송.

 

실습에서는 2번, 3번 사용


레이스 컨디션 공격 시, 단일 패킷으로 전송되는 요청이 더 빠르게 처리될 수 있고, 패킷이 여러 개로 분할되지 않으면 처리 지연이 적고, 성공 확률이 더 높아질 수 있다고 말하는 중..

 

그룹 요청을 병렬로 보내는 옵션을 선택하면 리피터가 그룹 내 모든 요청을 한 번에, 동시에 전송함

21개의 패킷이 0.5밀리초 이내에, 동시에 서버에 도착하는.. (타이밍 우회가 가능해지는, 이때 21개 요청이 모두 성공적이니까 크레딧 이체가 21번 되는 거)

 

Wireshark에서 패킷을 분석해보면 병렬 전송을 하면 각 요청이 12개 패킷으로 나뉘어서 전송되고, 순차 전송은 요청당 10개 패킷만 사용되는 것을 확인했다. 원인은 바로 burp suite의 repeater의 동기화 방식 때문. 병렬 전송을 위해 요청 도착 타이밍을 맞추기 위한 트릭을 사용하는데, http/2 이상이면 하나 tcp 패킷 안에 여러 요청을 담아서 전송하는 거고, HTTP/1일 경우 
Last-byte synchronization 기법, 즉 병렬 기법을 사용한다...  Burp는 각 요청의 마지막 바이트를 잠시 보류하고, 모든 요청의 나머지 바이트를 전송한 후, 마지막 바이트들을 동시에 전송. 이렇게 하면 서버는 모든 요청을 거의 동시에 받은 것처럼 처리됨

-> 요청이 2개의 패킷으로 분리됨. 첫 번째는 대부분의 데이터, 두 번째는 마지막 바이트! 

 

어쨌든 해당 공격으로 병렬 공격을 해봤더니 flag 값을 얻을 수 있었음.. 

그래서 2번 공격이랑 3번 공격을 쓰라는건가? 

 

일단 실습에선 2번 방법이 쓰임 Send group in sequence (separate connections) 21개의 별도 tcp 세션으로 생성되었으니.. 단, 경쟁 조건 취약점 공격에 사용되는 방식은 '동시성 충돌'을 노리는 공격의 특징이 있는 병렬 공격, 3번이다. 그래서 2번 방식으로 먼저 실험을 해서 wireshark에서도 성공, 실패 여부를 보고 레이스 컨디션의 존재 가능성을 확인한 후 3번 방식으로 공격을 시도한다. 

 

 

Detection and Mitigation

Detection

race condition 공격은 subtle vulnerabilities, 정교한 취약점에 사용될 수 있음. (xss, sql 인젝션처럼 한줄만으로 공격이 가능한 것과는 달리) 

 

Mitigation

1. Synchronization Mechanisms (동기화 메커니즘)

현대 프로그래밍 언어는 락(lock)과 같은 동기화 메커니즘을 제공한다. 락을 얻은 스레드만 공유 자원에 접근할 수 있고, 락을 해제하기 전까지 다른 스레드는 해당 자원에 접근할 수 없다.
> 하나의 스레드만 자원에 접근하도록 강제하면 race condition 방지에 효과적

 

2. Atomic Operations (원자적 연산)

원자적 연산은 더 이상 쪼갤 수 없는 불가분의 실행 단위로, 다른 스레드의 간섭 없이 한 번에 실행되는 것이 특징. 작업이 중단되지 않고 완전하게 끝날 수 있도록 보장함 -> 무결성

 

3. Database Transactions (데이터베이스 트랜잭션)

트랜잭션은 여러 데이터베이스 작업을 하나의 단위로 묶는다. 따라서 트랜잭션 안의 모든 작업은 전부 성공하거나 전부 실패 둘 중 하나여야 함. 데이터 일관성을 보장하고, 여러 프로세스가 동시에 데이터를 수정할 때 발생할 수 있는 경쟁 조건을 방지. (rollback, commit, begin 이런거) -> 여러 사용자가 동시에 DB를 조작해도 정합성 유지함

 

실습

 

드디어 실습..

 

세 가지 계정을 제시하고 있다. 계정 중 하나가 잔고에 1,000달러 이상이 되도록 만들어야 한다고 함.

 

 

blueapple, whiteHorse, greenOrange 세 계정 모두 100달러씩 가지고 있다. blueapple 계정으로 로그인을 해서 whitehorse나 greenorange에 100달러를 총 10번 동시에 보내는 공격을 burp suite를 이용해서 진행해보도록 하겠다. (이전에 했던 실습 활용)

 

 

 

 

근데 수수료가 5% 있어서; 11번 보내면 되겠다.

 

프록시로 보면 다음과 같이 100달러를 보내는게 fund_being_transferred, 5달러의 calculatedfee, 5달러의 receiver_amount가 있다. 

 

총 11개 send

 

아 send 전에 꼭 parallel로 설정해주기 그냥 send 누르면 저 선택된 1번 탭만 한번 실행되는 거다.

 

 

 

response는 200 ok다.

 

근데 이게 프로세싱에서 더이상 넘어가지 않는다.

 

참고로 옆에 보면 following redirections가 있는데, 

request에 location: ~이 있으면

follow redirection 선택시 burp가 location 위치로 get 요청을 추가로 보낸다. 즉, 리디렉션을 따라가며 최종 응답을 확인할 수 있음

 

 

1번

2번 누르니까 200ok가 나옴

 

*참고로 Burp의 Repeater 탭은 수동 요청 실험용으로 이걸로 요청을 보내면 브라우저 화면에는 변화가 없음.
브라우저에 있는 웹 페이지는 여전히 서버와 독립적으로 동작 중이다. 200 OK는 "서버가 요청을 받고 정상적으로 처리했다"는 의미일 뿐 이 요청이 실제로 잔액 전송 완료!라는 의미가 아님. 서버가 처리 중간에 응답을 미리 보낼 수도 있고 (또는 프론트엔드가 Ajax로 비동기 처리 중이라서 응답을 아직 렌더링하지 않았을 수도 있음) 

 

새로고침을 해봤더니 다음과 같이 돈이 그대로다... 흠

 

 

근데 다시 똑같이 해서 intercept off를 했는데 transaction successful이 뜬다.

 

Zavodni Stav 계정으로 로그인했더니 385달러가있다. (근데 100달러 11번 보냈는데 왜 385달러만 들어있지)

 

이번엔 warunki에게 385달러를 3번 보내본다.

 

(중간에 오류가 계속 나서 다시 할 예정..)

 

 

--> 4일만에 다시 시도

새롭게 알게된 사실은 burp suite repeater에서 duplicate하고 send를 한 후, intercept off를 하면 Burp가 직접 서버로 요청을 보내고, 동작하는 거라고 한다. 이 repeater의 작동방식이 어떻게 되는지 좀 애매했는데.. 일단 다시 시도해본다.

 

 

90달러를 11번 send 했다.

 

 

;;;

 

계속 안돼서 몇십번을 시도했다.. 10달ㄹㅓ를 50번 보냈더니 result에 true가 드디어 떴다

 

근데 이번엔 processing이 안사라짐; 아 ㅡㅡ 근데 다시 로그인하니까 계좌 잔액이 0이 되어있다. 이게무슨

 

 

356달러가 있으니까.. 한 4번 정도만 보내보자

 

;;

warunki 계좌에 가니까 100달러였는데 432가 들어있다;

ㅋㅋㅋ ㅠㅠ

 

1시간을 해도 해결이 안됨

 

 

----

 

아예 다 terminate 시키고 다시 시도했다.

 

그리고 내가 실습한 환경의 경우는 repeater로 공격할 때 일부 request가 internal server error가 떠서 잘 전송이 안돼서, duplicate을 좀 많이 해줬다.

 

그리고 아까처럼 transaction failed라고 뜨던데 그 이유는 마지막에 request를 보낸게 response 값으로 false가 떴기 때문이다.

 

Zavodni Stav 계정으로 로그인해보니 송금이 잘 된 것을 확인할 수 있었다.

 

 

zavodni 계정으로는 딱 10번 request했다.

 

ㅠㅠ... 이번 한번만 더 해보고 안되면 포기하려고 했는데.. 다행이다. 실습까지 다 끝나는데 10시간은 걸린 거 같다.. 덕분에 repeater 기초사용법은 빠삭하게 익혔다..

 

728x90

'CTF, 워게임 문제 풀이 > Try Hack Me' 카테고리의 다른 글

THM: Intro to Cross-site Scripting  (0) 2025.06.08
THM: Intro to SSRF  (0) 2025.06.08
THM: File Inclusion  (0) 2025.06.07
THM: IDOR  (0) 2025.06.05
THM: Authentication Bypass  (0) 2025.06.03

댓글