본문 바로가기

4-5. 2022-2 심화 스터디/버그 헌팅과 모의 해킹

[2022.09.17] 크로스 사이트 스크립팅(xss)

XSS 개념정리

  • xss: 웹 사이트가 특정 문자의 입력값 검증을 하지 않아 브라우저가 악성 자바스크립트를 실행할 때마다 발생.
  • 취약점을 유발하는 문자: 큰따옴표, 작은 따옴표, 꺾쇠괄호(<>)

 

사이트에서 문자를 올바르게 검증하는 경우: HTML 엔터키로 렌더링됨.
  • ex) 큰따옴표(”)를 " 또는 &quot; 또는 &#34;
  • ex2) " 작은따옴표(’)를 &apos 또는 &#39;

 

올바르게 검증을 하지 않는 경우: HTML과 자바스크립트로 웹 페이지 구조를 정의할 수 있음
  • ex) 꺾쇠를 제거하지 않는다면 를 삽입하여 페이로드를 주입할 수 있다.
    → 입력값 검증을 하지 않는 웹사이트에서 해당 자바스크립트를 실행.

 

동일 출처 정책(SOP):  document가 다른 출처에서 가져온 자원과 상호작용하는 방법을 제한.
  • ex) www..com을 방문해 브라우저에서 www. .com/profile에 대한 GET 요청을 호출했을 때 SOP에서 www. .com이 www..com/profile의 응답을 읽지 못하도록 제 한. 출처 결정: 프로토콜, 호스트, 포트가 결정

 

큰따옴표, 작은 따옴표를 이용해서도 공격 가능
  • 큰따옴표 → 큰따옴표로 닫아주고 다른 스크립트 작성
  • 작은 따옴표 → 작은 따옴표 + 세미콜론(;)으로 닫아주고 원하는 스크립트 작성

 

XSS의 종류

반사 vs 저장
  • 반사 XSS: 사이트의 어느 곳에도 저장되지 않은 단일 HTTP 요청이 전달돼 XSS 페이 로드를 실행할 때 발생
    해결방법 XSS Auditors도입: 자바스크립트를 실행하는 악성 링크로부터 사용자를 보호.
    !그러나 공격자들이 XSS Auditor를 우회하는 경우가 많고, 방법이 자주 바뀜!
  • 저장 XSS: 악성 페이로드를 저장하고, 값을 검증하지 않고 렌더링할 때 발생
DOM 기반 vs 블라인드(blind) vs 자체(self)
  • DOM 기반 XSS공격: 웹 사이트의 기존 자바스크립트 코드를 조작해 악성 자바스크립 트를 실행하는 것을 포함함.(반사 또는 저장 공격에 해당.)
  • 블라인드 XSS: 해커가 액세스할 수 없는 웹 사이트 위치에서 다른 사용자가 XSS 페이 로드를 렌더링하는 저장 XSS공격.
    ex) SNS에서 개인 프로필을 만들 때, 이름에 XSS를 추가할 수 있는 경우, 공격이 가능. (관리자가 사이트의 관리자용 페이지를 방문했을 때 XSS 실행되는 경우)
  • 자체(self) XSS 취약점: 페이로드를 입력하는 사용자에게만 영향을 줄 수 있는 취약점 → 공격자는 자신만을 공격 대상으로 삼을 수 있고, 심각도가 낮으며 대부분 포상금을 받을 수 없음

xss 사례  - Google 태그 관리자에 저장 xss

URL: tagmanager.google.com/
출처: https://mahmoudser/blogspot.com/2015/09/how-i-found-xss-vulnerability-in-google.html
보고 날짜: 2015년 9월 12일'
공격 대상: SEO도구인 구글 태그 관리자(Tag Manager), JSON 파일을 업로드하는 기능
  • 일반적인 웹 보안의 모범 사례는 렌더링 시점에 사용자의 입력값을 검증하는 것. 하지만 구글은 사용자 입력값 저장을 렌더링 시점이 아닌 사용자의 입력값 검증 이후에 시도했음
 첫번째 공격: SEO도구인 구글 태그 관리자(Tag Manager)
    •  페렌바흐는 #"><img scr=/onerror=alert(3)> 와 같은 페이로드를 입력
    • 페이로드가 양식 필드에서 승인될 경우 기존 HTML 태그를 닫고 존재하지 않는 이미지 로드 실행, 이미지를 찾을 수 없기 때문에 웹 사이트는 onerror 함수인 alert(3)을 실행해야 했으나 구글의 입력값 검증에 막혀 실패.
