드림핵 Web Hacking-7일차

지선·2023년 8월 1일

드림핵WebHacking

목록 보기
8/12

SSRF(Server-Side Request Forgery)

웹 서비스의 요청을 변조하는 취약점, 웹 서비스의 권한으로 변조된 요청 보낼 수 O
->SSRF취약점을 통해 웹 서비스 권한으로 내부망(관리자페이지) 서비스 이용할 수 O

SSRF 취약점 예제 코드

# pip3 install flask requests # 파이썬 flask, requests 라이브러리를 설치하는 명령
# python3 main.py # 파이썬 코드를 실행 명령

from flask import Flask, request
import requests
app = Flask(__name__)

@app.route("/image_downloader")
# /image_downloader 엔트포인트
def image_downloader():
	# image_downloader 함수 생성
    image_url = request.args.get("image_url", "") # image_url 값을 받아옴
    response = requests.get(image_url) # requests 라이브러리를 사용해서 image_url URL에 HTTP GET 메소드 요청을 보내고 response에 저장
    return ( 
        response.content, # HTTP 응답으로 온 데이터
        200, # 응답 코드 (200은 요청 성공)
        {"Content-Type": response.headers.get("Content-Type", "")}, # HTTP 응답으로 온 헤더 중 Content-Type
    )
    
@app.route("/request_info")
# /request_info 엔드포인트
def request_info():
	# request_info 함수 생성
    return request.user_agent.string # 접속하는데 사용된 브라우저(user-agent)의 정보 return 
    
app.run(host="127.0.0.1", port=8000)

문제점

http://127.0.0.1:8000/image_downloader?image_url=http://127.0.0.1:8000/request_info

image_downloader 엔드포인트에 request_info 엔드포인트 경로 입력
입력하면 "http://127.0.0.1:8000/request_info"에 HTTP 요청 보내고 응답 반환
->브라우저 정보: python-requests/라이브러리버전
을 반환
접속하는데 사용된 브라우저의 정보가 아닌 python-requests가 출력된 이유:
웹 서비스에서 HTTP 요청을 보냈기 때문

즉, 웹서비스에서 사용하는 마이크로서비스의 API주소를 알아낸 후 image_url에 주소를 전달하면 외부에서 접근할 수 없는 마이크로서비스의 기능 사용 가능

웹 서비스 요청 URL에 이용자 입력값이 포함되는 경우

INTERNAL_API = "http://api.internal/"
# api.internal: 회사 내부에서 사용되는 api
# INTERNAL_API = "http://172.17.0.3/"

@app.route("/v1/api/user/information")
# /v1/api/user/information 엔드포인트
def user_info():
	user_idx = request.args.get("user_idx", "")
    #user_idx를 받아와서 user_idx에 저장
	response = requests.get(f"{INTERNAL_API}/user/{user_idx}")
    # HTTP GET 메소드 요청을 보내고 response에 저장
    # url의 예시: http://172.17.0.3/v1/api/user/information?user_idx=1
    # ->웹서비스는 http://172.17.0.3/user/1에 요청 보냄
@app.route("/v1/api/user/search")
# /v1/api/user/search 엔드포인트
def user_search():
	user_name = request.args.get("user_name", "")
    # user_name 받아와서 user_name에 저장
	user_type = "public"
    # user_type은 public
	response = requests.get(f"{INTERNAL_API}/user/search?user_name={user_name}&user_type={user_type}")
    # HTTP GET 메소드 요청을 보내고 response에 저장
    # url의 예시: http://x.x.x.x/v1/api/user/search?user_name=hello
    # ->웹서비스는 http://api.internal/user/search?user_name=hello&user_type=public에 요청 보냄

문제점

../

user_info 함수의 user_idx에 ../search 입력하면 (Path Traversal)
http://api.internal/search에 요청 보냄

#

user_search 함수의 user_name에 secret&user_type=private#를 입력하면
http://api.internal/search?user_name=secret&user_type=private#&user_type=public에 요청 보냄
# 뒤에 붙은 문자열은 API 경로에서 생략

웹 서비스의 요청 Body에 이용자의 입력값이 포함되는 경우 예시 코드

# pip3 install flask
# python main.py
from flask import Flask, request, session
import requests
from os import urandom
app = Flask(__name__)
app.secret_key = urandom(32)
INTERNAL_API = "http://127.0.0.1:8000/"
header = {"Content-Type": "application/x-www-form-urlencoded"}


@app.route("/v1/api/board/write", methods=["POST"])
# 엔드포인트가 /v1/api/board/write이고, post 메소드인 경우
def board_write():
    session["idx"] = "guest" # session idx를 guest로 설정
    title = request.form.get("title", "") # title 값을 form 데이터에서 가져옴
    body = request.form.get("body", "") # body 값을 form 데이터에서 가져옴
    data = f"title={title}&body={body}&user={session['idx']}" # 전송할 데이터
    response = requests.post(f"{INTERNAL_API}/board/write", headers=header, data=data) # INTERNAL API에 이용자가 입력한 값을 HTTP BODY 데이터로 사용해서 요청
    return response.content # INTERNAL API 응답 결과 반환
    # 이용자의 입력값을 HTTP Body에 포함, 내부API로 요청 보냄, 세션 정보는 guest 계정
    
