먼저 웹 애플리케이션에서 경쟁 조건이 어떻게 일어나는지 보자.
웹 애플리케이션은 Client-Server Model을 따르는데, 간단하게 아래와 같다.
Client : 서비스를 위해 리퀘스트를 만드는 프로그램이나 애플리케이션 ex) 웹 서버로부터 리퀘스트한 웹 페이지를 받아 서비스함
Server : 던진 리퀘스트를 받아 리스폰스를 통해서 서비스해주는 프로그램이나 시스템 ex)HTTP GET 리퀘스트를 받고 HTML 페이지를 리스폰스로 보냄
일반적으로는 client는 네트워크를 통해 리퀘스트를 보내고, server는 이를 받아 처리하고 리소스를 다시 보낸다.
이는 한 계층이 아니라 Multi-Tier 구조를 따른다.
가장 일반적 설계는 3계층을 사용하는데
Presentation tier : 웹 애플리케이션에서 클라이언트 측에서 웹 브라우저로 구성된다. HTML, CSS, JS 코드를 렌더링한다.
Application tier : 비즈니스 로직과 기능을 포함한다. 리퀘스트를 수신, 처리하며 데이터 계층과 상호작용한다. Node.js, PHP 같은 서버 프로그래밍 언어를 사용해 구현된다.
Data tier : 데이터를 저장하고 조작한다. 데이터베이스 작업의 CRUD가 포함되며 DBMS를 사용해 수행된다.
비즈니스 로직에서 상태는 송금이 됐는지 안됐는지, 쿠폰이 적용 상태인지 미적용 상태인지처럼 이상적으로는 두 가지 상태를 예상할 수 있다. 하지만 서비스를 설명하거나 실제로 서비스를 할 때 두 가지 상태로는 사용자의 많은 시나리오를 커버하기에는 부족하다.
송금이 가능한지 검사하는 상태인지, 쿠폰 적용이 가능한 상품을 선별하는 상태인지 등의 중간 상태를 필요로 할 수 있다. 이는 애플리케이션의 개발 상태에 따라서 수 없이 많은 상태를 요구할 수도 있다는 말이다.

쿠폰을 적용하기까지 위와 같은 프로세스의 로직이 있다고 가정하면 Coupon applied가 될 때까지 4개의 상태를 거친다는 것을 알 수 있다.
물론 위 타임라인에서는 쿠폰이 있고, 쿠폰을 적용할 수 있는 제품이라는 가정에서 쿠폰이 적용될 때까지의 시간 흐름에 따라서 순서가 존재한다.
Coupon applied로 상태가 변경되지 않는 한 반복의 통제가 없어 보인다. 그렇다는 것은 쿠폰이 여러번 적용될 수 있다는 가능성을 보인다.
마찬가지로 송금 프로세스의 상태도 유사하다.
미송금, 송금의 두 상태가 이상적으로 보이지만 비즈니스 로직을 생각하면 셋 이상의 상태는 필수적이다.

극단적으로 간단한 상태 다이어그램이지만 우리가 생각하기에 송금 프로세스에 필요한 상태는 셋으로 충분해보인다.
이 서비스에도 "시간"이라는 프로세스의 취약한 사실이 존재한다. 하지만 아무리 취약한 웹 애플리케이션의 서버에도 프로세스가 돌아가는 시간은 매우 짧다.
위 프로세스를 악용하려면 리퀘스트가 동시에 서버에 도달해야 하고, 그 간격은 단 몇 밀리초에 가깝다. 이를 Time Window라고 할 수있다.
Burp Suite를 통해서 짧은 Time Window에서 중복된 요청이 서버에 도달하도록 해보자.
두 사용자에 대한 정보를 가정해보자

웹 애플리케이션에서는 이동통신사가 휴대폰 이체를 제공한다.
이 데모에서는 경쟁 조건이 취약점이 실제로 존재하는지 확인하고 예시에서 말했던 잔액보다 큰 이체 금액을 송금해서 취약점을 악용해보자.

User1에 로그인하고 이체를 시도해보자.

로그인 리퀘스트를 가로채보니 User1이 맞다.
User2의 아이디가 휴대폰 번호이므로 1.5달러를 이체해보자.


POST 리퀘스트를 잡아서 확인해보면 입력한 정보와 리다이렉션 주소, 성공 상태가 모두 정상적이다

Repeater로 보내서 보낸 리퀘스트를 20개 복제해 한 그룹에 넣어주자.

separate connections로 시도해보면

몇가지 요청은 잘 된 것 같으나 몇 가지는 잘 안된 것 같다.
Parallel 방식으로 보내보자.

각 리퀘스트는 비슷한 시간에 마무리됐고 각 패킷의 크기도 12로 같았다.


패킷을 보내는 크기가 다른 것을 확인했는데, 왜일까?
그룹으로 보낸 HTTP 리퀘스트의 문서를 보면 Parallel로 전송할 때 Repeater는 리퀘스트가 대상에 짧은 시간 내에 도착하도록 동기화하기 위해서 많은 기술이 존재하는데

[FIN, ACK], [FIN, ACK]의 두 패킷으로 전송된 요청을 볼 수 있다.
같은 작업을 User2에서 수행했더니 1달러 남짓 남았던 User1의 잔고가 보낸만큼 늘어나있었다.

그럼 15달러를 10번 User2에게 보낼 수 있을까?

Repeater에서 amount를 15로 바꾼 후 parallel로 보냈더니 리다이렉션이 문제 없이 갔다.

하지만 뭔가 패킷로스가 난건지 몇 번은 보내고 몇 번은 가지 않았나보다.

다시 시도해보니 이번엔 15달러씩 11번 총 150달러가 보내진 것 같다.

문제는 순서였다.
리퀘스트를 Repeater로 복사하고 10번을 복제해서 그룹화한 후 보냈는데, 이는 1달러를 10개 복사한 후 amount를 15로 고치니 당연하게도 15x11달러가 아니라 15+1x10달러인 25달러가 송금된 것이다.
그래서 Repeater에서 amount를 먼저 15로 고친 후 리퀘스트를 복사해서 11개의 리퀘스트로 그룹을 만들었더니 15x11달러가 송금돼 -81달러가 남아있던 것이다.
와중에 서버가 다운돼서 다시 시도했더니

됐다 !
추가적으로 송금이 실패했던 케이스가 있었는데,
계좌에 만약 10달러가 있었다면, 처음 리퀘스트를 날릴 때 1달러로 송금하고 Repeater에서 리퀘스트를 10개 복사 후 15달러로 amount를 수정해 보낼 때 처음 시도하는 리퀘스트에서 계좌 잔액이 15달러 미만이었기 때문에 리퀘스트 그룹 자체가 송금 실패로 보내지지 않았던 것이다.
때문에 복제할 리퀘스트에 금액을 수정할 때는 송금하고 싶은 금액이 송금할 계좌의 잔액보다 크면 안됐던 것이다.