너무 별거 아니지만... 이번에 HTB Cyber Apocalypse 2024에서 풀었던 문제 중 트릭이 생소한 문제여서 write up을 써보려고 합니다
medium으로 나온 문제이지만 난이도 자체는 많이 쉬운 문제입니다. 문제 풀 때는 정신이 없어서 원리고 나발이고 그냥 툴 써서 풀었는데, 끝나고 분석해보니 나름 재밋기도 하고 분석해본 겸 작성합니다.
사용된 CVE가 두 개인데, CVE-2022-39227에 대한 내용을 주로 담고 있습니다.
1. 문제 구성
API 서버가 컨셉이고, 세 가지 API를 사용할 수 있습니다.

/api/v1/get_ticket: jwt token 발급/api/v1/chat/{chatId}: chat 내용 확인 가능, token 필요/api/v1/flag: flag 확인, admin token 필요
2. CVE-2023-45539
token 발급을 위해 /api/v1/get_ticket 을 접근해보면, 아래와 같이 Forbidden이 발생합니다.

haproxy.cfg에서 관련 설정을 확인할 수 있습니다. 여기서 HAProxy는 소프트웨어 로드 밸런서입니다. 이중화에도 용이하게 사용된다고 하네요
global
daemon
maxconn 256
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend haproxy
bind 0.0.0.0:1337
default_backend backend
http-request deny if { path_beg,url_dec -i /api/v1/get_ticket }
backend backend
balance roundrobin
server s1 0.0.0.0:5000 maxconn 32 check
/api/v1/get_ticket 경로가 deny 된 것을 볼 수 있는데요, 저는 여기서 저 문자열만 bypass하면 될 것 같아서 /api/v1/./get_ticket 으로 요청했고, 실제로 잘 되었습니다만...

이게 원래 이렇게 푸는 게 아니고 CVE-2023- 45539을 사용해서 푸는 거였다고 하네요
CVE-2023- 45539는 HAProxy의 룰 검사 중 #을 URL로 인식해서 위와 같은 설정을 bypass하는 문제였다고 합니다. (URL에서 #은 원래 fragment로 취급되어서 # 뒷부분은 URL의 경로에 영향을 미치지 않음)
가령 중요 경로 하위는 .jpg .gif 같은 static 확장자만 허용했는데, 아래와 같이 요청하는 일이 발생하는 것이죠.
http://victim.com/admin/index.html#.png
이렇게 하면 룰 검사를 bypass하고, 실제 요청은 # 뒤가 fragment 처리 되면서 /admin/index.html을 향하게 되므로 중요 정보가 노출될 수 있다는 문제였습니다.
원래 의도대로 풀면 아래와 같습니다.

3. 그래서 이제 뭐함
이제 얻은 token으로 뭘 할 수 있을까요? /api/v1/chat/{chatID} 로 채팅들을 보는데... 내용도 너무 많고 문제는 이 chat 내용이 DB에서 오는 것도 아니고 json으로 저장된 파일을 불러오는 겁니다. 배포 파일에도 공개가 되어있어서 비밀 내용은 아닌 것 같아요. file leak이라도 해야하나 했는데 chatID를 숫자만 불러오기 때문에 쉽지 않습니다.