@app.route("/board/write", methods=["POST"])
# 엔드포인트가 /board/write이고, post 메소드인 경우
def internal_board_write():
    # form 데이터로 입력받은 값을 JSON 형식으로 변환 후 반환
    # JSON(JavaScript Object Notation): 사람이 읽을 수 있는 텍스트를 사용해 데이터를 저장하고 전송하는 데이터 공유를 위한 개방형 표준 파일 형식 , XML에 대한 좋은 대안
    title = request.form.get("title", "")
    body = request.form.get("body", "")
    user = request.form.get("user", "")
    info = {
        "title": title,
        "body": body,
        "user": user,
    }
    return info #JSON 형식으로 변환한 info 반환
    
    
@app.route("/")
def index():
    # board_write 기능을 호출하기 위한 페이지
    return """
        <form action="/v1/api/board/write" method="POST">
            <input type="text" placeholder="title" name="title"/><br/>
            <input type="text" placeholder="body" name="body"/><br/>
            <input type="submit"/>
        </form>
    """
app.run(host="127.0.0.1", port=8000, debug=True)

문제점
data = f"title={title}&body={body}&user={session['idx']}에서
title에 title&user=admin를 삽입하면
title=title&user=admin&body=body&user=guest 데이터 구성
->API는 앞에 존재하는 파라미터 값 가져와 사용->user값 변조 가능

web-ssrf

코드부터 살펴보자

#!/usr/bin/python3
from flask import (
    Flask,
    request,
    render_template
)
import http.server
import threading
import requests
import os, random, base64
from urllib.parse import urlparse

app = Flask(__name__)
app.secret_key = os.urandom(32)

try:
    FLAG = open("./flag.txt", "r").read()  # Flag is here!!
except:
    FLAG = "[**FLAG**]"


@app.route("/")
def index():
    return render_template("index.html")
# / 엔드포인트: index.html 불러옴


@app.route("/img_viewer", methods=["GET", "POST"])
def img_viewer():
    if request.method == "GET":
    # GET 메소드일경우
        return render_template("img_viewer.html")
        # img_viewer.html 불러옴
    elif request.method == "POST":
    # POST 메소드일경우
        url = request.form.get("url", "")
        # url 받아와서 url에 저장
        urlp = urlparse(url)
        # urlparse: url을 구성요소로 파싱하기
        # 결과의 예시 : urlp(scheme='https', netloc='www.dreamhack.io', path='/index.html', params='', query='name=eeee&age=111111', fragment='')
        if url[0] == "/":
        # url의 0번째 인덱스가 /인 경우
            url = "http://localhost:8000" + url
            # 입력한 url은 "http://localhost:8000" + url이렇게 url에 저장
        elif ("localhost" in urlp.netloc) or ("127.0.0.1" in urlp.netloc):
        	#urlp의 netloc가 localhost이거나 127.0.0.1인 경우
            #localhost와 127.0.0.1을 우회해야함
            data = open("error.png", "rb").read()
            # error.png 열어서 보여줌
            img = base64.b64encode(data).decode("utf8")
            # base64.b64encode: base64 인코딩 된 바이트 데이터 리턴, 바이트 데이터는 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)


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)

플래그는 /app/flag.txt에 저장되어 있다고 한다.

urlp의 netloc가 localhost이거나 127.0.0.1인 경우 error.png를 보여주므로
localhost와 127.0.0.1을 우회해야함

url 필터링 우회 방법에는 여러가지가 있지만
localhost를 LocAlHost로 변환해서 사용

문제는 local_port가 1500에서 1800중 하나
burp suite켜서 intruder로 보내주기


1645에서만 Length가 짧다 아무래도 이게 local_port가 인듯 하다.

그래서 http://LocAlHost:1645/flag.txt를 입력해주었더니

이렇게 나오고 flag는 안나왔다..

다시 burp suite로 가서 http://LocAlHost:1645/flag.txt를 proxy로 잡아주고
repeater로 보내준 후 send 해도 안보였다.
근데 자세히 보니까 img src="주소"가 나왔는데

<img src="data:image/png;base64, REh7NDNkZDIxODkwNTY0NzVhN2YzYmQxMTQ1NmExN2FkNzF9"/>

이게 뭐지 싶어서 긁어주니까

나왔다
(개발자모드로 가도 img src="어쩌고저쩌고" 나와있었다.)

이 문제는 사실 모르겠어서.. url 우회하는 방법의 강의를 좀 봤다..

그래서 강의에 나온 내용을 좀 정리하려고 한다.

URL 필터링 우회

http://vcap.me:8000/
127.0.0.1에 매핑됨

http://0x7f.0x00.0x00.0x01:8000/
127.0.0.1을 16진수로 변환한것

http://0x7f000001:8000/
16진수로 변환 후 . 제거

http://2130706433:8000/
10진수로 변환

http://Localhost:8000/
url에서 호스트와 스키마는 대소문자 구분 x

http://127.0.0.255:8000/
127.1, 127.0.1과 같은 호스트, 127.0.0.1~127.0.0.255까지는 모두 로컬 호스트 가리킴
profile
긍정왕되기

0개의 댓글