לפני כמה ימים נתקלתי בשירות שאימת את החתימה של בקשות בצד השרת. זה היה קזינו מקוון קטן, שלכל בקשה בדק איזה ערך שנשלח על ידי המשתמש מהדפדפן. ללא קשר למה שעשית בקזינו: ביצוע הימור או ביצוע הפקדה, פרמטר נוסף בכל בקשה היה הערך "סימן", המורכב מקבוצה של תווים אקראיים לכאורה. אי אפשר היה לשלוח בקשה בלעדיה - האתר החזיר שגיאה, והוא מנע ממני לשלוח בקשות מותאמות אישית משלי.
אלמלא הערך הזה, הייתי עוזב את האתר באותו רגע ולא חושב עליו שוב. אבל, כנגד כל הסיכויים, לא תחושת הרווח המהיר גרמה לי להתרגש, אלא העניין המחקרי והאתגר שהקזינו נותן לי עם ההוכחה שלו.
בלי קשר למטרה שהמפתחים חשבו כשהם הוסיפו את הפרמטר הזה, נראה לי שזה היה בזבוז זמן. אחרי הכל, החתימה עצמה נוצרת בצד הלקוח, וכל פעולה מצד הלקוח יכולה להיות כפופה להנדסה לאחור.
במאמר זה, אדון כיצד הצלחתי:
מאמר זה ילמד אותך כיצד לחסוך בזמנך היקר ולדחות פתרונות חסרי תועלת אם אתה מפתח שמעוניין לבצע פרויקטים מאובטחים. ואם אתה פנטסטר, לאחר קריאת מאמר זה, אתה יכול ללמוד כמה שיעורים שימושיים על איתור באגים כמו גם לתכנת הרחבות משלך לסכין השוויצרי של האבטחה. בקיצור, כולם בצד החיובי.
בואו סוף סוף נגיע לנקודה.
אז השירות הוא קזינו מקוון עם קבוצה של משחקים קלאסיים:
האינטראקציה עם השרת פועלת כולה על בסיס בקשות HTTP. ללא קשר למשחק שתבחר, כל בקשת POST לשרת חייבת להיות חתומה - אחרת השרת יגרום לשגיאה. חתימה על בקשות בכל אחד מהמשחקים האלה עובדת על אותו עיקרון - אני אקח רק משחק אחד כדי לחקור כדי שלא אצטרך לעשות את אותה העבודה פעמיים.
ואני הולך לקחת משחק בשם Dragon Dungeon.
המהות של המשחק הזה היא לבחור את הדלתות בטירה ברצף בתפקיד אביר. מאחורי כל דלת מסתתר אוצר או דרקון. אם השחקן נתקל בדרקון מאחורי דלת, המשחק נעצר והוא מפסיד כסף. אם האוצר נמצא - סכום ההימור הראשוני גדל והמשחק ממשיך עד שהשחקן לוקח את הזכייה, מפסיד או עובר את כל הרמות.
לפני תחילת המשחק, על השחקן לציין את סכום ההימור ומספר הדרקונים.
אני מזין את המספר 10 כסכום, משאיר דרקון אחד ומסתכל על הבקשה שתישלח. ניתן לעשות זאת מכלי המפתחים בכל דפדפן, ב-Chromium אחראית לכך לשונית הרשת.
כאן אתה גם יכול לראות שהבקשה נשלחה לנקודת הקצה /srv/api/v1/dungeon
.
הכרטיסייה Payload מציגה את גוף הבקשה עצמו בפורמט JSON
שני הפרמטרים הראשונים ברורים - אני בחרתי אותם מהממשק; האחרון, כפי שאתה יכול לנחש, הוא timestamp
או הזמן שחלף מאז 1 בינואר 1970, עם דיוק Javascript טיפוסי של אלפיות שניות.
זה משאיר פרמטר אחד לא פתור ו, - וזו החתימה עצמה. כדי להבין איך הוא נוצר, אני עובר ללשונית מקורות - המקום הזה מכיל את כל המשאבים של השירות שהדפדפן טען. כולל Javascript, האחראי על כל ההיגיון של חלק הלקוח באתר.
זה לא כל כך קל להבין את הקוד הזה - הוא ממוזער. אתה יכול לנסות לטשטש את הכל - אבל זה תהליך ארוך ומייגע שייקח הרבה זמן (בהתחשב בכמות קוד המקור), אני לא מוכן לעשות את זה.
האפשרות השנייה והפשוטה יותר היא פשוט למצוא את החלק הדרוש בקוד על ידי מילת מפתח ולהשתמש ב-debugger. זה מה שאני אעשה, כי אני לא צריך לדעת איך כל האתר עובד, אני רק צריך לדעת איך החתימה נוצרת.
לכן, כדי למצוא את החלק בקוד שאחראי להפקת הקוד, ניתן לפתוח חיפוש בכל המקורות באמצעות שילוב המקשים CTRL+SHIFT+F
ולחפש הקצאת ערך למפתח sign
שנשלח בבקשה.
למרבה המזל, יש רק התאמה אחת, מה שאומר שאני בדרך הנכונה.
אם תלחץ על התאמה, תוכל להגיע לקטע הקוד שבו נוצרת החתימה עצמה. הקוד מעורפל כמו קודם לכן עדיין קשה לקרוא אותו.
מול שורת הקוד שמתי נקודת עצירה, מרענן את העמוד ומציע הצעה חדשה ב"דרקונים" - כעת הסקריפט הפסיק את עבודתו בדיוק ברגע של יצירת החתימה, וניתן לראות את המצב של כמה משתנים.
הפונקציה שנקראת מורכבת מאות אחת, גם המשתנים - אבל אין בעיה. אתה יכול ללכת לקונסולה ולהציג את הערכים של כל אחד מהם. המצב מתחיל להתבהר.
הערך הראשון I פלט הוא הערך של המשתנה H
, שהוא פונקציה. אתה יכול ללחוץ עליו מהמסוף ולעבור למקום שבו הוא מוצהר בקוד, להלן הרישום.
זהו קטע קוד די גדול שבו ראיתי רמז - SHA256. זהו אלגוריתם גיבוב. אתה יכול גם לראות ששני פרמטרים מועברים לפונקציה, מה שרומז שאולי זה לא רק SHA256, אלא HMAC SHA256 עם סוד.
כנראה המשתנים שמועברים כאן (גם פלט לקונסולה):
10;1;6693a87bbd94061678473bfb;1732817300080;gRdVWfmU-YR_RCuSkWFLCUTly_GZfDx3KEM8
- ישירות הערך שעליו מוחל פעולת HMAC SHA256.31754cff-be0f-446f-9067-4cd827ba8707
הוא קבוע סטטי שפועל כסוד
כדי לוודא זאת, אני קורא לפונקציה ומקבל את החתימה המשוערת
עכשיו אני הולך לאתר שסופר את HMAC SHA256 ומעביר אליו ערכים.
והשוואה לזה שנשלח בבקשה כשהצעתי את ההצעה.
התוצאה זהה, מה שאומר שהניחושים שלי היו נכונים - הוא באמת משתמש ב-HMAC SHA256 עם סוד סטטי, שמועבר למחרוזת שנוצרה במיוחד עם הקצב, מספר הדרקונים ועוד כמה פרמטרים, עליהם אספר בהמשך. במהלך המאמר.
האלגוריתם די פשוט וישיר. אבל זה עדיין לא מספיק - אם זו הייתה מטרה בתוך פרויקט עבודה עבור pentest למצוא נקודות תורפה, הייתי צריך ללמוד איך לשלוח שאילתות משלי באמצעות Burp Suite.
וזה בהחלט מצריך אוטומציה, וזה מה שאני הולך לדבר עליו עכשיו.
הבנתי את האלגוריתם של יצירת חתימות. עכשיו הגיע הזמן ללמוד כיצד ליצור אותו באופן אוטומטי על מנת לפשט את כל הדברים המיותרים בעת שליחת בקשות.
אתה יכול לשלוח בקשות באמצעות ZAP, Caido, Burp Suite וכלים אחרים. מאמר זה יתמקד ב-Burp Suite, מכיוון שלדעתי היא הכי ידידותית למשתמש וכמעט מושלמת. מהדורת הקהילה ניתנת להורדה בחינם מהאתר הרשמי, זה מספיק לכל הניסויים.
מחוץ לקופסה Burp Suite לא יודע ליצור HMAC SHA256. לכן, על מנת לעשות זאת, ניתן להשתמש בהרחבות המשלימות את הפונקציונליות של Burp Suite.
הרחבות נוצרות הן על ידי חברי הקהילה והן על ידי המפתחים עצמם. הם מופצים דרך חנות BApp, Github, או מאגרי קוד מקור אחרים המובנה בחינם.
יש שני מסלולים שאתה יכול ללכת:
לכל אחת מהדרכים הללו יש את היתרונות והחסרונות שלה, אני אראה לכם את שניהם.
השיטה עם הרחבה מוכנה היא הקלה ביותר. זה להוריד אותו מ-BApp Store ולהשתמש בתכונות שלו כדי ליצור ערך עבור פרמטר sign
.
התוסף שהשתמשתי בו נקרא Hackvertor . זה מאפשר לך להשתמש ב-XML כמו תחביר כדי שתוכל לקודד/פענח באופן דינמי, להצפין/לפענח, לגיבוב נתונים שונים.
כדי להתקין אותו, Burp דורש:
עבור ללשונית הרחבות
הקלד Hackvertor בחיפוש
בחר את התוסף שנמצא ברשימה
לחץ על התקן
לאחר התקנתו, כרטיסייה בעלת אותו שם תופיע ב-Burp. ניתן לגשת אליו ולהעריך את יכולות התוסף ואת מספר התגים הזמינים, שכל אחד מהם ניתן לשילוב אחד עם השני.
כדי לתת דוגמה, אתה יכול להצפין משהו עם AES סימטרי באמצעות התג <@aes_encrypt('supersecret12356','AES/ECB/PKCS5PADDING')>MySuperSecretText<@/aes_encrypt>
.
הסוד והאלגוריתם נמצאים בסוגריים, ובין התגים הטקסט עצמו להצפנה. ניתן להשתמש בכל תגיות ב-Repeater, Intruder וכלים מובנים אחרים של Burp Suite.
בעזרת תוסף Hackvertor ניתן לתאר כיצד יש ליצור חתימה ברמת התג. אני הולך לעשות את זה על דוגמה של בקשה אמיתית.
אז, אני מבצע הימור ב-Dragon Dungeon, מיירט את אותה בקשה שיירטתי בתחילת המאמר הזה עם Intercept Proxy, ומדגיש אותה ב-Repeater כדי להיות מסוגל לערוך אותה ולהגיש אותה מחדש.
כעת במקום הערך ae04afe621864f569022347f1d1adcaa3f11bebec2116d49c4539ae1d2c825fc
, עלינו להחליף את האלגוריתם ליצירת HMAC SHA256 באמצעות התגים שסופקו על ידי Hackvertor.
Формула генерации у меня получилась следующая <@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>10;1;6693a87bbd94061678473bfb;<@timestamp/>000;MDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4<@/hmac_sha256>
.
שקול את כל הפרמטרים:
10
- סכום הימור1
- מספר דרקונים6693a87bbd94061678473bfb
- מזהה משתמש ייחודי ממסד הנתונים של MongoDB, ראיתי אותו תוך כדי ניתוח החתימה מהדפדפן, אבל לא כתבתי על זה אז. הצלחתי למצוא אותו על ידי חיפוש בתוכן השאילתות ב-Burp Suite, הוא חוזר מהשאילתה /srv/api/v1/profile/me
.
<@timestamp/>000
- יצירת חותמת זמן, שלושת האפסים האחרונים מצמצמים את הזמן לאלפיות שניותMDWpmNV9-j8tKbk-evbVLtwMsMjKwQy5YEs4
- אסימון CSRF, המוחזר מנקודת הקצה /srv/api/v1/csrf
, ומוחלף בכל בקשה, בכותרת X-Xsrf-Token
.<@hmac_sha256('31754cff-be0f-446f-9067-4cd827ba8707')>
ו <@/hmac_sha256>
- תגי פתיחה וסגירה להפקת HMAC SHA256 מהערך המוחלף עם הסוד כקבוע 31754cff-be0f-446f-9067-4cd827ba8707
.
חשוב לציין: הפרמטרים חייבים להיות מחוברים זה לזה באמצעות ;
לפי סדר קפדני, - אחרת החתימה תיווצר בצורה שגויה - כמו בצילום המסך הזה שבו החלפתי את התעריף ואת מספר הדרקונים
שם טמון כל הקסם.
עכשיו אני עושה שאילתה נכונה, שבה אני מציין את הפרמטרים בסדר הנכון, ומקבל מידע שהכל הצליח והמשחק התחיל - זה אומר שהאקוורטור יצר חתימה במקום נוסחה, החליף אותה בשאילתה, והכל עובד .
עם זאת, לשיטה זו יש חיסרון משמעותי - לא ניתן להיפטר לחלוטין מעבודה ידנית. בכל פעם שאתה משנה את קצב או מספר הדרקונים ב-JSON, אתה צריך לשנות את זה בחתימה עצמה כדי שיתאימו.
כמו כן, אם אתה שולח בקשה חדשה מהכרטיסייה Proxy ל-Intruder או Repeater, אתה צריך לכתוב מחדש את הנוסחה, וזה מאוד מאוד לא נוח כאשר אתה צריך כרטיסיות רבות למקרי בדיקה שונים.
נוסחה זו תיכשל גם בשאילתות אחרות שבהן נעשה שימוש בפרמטרים אחרים.
אז החלטתי לכתוב הרחבה משלי כדי להתגבר על החסרונות הללו.
אתה יכול לכתוב הרחבות עבור Burp Suite ב-Java ו-Python. אני אשתמש בשפת התכנות השנייה מכיוון שהיא פשוטה ויזואלית יותר. אבל אתה צריך להכין את עצמך מראש: ראשית עליך להוריד את Jython Standalone מהאתר הרשמי, ולאחר מכן את הנתיב לקובץ שהורדת בהגדרות Burp Suite.
לאחר מכן, עליך ליצור קובץ עם קוד המקור עצמו והסיומת *.py
.
כבר יש לי בילט שמגדיר את ההיגיון הבסיסי, הנה התוכן שלו:
הכל פשוט ואינטואיטיבי:
getActionName
- שיטה זו מחזירה את שם הפעולה שתתבצע על ידי התוסף. ההרחבה עצמה מוסיפה כלל טיפול ב-Session שניתן להחיל בגמישות על כל אחת מהבקשות, אך על כך בהמשך. חשוב לדעת כי שם זה עשוי להיות שונה משם התוסף, וכי ניתן יהיה לבחור אותו מהממשק.performAction
- ההיגיון של הכלל עצמו, שיחול על הבקשות שנבחרו, יפורט כאן
שתי השיטות מוצהרות על פי ממשק ISessionHandlingAction .
עכשיו לממשק IBurpExtender . הוא מצהיר על השיטה הנחוצה היחידה registerExtenderCallbacks
, שמתבצעת מיד לאחר טעינת התוסף, ונחוצה כדי שהיא תעבוד בכלל.
כאן מתבצעת התצורה הבסיסית:
callbacks.setExtensionName(EXTENSION_NAME)
- רושם את התוסף הנוכחי כפעולה לטיפול בהפעלותsys.stdout = callbacks.getStdout()
- מפנה את הפלט הסטנדרטי (stdout) לחלון הפלט של Burp Suite (חלונית "הרחבות")self.stderr = PrintWriter(callbacks.getStdout(), True)
- יוצר זרם עבור פלט שגיאותself.stdout.println(EXTENSION_NAME)
- מדפיס את שם התוסף ב-Burp Suiteself.callbacks = callbacks
- שומר את אובייקט ה-callbacks כתכונה עצמית. זה נחוץ לשימוש מאוחר יותר ב-Burp Suite API בחלקים אחרים של קוד ההרחבה.self.helpers = callbacks.getHelpers()
- מקבל גם שיטות שימושיות שיהיו נחוצות כאשר התוסף פועל
עם ההכנות המקדימות, זהו. כעת תוכלו לטעון את התוסף ולוודא שהיא פועלת בכלל. כדי לעשות זאת, עבור ללשונית הרחבות ולחץ על הוסף.
בחלון שמופיע, ציין
ולחץ על הבא.
אם קובץ קוד המקור עוצב כהלכה, לא אמורות להתרחש שגיאות, והכרטיסייה פלט תציג את שם ההרחבה. זה אומר שהכל עובד כמו שצריך.
התוסף נטען ועובד - אבל כל מה שנטען היה עטיפה בלי שום היגיון, עכשיו אני צריך את הקוד ישירות כדי לחתום על הבקשה. כבר כתבתי את זה וזה מוצג בצילום המסך למטה.
הדרך בה כל התוסף עובד היא שלפני שהבקשה תישלח לשרת, היא תשתנה על ידי ההרחבה שלי.
אני מקבל תחילה את הבקשה שהרחבה יירטה, ומקבל את התעריף, ואת מספר הדרקונים, מגופה
json_body = json.loads(message_body) amount_currency = json_body["amountCurrency"] dragons = json_body["dragons"]
לאחר מכן, אני קורא את חותמת הזמן הנוכחית ומקבל את אסימון ה-CSRF מהכותרת המתאימה
currentTime = str(time.time()).split('.')[0]+'100' xcsrf_token = None for header in headers: if header.startswith("X-Xsrf-Token"): xcsrf_token = header.split(":")[1].strip()
לאחר מכן, הבקשה עצמה נחתמת באמצעות HMAC SHA256
hmac_sign = hmac_sha256(key, message=";".join([str(amount_currency), str(dragons), user_id, currentTime, xcsrf_token]))
הפונקציה עצמה והקבועים המציינים את הסוד ואת מזהה המשתמש הוכרזו מראש בראש
def hmac_sha256(key, message): return hmac.new( key.encode("utf-8"), message.encode("utf-8"), hashlib.sha256 ).hexdigest() key = "434528cb-662f-484d-bda9-1f080b861392" user_id = "zex2q6cyc4ba3gvkyex5f80m"
לאחר מכן הערכים נכתבים לגוף הבקשה ומומרים ל-JSON
json_body["sign"] = hmac_sign json_body["t"] = currentTime message_body = json.dumps(json_body)
השלב האחרון הוא ליצור בקשה חתומה ושונתה ולשלוח אותה אל
httpRequest = self.helpers.buildHttpMessage(get_final_headers, message_body) baseRequestResponse.setRequest(httpRequest)
זה הכל, קוד המקור כתוב. כעת תוכלו לטעון מחדש את התוסף ב-Burp Suite (יש לעשות זאת לאחר כל שינוי בסקריפט), ולוודא שהכל עובד.
אבל תחילה עליך להוסיף כלל חדש לעיבוד בקשות. כדי לעשות זאת, עבור אל הגדרות, אל הקטע הפעלות. כאן תמצאו את כל הכללים השונים המופעלים בעת שליחת בקשות.
לחץ על הוסף כדי להוסיף תוסף המופעל בסוגים מסוימים של בקשות.
בחלון שמופיע אני משאיר הכל כמו שהוא ובחר הוסף בפעולות כלל
תופיע רשימה נפתחת. בו, בחר באפשרות Invoke a Burp extension.
וציין עבורו את השלוחה שתיקרא בעת שליחת בקשות. יש לי אחד, וזה הרחבת Burp.
לאחר בחירת התוסף, אני לוחץ על אישור. ואני עובר ללשונית היקף, שם אני מציין:
היקף כלים - Repeater (התוסף אמור להפעיל כאשר אני שולח בקשות באופן ידני דרך Repeater)
URL Scope - כלול את כל כתובות האתרים (כדי שזה יעבוד על כל הבקשות שאני שולח).
זה אמור לעבוד כמו בצילום המסך למטה.
לאחר לחיצה על אישור, כלל ההרחבה הופיע ברשימה הכללית.
סוף סוף, אתה יכול לבדוק הכל בפעולה! כעת תוכל לשנות שאילתה כלשהי ולראות כיצד החתימה תתעדכן באופן דינמי. ולמרות שהשאילתה תיכשל, זה יהיה בגלל שבחרתי בשער שלילי, לא בגלל שמשהו לא בסדר בחתימה (אני פשוט לא רוצה לבזבז כסף 😀). התוסף עצמו עובד והחתימה נוצרת בצורה נכונה.
הכל מצוין, אבל יש שלוש בעיות:
כדי לפתור זאת, עלינו להוסיף שתי בקשות נוספות, שניתן לבצע באמצעות ספריית Burp Suite המובנית, במקום כל צד שלישי, במקום requests
.
לשם כך, עטפתי קצת היגיון סטנדרטי כדי להפוך את השאילתות לנוחות יותר. באמצעות השיטות הסטנדרטיות של Burp, האינטראקציה עם שאילתות נעשית ב-pleintext.
def makeRequest(self, method="GET", path="/", headers=None, body=None): first_line = method + " " + path + " HTTP/1.1" headers[0] = first_line if body is None: body = "{}" http_message = self.helpers.buildHttpMessage(headers, body) return self.callbacks.makeHttpRequest(self.request_host, self.request_port, True, http_message)
והוסיפו שתי פונקציות המחלצות את הנתונים שאני צריך, אסימון CSRF ו-UserID.
def get_csrf_token(self, headers): response = self.makeRequest("GET", "/srv/api/v1/csrf", headers) message = self.helpers.analyzeRequest(response) raw_headers = str(message.getHeaders()) match = re.search(r'XSRF-TOKEN=([a-zA-Z0-9_-]+)', raw_headers) return match.group(1) def get_user_id(self, headers): raw_response = self.makeRequest("POST", "/srv/api/v1/profile/me", headers) response = self.helpers.bytesToString(raw_response) match = re.search(r'"_id":"([a-f0-9]{24})"', response) return match.group(1)
ועל ידי עדכון האסימון עצמו בכותרות הנשלחות
def update_csrf(self, headers, token): for i, header in enumerate(headers): if header.startswith("X-Xsrf-Token:"): headers[i] = "X-Xsrf-Token: " + token return headers
פונקציית החתימה נראית כך. כאן חשוב לציין שאני לוקח את כל הפרמטרים המותאמים אישית שנשלחים בבקשה, מוסיף בסוף אותם את ה- user_id
הסטנדרטי, currentTime
, csrf_token
, וחותם את כולם ביחד באמצעות ;
כמפריד.
def sign_body(self, json_body, user_id, currentTime, csrf_token): values = [] for key, value in json_body.items(): if key == "sign": break values.append(str(value)) values.extend([str(user_id), str(currentTime), str(csrf_token)]) return hmac_sha256(hmac_secret, message=";".join(values))
הרצפה הראשית הצטמצמה לכמה שורות:
OrderedDict
שמייצר את המילון ברצף נוקשה שכן חשוב לשמר אותו בזמן החתימה. csrf_token = self.get_csrf_token(headers) final_headers = self.update_csrf(final_headers, csrf_token) user_id = self.get_user_id(headers) currentTime = str(time.time()).split('.')[0]+'100' json_body = json.loads(message_body, object_pairs_hook=OrderedDict) sign = self.sign_body(json_body, user_id, currentTime, csrf_token) json_body["sign"] = sign json_body["t"] = currentTime message_body = json.dumps(json_body) httpRequest = self.helpers.buildHttpMessage(final_headers, message_body) baseRequestResponse.setRequest(httpRequest)
צילום מסך, ליתר ביטחון
עכשיו, אם אתה הולך למשחק אחר שבו פרמטרים מותאמים אישית הם כבר 3 במקום 2, ותשלח בקשה, אתה יכול לראות שהיא תישלח בהצלחה. זה אומר שהסיומת שלי היא עכשיו אוניברסלית ועובדת עבור כל הבקשות.
דוגמה לשליחת בקשה למילוי חשבון
הרחבות הן חלק בלתי נפרד מ-Burp Suite. לעתים קרובות שירותים מיישמים פונקציונליות מותאמת אישית שאף אחד מלבדך לא תכתוב מראש. לכן חשוב לא רק להוריד תוספים מוכנים, אלא גם לכתוב משלכם, וזה מה שניסיתי ללמד אתכם במאמר זה.
זה הכל לבינתיים, שפר את עצמך ואל תחלי.
קישור לקוד המקור של התוסף: *לחץ* .