따라서 flag와는 관련 없는 내용같아서 패스하고, /api/v1/flag를 사용하려면 어떻게 해야하는지 확인해봅시다.
/api/v1/flag 를 사용하려면 token이 admin인지 확인을 하던데 어떻게 확인하는지 살펴봅시다.
간단하게 role과 user가 주어지며, 기본적으로 생성되는 token은 role이 guest로 구성됩니다.
# python_jwt==3.3.3 as jwt
@api_blueprint.route('/get_ticket', methods=['GET'])
def get_ticket():
claims = {
"role": "guest",
"user": "guest_user"
}
token = jwt.generate_jwt(claims, current_app.config.get('JWT_SECRET_KEY'), 'PS256', datetime.timedelta(minutes=60))
return jsonify({'ticket: ': token})
token의 role을 검사해서 administrator인지 확인하는 것을 볼 수 있습니다.
# authrize_roles('administrator')
def authorize_roles(roles):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
token = request.headers.get('Authorization')
if not token:
return jsonify({'message': 'JWT token is missing or invalid.'}), 401
try:
token = jwt.verify_jwt(token, current_app.config.get('JWT_SECRET_KEY'), ['PS256'])
user_role = token[1]['role']
if user_role not in roles:
return jsonify({'message': f'{user_role} user does not have the required authorization to access the resource.'}), 403
return func(*args, **kwargs)
except Exception as e:
return jsonify({'message': 'JWT token verification failed.', 'error': str(e)}), 401
return wrapper
return decorator
4. CVE-2022-39227
여기서 token을 생성하고 관리하는 라이브러리는 python-jwt입니다. 문제에는 3.3.3 버전이 사용되었죠.
검색해보니 3.3.3 이하 버전에는 CVE-2022-39227 취약점이 있다고 하는데, 놀랍게도 secret key 없이 권한 상승이 가능하다고 합니다.
대회 진행 중에는 잘 나온 poc가 있어서( https://github.com/user0x1337/CVE-2022-39227 ) 그거 사용해서 후딱 풀었는데, 여기서는 소개만 해드리고 원리에 대해서 알아봅시다.
GitHub - user0x1337/CVE-2022-39227: CVE-2022-39227 : Proof of Concept
CVE-2022-39227 : Proof of Concept . Contribute to user0x1337/CVE-2022-39227 development by creating an account on GitHub.
github.com
JWT는 header/payload/signature 세 가지가 . 으로 구분되어 있습니다.

공격자는 header와 payload 사이에 변조한 payload를 넣어 signature를 속임과 동시에 변조한 payload를 보낼 수 있습니다. 어떻게 진행되는지 확인해봅시다.
우선 발급 받은 token을 .으로 나누고, payload 부분을 base64 decode 해줍니다.

그리고 decode된 payload 에 role 부분을 강제로 변경하여 변조된 payload를 만듭니다.

이를 다시 base64 encoding합니다..

아래와 같이 합치면 완성입니다! 이렇게 보면 이게 왜 되는지 모르겠죠
{"header.modify_payload":"", "protected":"header", "payload":"payload", "signature":"signature"} 이렇게 된 모습입니다.

위에서 나온 값을 이쁘게 보면 아래와 같습니다. 이 값을 중괄호 포함하여 전부 넣어야 합니다. 이런 JWT 본 적 있으신가요

JWT는 flattened 으로 표현하는 방식이 있는데,
{
"header":{...},
"payload":{...},
"signature":{...}
}
이렇게 표현하는 방식을 flattened이라고 합니다. 저희가 원래 알던 JWT는 compact 방식이라고 합니다.
일부 라이브러리들은 flattened와 compact 둘 다 채용하는데 그 중 jwcrypto의 JWS 라이브러리가 해당됩니다.
JWS는 signature를 검증, 생산하는 라이브러리입니다. 여기서 JWS가 갑자기 왜 나오냐
python-jwt는 거의 모든 로직을 jwcrypto에 의존하고 있기 때문입니다. 그 중 JWS도 사용하고요. import 되는 부분을 보면 알 수 있습니다. 주로 봐야 할 부분은 JWS와 json_decode 부분입니다.
from jwcrypto.jws import JWS, JWSHeaderRegistry
from jwcrypto.common import base64url_encode, base64url_decode, \
json_encode, json_decode
아래는 python-jwt의 verify_jwt 함수입니다. 가져온 token을 . 으로 구분하고, 이를 json_decode 하고 있습니다.

그리고 아래에서 JWS를 사용해서 signature의 유효성을 검사합니다.

그럼 JWS의 코드를 보면서 어떻게 검사하는지 확인해봅시다.
flattened 방식이 먼저 수행되고, 이 과정에서 입력된 값의 signature와 protected, payload를 사용하여 값을 검증하는 것을 볼 수 있습니다. 즉 그 외의 필드는 그냥 무시합니다. (protected는 일반 header보다 signature 검증에 대한 정보를 좀 더 담고 있지만, 거의 같은 내용입니다. 알고리즘 종류 등..)
# flattened 방식
try:
djws = json_decode(raw_jws)
if 'signatures' in djws:
o['signatures'] = []
for s in djws['signatures']:
os = self._deserialize_signature(s)
o['signatures'].append(os)
self._deserialize_b64(o, os.get('protected'))
else:
o = self._deserialize_signature(djws)
self._deserialize_b64(o, o.get('protected'))
if 'payload' in djws:
if o.get('b64', True):
o['payload'] = base64url_decode(str(djws['payload']))
else:
o['payload'] = djws['payload']
# compact 방식
except ValueError:
data = raw_jws.split('.')
if len(data) != 3:
raise InvalidJWSObject('Unrecognized'
' representation') from None
p = base64url_decode(str(data[0]))
if len(p) > 0:
o['protected'] = p.decode('utf-8')
self._deserialize_b64(o, o['protected'])
o['payload'] = base64url_decode(str(data[1]))
o['signature'] = base64url_decode(str(data[2]))
self.objects = o
따라서 변조된 부분 중 아래의 부분만 사용하게 됩니다. 그리고 이 부분은 원래의 token의 header, payload, signature를 사용해서 구성했습니다. 따라서 검증에 아무 문제가 없습니다.

그럼 이 token을 python-jwt에서는 어떻게 구분할까요?
아까 처음 부분에서 .으로 구분하는 것을 확인했습니다.

따라서 아래와 같은 일이 일어납니다. 신기하죠

이러면 base64 decoding할 때 문제가 생기지 않을까? 싶을텐데요
base64url_decode(jwcrypto의 함수)는 결론적으로 base64 라이브러리의 urlsafe_b64decode를 사용하는데, urlsafe_b64decode는 base64의 문자가 아니라면 무시한다고 합니다. (그냥 b64decode도 해당)

5. solve + 여담

이렇게 풀면 되는 문제였습니다. 알기만 하면 난이도가 굉장히 낮죠
사실 이 라이브러리는 많이 쓰이지도 않고 이젠 개발이 중단된 것 같아서 사실 파급력이라고 할 건 없을거같아요. 문제용으로는 재밋는듯
이런 취약점도 있더라 하는 느낌으로 분석했습니다 재밋잖아요 하하
'write-up > CTF' 카테고리의 다른 글
| [codegate2025 qual] Web write-up (0) | 2025.04.03 |
|---|---|
| [BlackHat MEA qual CTF 2024] WEB all write-up (0) | 2024.09.03 |
| [CCE 2024 Qual] ccend write-up (0) | 2024.08.12 |
| [TFC CTF 2024] write-up (0) | 2024.08.06 |
| [justCTF 2023] write-up (0) | 2023.06.06 |
댓글