웹해킹

웹해킹 3주차

유민기 2025. 2. 10. 22:17
#!/usr/bin/python3
from flask import Flask, request, render_template
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
import urllib
import os

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

try:
    FLAG = open("./flag.txt", "r").read()
except:
    FLAG = "[**FLAG**]"


def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    try:
        service = Service(executable_path="/chromedriver")
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome(service=service, options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get("http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(url)
    except Exception as e:
        driver.quit()
        # return str(e)
        return False
    driver.quit()
    return True


def check_xss(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)

def xss_filter(text):
    _filter = ["script", "on", "javascript"]
    for f in _filter:
        if f in text.lower():
            return "filtered!!!"

    advanced_filter = ["window", "self", "this", "document", "location", "(", ")", "&#"]
    for f in advanced_filter:
        if f in text.lower():
            return "filtered!!!"

    return text

@app.route("/")
def index():
    return render_template("index.html")


@app.route("/vuln")
def vuln():
    param = request.args.get("param", "")
    param = xss_filter(param)
    return param


@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param")
        if not check_xss(param, {"name": "flag", "value": FLAG.strip()}):
            return '<script>alert("wrong??");history.go(-1);</script>'

        return '<script>alert("good");history.go(-1);</script>'


memo_text = ""


@app.route("/memo")
def memo():
    global memo_text
    text = request.args.get("memo", "")
    memo_text += text + "\n"
    return render_template("memo.html", memo=memo_text)


app.run(host="0.0.0.0", port=8000)

 

  • / → 기본 인덱스 페이지 렌더링
  • /vuln → XSS 필터링 후 결과 반환
  • /flag → XSS 공격 성공 여부 확인
  • /memo → 메모 기능
def read_url(url, cookie={"name": "name", "value": "value"}):
    cookie.update({"domain": "127.0.0.1"})
    try:
        service = Service(executable_path="/chromedriver")
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome(service=service, options=options)
        driver.implicitly_wait(3)
        driver.set_page_load_timeout(3)
        driver.get("http://127.0.0.1:8000/")
        driver.add_cookie(cookie)
        driver.get(url)
    except Exception as e:
        driver.quit()
        return False
    driver.quit()
    return True
  • Chrome WebDriver를 사용하여 특정 URL을 방문하고, 쿠키를 설정하는 기능을 수행합니다.
  • 실행 시 chromedriver가 필요합니다.
  • 127.0.0.1:8000을 대상으로 웹 요청을 수행합니다.
  • 쿠키를 add_cookie()로 설정한 후 페이지를 다시 로드합니다.
  • 위험 요소: XSS 검사를 위해 Selenium을 활용하는데, 127.0.0.1 내부에서만 실행되므로 서버 사이드에서 악성 코드 실행 가능성이 있음.

공격 흐름

Flask 애플리케이션에서 request.args를 통해 특정 쿼리 파라미터(param) 값을 가져오고, 만약 해당 값이 존재하지 않으면 빈 문자열("")을 반환합니다. 이후, param 값은 xss_filter() 함수를 통해 필터링됩니다.

  1. /flag 엔드포인트에서 POST 요청을 보냄.
  2. param 값을 check_xss() 함수로 전달하여 내부에서 read_url() 실행.
  3. read_url()이 FLAG 쿠키를 설정한 후, 공격자가 지정한 URL을 방문.
  4. 공격자가 지정한 URL에서 FLAG 쿠키 값을 탈취할 수 있도록 유도.

 

def check_xss(param, cookie={"name": "name", "value": "value"}):
    url = f"http://127.0.0.1:8000/vuln?param={urllib.parse.quote(param)}"
    return read_url(url, cookie)

 

  • 특정 파라미터를 포함한 /vuln 페이지를 방문하여 XSS 공격이 가능한지 검사하는 함수.
  • read_url()을 통해 vuln?param=<value> 페이지에 접근하여 XSS 필터를 우회할 수 있는지 확인.

param 값을 가지고 /vuln 페이지에 접근하여 read_url에 넘김.

이때 /vuln에서는 xss 필터링 함수를 거치기 때문에 param 값이 필터링에 걸리지 않아야 flag를 띄움

 

 

def xss_filter(text):
    _filter = ["script", "on", "javascript"]
    for f in _filter:
        if f in text.lower():
            return "filtered!!!"

    advanced_filter = ["window", "self", "this", "document", "location", "(", ")", "&#"]
    for f in advanced_filter:
        if f in text.lower():
            return "filtered!!!"

    return text

 

xss_filter() 함수는 다음과 같은 문자열을 필터링합니다.

1. 필터링 대상 문자열

  • 기본 필터링: "script", "on", "javascript"
  • 추가 필터링: "window", "self", "this", "document", "location", "(", ")", "&#"

2. 필터링 방식

  • 입력된 문자열을 소문자로 변환한 후 검사하므로, **대소문자를 조작하는 기법(XSS 우회 기법 중 하나)**이 적용되불가
  • 필터링된 문자열을 공백으로 치환하는 것이 아니라 "filtered!!!"로 대체하므로,
    • scrscriptipt와 같이 필터링될 단어를 분할하여 문자열을 조작하는 조합 우회 기법도 사용할 수 없음
    • 즉, scr<script>ipt → "scrfiltered!!!ipt"로 변환되므로, "script"가 복원되지 않음.

3. 결론

  • 단순한 대소문자 변경이나 문자열 조합 기법을 이용한 필터링 우회는 불가능
  • 그러나 탭(\t), 개행(\n), 유니코드 변형(예: \u0020) 등의 특수 문자 인코딩을 활용한 우회 공격은 여전히 가능

 

 

@app.route("/flag", methods=["GET", "POST"])
def flag():
    if request.method == "GET":
        return render_template("flag.html")
    elif request.method == "POST":
        param = request.form.get("param")
        if not check_xss(param, {"name": "flag", "value": FLAG.strip()}):
            return '<script>alert("wrong??");history.go(-1);</script>'

        return '<script>alert("good");history.go(-1);</script>'

 

POST 요청으로 param을 받아서 check_xss() 함수로 XSS 공격을 검증

만약 XSS가 성공하면 "flag" 쿠키 값이 설정된 상태로 요청이 진행되므로 공격 성공 여부를 확인할 수 있음

 

1. GET 요청:

  • /flag 엔드포인트에 GET 요청을 보내면 flag.html을 렌더링하여 표시

 

2. POST 요청:

  • param이라는 파라미터 값을 받아 param 변수에 저장
  • 이후, check_xss(param, {"name": "flag", "value": FLAG.strip()}) 함수를 호출
  • check_xss() 함수 역할:
    • param 값을 포함한 URL을 생성한 후 read_url()을 호출하여 내부적으로 해당 URL을 방문한다.
    • 이때, read_url() 함수는 쿠키(name=flag, value=FLAG)를 설정한 상태로 요청을 보낸다.

3. 결론

  • check_xss()에 전달되는 쿠키의 value 값이 우리가 찾아야 할 FLAG 값으로 추정
  • 공격자는 check_xss()를 활용하여 특정 URL로 요청을 보낼 수 있으며, 이 URL에서 FLAG 값을 탈취할 방법을 찾아야함
    •  
  •  

 

memo_text = ""

@app.route("/memo")
def memo():
    global memo_text
    text = request.args.get("memo", "")
    memo_text += text + "\n"
    return render_template("memo.html", memo=memo_text)
  • GET 방식으로 전달된 memo 값을 memo_text 변수에 누적 저장하고 렌더링합니다.

/memo 에서 파라미터 값을 출력해 주니까

/flag에서 파라미터 값에 스크립트를 실행시켜 /memo 페이지에 쿠키를 추가해 보내주면 memo 페이지에서 flag 값을 볼 수 있을 것이다.

 

 

 

GET 요청:

  • 데이터는 **쿼리 문자열(Query String)**을 통해 전송되며, URL에 직접 포함
  • 이 과정에서 브라우저는 **URL 정규화(URL Normalization)**를 수행하여 불필요한 공백, 특수 문자 등을 표준적인 형태로 변환
  • 예를 들어, +(공백)이나 %20 등의 인코딩된 문자가 자동 변환될 수 있음

POST 요청:

  • 데이터는 Request Body(본문)에 포함되어 전송
  • 쿼리 문자열이 아닌 별도의 데이터 영역에서 전달되므로 URL 정규화 과정이 적용되지 않음
  • 따라서, 공격자가 필터링을 우회하기 위해 일부러 공백이나 특수 문자를 삽입하는 경우, 브라우저가 이를 자동 변환하지 않아 공격이 성공할 가능성이 존재함
<iframe src="javascri	pt:locatio	n.href='/memo?memo='%2bdocumen	t.cookie">

위와 같은 페이로드는 다음과 같은 원리로 필터링을 우회할 수 있습니다.

  1. javascri pt:
    • script라는 문자열이 필터링되었을 경우, 일부러 탭(Tab, \t) 문자를 삽입하여 필터를 우회하는 기법
    • javascri\tpt는 브라우저에서 자동으로 javascript로 해석될 수 있음
  2. locatio n.href:
    • location.href도 마찬가지로 탭 문자를 삽입하여 필터링을 우회할 수 있음
    • 필터가 단순히 location이라는 문자열을 검색해 차단할 경우, locatio\tn과 같이 입력하면 우회
  3. 쿼리 문자열에서 %2b (+) 사용:
    • URL 인코딩을 이용해 + 대신 %2b를 사용하여 필터링을 회피할 수 있음
    • 예를 들어, '/memo?memo='%2bdocumen t.cookie'는 document.cookie를 memo 값으로 전달하여 쿠키 탈취를 시도할 수 있음.