두번째 공격:  JSON 파일을 업로드하는 기능을 공격 대상 삼아 페이로드 작성
“data” :{
       “name”: “#”><img scr=/onerror=alert(3)>" 
      “type”: “AUTO_EVENT_VAR” 
      “autoEventVarMacro”: {
                "varType”: “HISTORY_NEW_URL_FRAGMENT”
        }
}
  • 구글은 렌더링 시점이 아닌 전송하는 시점에 웹 양식을 대상으로 입력값을 검증했고, 결 과적으로 파일 업로드 기능에 입력값 검증 작업을 추가하지 않아 페이로드가 실행됨. 

 

시사점
  1. 각각의 입력값이 처리되는 방식이 다를 수 있기 때문에 값을 입력하고자 제공 되는 모든 방법을 테스트 할 것
  2. 잘 알려진 취약점이더라도 개발자의 실수가 있을 수 있으니 시도해 볼 것

xss/csrf 모의해킹

 

Dreamhack csrf-1

>python code

더보기

python code

#!/usr/bin/python3
from flask import Flask, request, render_template
from selenium import webdriver
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:
        options = webdriver.ChromeOptions()
        for _ in [
            "headless",
            "window-size=1920x1080",
            "disable-gpu",
            "no-sandbox",
            "disable-dev-shm-usage",
        ]:
            options.add_argument(_)
        driver = webdriver.Chrome("/chromedriver", 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()
        print(str(e))
        # return str(e)
        return False
    driver.quit()
    return True


def check_csrf(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)


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


@app.route("/vuln")
def vuln():
    param = request.args.get("param", "").lower()
    xss_filter = ["frame", "script", "on"]
    for _ in xss_filter:
        param = param.replace(_, "*")
    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_csrf(param):
            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", None)
    if text:
        memo_text += text
    return render_template("memo.html", memo=memo_text)


@app.route("/admin/notice_flag")
def admin_notice_flag():
    global memo_text
    if request.remote_addr != "127.0.0.1":
        return "Access Denied"
    if request.args.get("userid", "") != "admin":
        return "Access Denied 2"
    memo_text += f"[Notice] flag is {FLAG}\n"
    return "Ok"


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

 

 

 

vuln(csrf) page

@app.route("/vuln")
def vuln():
    param = request.args.get("param", "").lower()
    xss_filter = ["frame", "script", "on"]
    for _ in xss_filter:
        param = param.replace(_, "*")
    return param
  •  vuln 페이지에선  frame, script, on 문자열을 필터링하고 있음
  • 따라서 <script>alert(1)</script>라는 스크립트를 그대로 출력하지 않음
memo

memo_text = ""


@app.route("/memo")
def memo():
    global memo_text
    text = request.args.get("memo", None)
    if text:
        memo_text += text
    return render_template("memo.html", memo=memo_text)
  • memo라는 쿼리스트링으로 값을 전달받고, memo_text에 append한 후 화면에 출력함
    • GET 방식으로 데이터를 전송
notice flag

@app.route("/admin/notice_flag")
def admin_notice_flag():
    global memo_text
    if request.remote_addr != "127.0.0.1":
        return "Access Denied"
    if request.args.get("userid", "") != "admin":
        return "Access Denied 2"
    memo_text += f"[Notice] flag is {FLAG}\n"
    return "Ok"

 

  • 주소가 127.0.0.1이 아니거나, userid의 값이 admin이 아니면 Access Denied를 출력함
  • 두 조건을 모두 만족시키면 flag를 얻을 수 있음
    • 이때, flag는 memo_text에 append 되므로, memo 페이지에서 확인 가능

 

flag

@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_csrf(param):
            return '<script>alert("wrong??");history.go(-1);</script>'

        return '<script>alert("good");history.go(-1);</script>'
  • 이곳에 답을 적어 제출
따라서...
  • <img src=/admin/notice_flag?userid=admin>을 입력 후 제출하면 flag를 얻을 수 있음!

 


XSS Challenges stage 1

HINT: very simple


webhacking.kr old-23

 

  • 문제가 원하는 것은 매우 명확함. 서버에서 <script>alert(1);</script>를 실행시키는 것.
  • 그러나 문자 두 개를 연달아 입력하면 ‘no hack’을 출력

  따라서... 

  • null 문자와 url 인코딩을 이용해 모든 문자 사이를 띄어 주면 문제를 해결할 수 있음

  • <s%00c%00r%00i%00p%00t>a%00l%00e%00r%00t(1);</s%00c%00r%00i%00p%00t>
  • ** 주의! 제출 버튼에 넣으면 안 되고, 꼭 url의 code를 이용해 GET 방식으로 전달해야 함. 이 부분으로 인해 CSRF/XXS 문제라고 이야기하는 듯.