מעבדות האקינג
למה להשתמש ב-NotHoly ל-CTF ולמעבדות
מודל שכותב את ה-exploit לאתגר שלכם ומסביר למה הוא עובד, triage סטטי לבינארי, ו-solve שרץ במעבדה שלכם, לא אצלנו.
בניית רשת Docker בלי גישה לאינטרנט, הרצת OWASP Juice Shop עליה ופרסום עותק לדפדפן רק על 127.0.0.1, עם אימות של כל שלב.
הבטיחות של מעבדה נקבעת לפי הנתיבים אליה וממנה. אם האפליקציה הפגיעה שלך יכולה להגיע לאינטרנט, היא יכולה לפנות למקומות שלא התכוונת לגעת בהם. אם היא מאזינה על כל הממשקים של המחשב, כל מי שמחובר לאותו Wi-Fi יכול להגיע אליה. Docker נותן שליטה בשני הנתיבים בעזרת כמה פקודות. המדריך הזה משתמש ב-OWASP Juice Shop כיעד ועובר על כל אחת מהן.
ניצור רשת בלי דרך החוצה, נפעיל עליה את Juice Shop, נפנה לאפליקציה מקונטיינר שני ונוכיח שהרשת לא מגיעה לאינטרנט. אחר כך נריץ עותק נפרד לדפדפן, שמפורסם רק על ממשק ה-loopback, כך ששום דבר ברשת המקומית לא יראה אותו. בידוד הוא בקרת האבטחה הראשונה בכל מעבדה, וכל תרגיל בהמשך, מסריקה ועד תרגול אקספלויטציה, נשען עליו.
אם עוד לא הרצת את Juice Shop, אפשר להתחיל בOWASP Juice Shop עם Docker. המאמר הזה מניח את יסודות הקונטיינרים מאותו מדריך.
מעבדת אבטחה מריצה תוכנה שנבנתה כדי להישבר. ב-Juice Shop יש פגמים מכוונים, ובשביל זה היא קיימת. הסיכון הוא לא באפליקציה עצמה אלא במה שקורה כשפגם, הגדרה שגויה או טעות הקלדה נותנים לאפליקציה, או לכלי שמופעל נגדה, נתיב למקום אחר.
שלושה מקרים מראים איך זה משתבש:
-p 3000:3000 פשוט. Docker מקבל אז חיבורים על כל ממשקי ה-host, ומכשירים אחרים ברשת שלך יכולים להגיע לאפליקציה הפגיעה.בידוד מכסה כל אחד מהמקרים. לרשת פנימית אין נתיב החוצה. לפורט שקשור ל-loopback אין נתיב דרך ה-LAN. רשת ייעודית שומרת את קונטיינרי המעבדה בנפרד מהפרויקטים האחרים שלך. אף אחד מהצעדים האלה לא קשה. קל לשכוח אותם כשממהרים, ולכן המדריך הזה דורש לאמת כל אחד.
שלושה רעיונות נדרשים כדי שהפקודות יהיו מובנות.
Docker מחבר קונטיינרים לרשתות וירטואליות. דרייבר ברירת המחדל הוא bridge. כל קונטיינר ברשת bridge מקבל כתובת IP משלו ב-subnet פרטי. ב-Linux ה-bridge יושב על ה-host עצמו. ב-Windows וב-macOS, Docker Desktop מריץ את המנוע בתוך מכונה וירטואלית של Linux, ולכן ה-bridge יושב בתוך ה-VM הזה.
קונטיינרים על ה-bridge של ברירת המחדל מגיעים זה לזה לפי כתובת IP ולא לפי שם. רשת bridge שיוצרים ידנית עם docker network create מתרגמת שמות קונטיינרים דרך DNS, בעזרת resolver שמובנה ב-Docker. לכן קונטיינר הכלי במדריך הזה יכול לפנות אל http://juice:3000 במקום להסתמך על IP שמשתנה אחרי הפעלה מחדש.
כשיוצרים רשת עם --internal, Docker מגביל אליה גישה חיצונית. התיעוד של Docker מתאר את זה כך: קונטיינרים ברשת פנימית יכולים לדבר זה עם זה, אבל לא עם שום דבר בחוץ. זה כולל את האינטרנט. רואים את זה בשלב 4, כשחיפוש של host חיצוני נכשל.
כברירת מחדל, פורט של קונטיינר לא מפורסם למחשבים אחרים. האפשרות -p או --publish משנה את זה. היא ממפה כתובת ופורט של ה-host לפורט של הקונטיינר, והכתובת שבוחרים קובעת אילו ממשקים מקבלים חיבורים.
-p 3000:3000 מאזין על כל ממשקי ה-host, כולל כתובת ה-LAN שלך. -p 127.0.0.1:3000:3000 מאזין רק על loopback, כך שרק תוכנות באותו מחשב יכולות להתחבר. התיעוד של Docker על פרסום פורטים מציג בדיוק את הצורה הזו. ההבדל הוא קידומת אחת, והוא משמעותי.
למעבדה יש חמישה חלקים:
juicenet, רשת פנימית בלי נתיב החוצה.juice, מופע Juice Shop שמחובר רק אל juicenet, בלי פורט מפורסם.curlimages/curl חד־פעמי ברשת juicenet שפונה אל juice לפי שם.example.com, וזה אמור להיכשל.juice-local, מופע Juice Shop שני שמפורסם על 127.0.0.1:3000 לדפדפן שלך.הקונטיינר שמבודדים הוא juice. הקונטיינר juice-local הוא עותק נוח לגלישה. הוא רץ על רשת ברירת המחדל, ולכן הוא יכול להגיע לאינטרנט. צריך לזכור את זה ולא להכניס לאפליקציה פרטי הזדהות אמיתיים או מידע אישי.
צריך:
docker info אמורה להדפיס פרטי שרת בלי שגיאות. על המחשב ששימש למדריך רץ Docker Engine 29.8.2 על Windows 11 דרך Docker Desktop.-w שבמירכאות בודדות.כדאי לבדוק שמות לפני שבוחרים אותם. פרויקטים אחרים במחשב שלך אולי כבר משתמשים בשמות כמו juice או juicenet. קודם מריצים את זה:
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'אם באחת הרשימות מופיע juice, juice-local או juicenet, צריך לבחור שמות אחרים או להסיר קודם את הישנים. אין למחוק שום דבר שלא יצרת.
כדאי לבדוק גם את ה-image שעומדים להריץ. Juice Shop מפרסם תוויות גרסה על ה-image שלו:
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'הפקודה הזו נכשלת כל עוד ה-image לא נמצא מקומית, כי הפקודה docker run הראשונה היא שמורידה אותו. על המחשב ששימש למדריך ה-image דיווח על הגרסה 20.3.0. אצלך ייתכן שתופיע גרסה אחרת, ולכן כדאי לרשום אותה.
docker network create --internal juicenetDocker מדפיס את המזהה של הרשת החדשה. אצלך הוא יהיה שונה. זה מה שהודפס על מחשב הבדיקה:
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9לאמת שהדגל נכנס לתוקף:
docker network inspect juicenet --format '{{.Internal}}'trueאם הפלט הוא false, הרשת נוצרה בלי --internal. צריך להסיר אותה עם docker network rm juicenet וליצור אותה מחדש. אין להמשיך לפני שמופיע true, כי כל הבדיקות בהמשך מניחות שהדגל פעיל.
docker run -d --name juice --network juicenet bkimminich/juice-shopלכל אפשרות יש תפקיד. -d מריץ את הקונטיינר ברקע. --name juice נותן לו שם DNS שקונטיינרים אחרים ברשת juicenet יכולים לתרגם. --network juicenet מחבר אותו רק לרשת הפנימית הזו. אין כאן -p, ולכן הקונטיינר לא מפרסם שום פורט ל-host.
Docker מדפיס את מזהה הקונטיינר:
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8האפליקציה בפנים מאזינה על פורט 3000. ל-Juice Shop לוקח זמן לעלות אחרי שהקונטיינר פעיל, ולכן אין לבדוק מיד. משתמשים בלולאה שבודקת אם קונטיינר כלי מקבל HTTP 200:
for i in $(seq 1 30); do
s=$(docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000)
echo "try $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneבזמן שהאפליקציה עולה, הלולאה מדפיסה 000, כלומר curl לא הצליח להתחבר. על מחשב הבדיקה היא הדפיסה 000 ארבע פעמים ואז 200 בניסיון החמישי. אצלך ייתכן שיידרשו יותר או פחות ניסיונות. הלולאה מוותרת אחרי 30 ניסיונות. אם 200 לא מופיע אף פעם, צריך לחפש שגיאות בפלט של docker logs juice.
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000200כדאי לקרוא כל חלק. --rm מוחק את קונטיינר הכלי כשהוא מסיים. --network juicenet שם את הכלי באותה רשת פנימית כמו האפליקציה. curlimages/curl הוא image קטן שמכיל curl. הכתובת http://juice:3000 משתמשת בשם הקונטיינר, ש-Docker מתרגם בתוך juicenet. הדגל -s מסתיר את פס ההתקדמות, -o /dev/null זורק את גוף התשובה, והדגל -w מדפיס רק את קוד הסטטוס.
זו התבנית שתחזור בהמשך. סורק, proxy או סקריפט מצטרפים לרשת המעבדה ומכוונים אל הקונטיינר לפי שם. שום דבר ב-host שלך לא צריך ניתוב אל האפליקציה.
תג ה-image כאן לא מקובע, ולכן Docker מושך את מה שהוא latest היום. במעבדה שמתכננים לחזור עליה, כדאי לקבע את התג אחרי שבדקת אותו.
בדיקה שעוברת שווה משהו רק כשלידה יש בדיקה שנכשלת. שלב 3 הראה שקונטיינר הכלי מגיע לאפליקציה, כלומר הרשת עובדת. עכשיו בודקים את הכיוון השני:
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"curl: (28) Resolving timed out after 5000 milliseconds
exit=28הדגל -m 5 מגביל את הבקשה לחמש שניות. -sS מסתיר את פס ההתקדמות ומשאיר שגיאות גלויות. קוד יציאה 28 הוא קוד ה-timeout של curl. הכישלון קורה בתרגום השם: החיפוש של example.com לא מקבל תשובה, ולכן curl אף פעם לא פותח חיבור. זה מה שמצפים לו מרשת בלי נתיב החוצה.
זו התוצאה הרצויה. המעבדה מגיעה לקונטיינרים שלה ולא מגיעה החוצה. כדאי לשמור את הפלט של שלבים 3 ו-4 בהערות המעבדה, עם התאריך וגרסת Docker. התיעוד הזה עוזר כשחוזרים ומשהו השתנה.
הדפדפן שלך לא יכול לתרגם את השם juice, והקונטיינר juice לא מפרסם שום פורט, ולכן אי אפשר להגיע אליו מה-host. כדי לגלוש, מפעילים עותק שני ומפרסמים אותו על loopback:
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shopהאפשרות -p 127.0.0.1:3000:3000 ממפה את פורט 3000 בממשק ה-loopback לפורט 3000 בתוך הקונטיינר. העותק הזה משתמש ב-bridge של ברירת המחדל ולא ברשת juicenet, ולכן הוא לא מבודד מהאינטרנט. זו פשרה מכוונת לעותק גלישה. כדי לקבל משהו הדוק יותר, צריך לתכנן ולבדוק תצורה נפרדת.
לעותק לוקח רגע לעלות. משתמשים באותו סוג של לולאה, הפעם מול ה-host:
for i in $(seq 1 30); do
s=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
echo "try $i: $s"
[ "$s" = "200" ] && break
sleep 3
doneעל מחשב הבדיקה העותק ענה 200 בניסיון העשירי. עכשיו מאמתים היכן הפורט קשור:
docker port juice-local3000/tcp -> 127.0.0.1:3000בשלב הבא בודקים את צד ה-LAN. את כתובת ה-IPv4 של המחשב ברשת המקומית מוצאים עם ipconfig ב-Windows או ip addr ב-Linux. אחר כך מבקשים את האפליקציה בכתובת הזו:
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://YOUR-LAN-IP:3000/צריך להחליף את YOUR-LAN-IP בכתובת שלך. על מחשב הבדיקה זה הדפיס 000 ו-curl יצא עם קוד 7, כלומר לא הצליח להתחבר. אותה בקשה אל 127.0.0.1 החזירה 200. בשביל הפער הזה קושרים ל-loopback.
עכשיו פותחים את http://127.0.0.1:3000 בדפדפן. אמור להופיע דף הבית של Juice Shop. משתמשים באפליקציה רק מאותו מחשב.
מריצים docker ps כדי לראות את שני הקונטיינרים זה לצד זה:
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'NAMES PORTS
juice-local 127.0.0.1:3000->3000/tcp
juice 3000/tcpבשורה של juice מופיע 3000/tcp בלי כתובת host. הפורט קיים בתוך הרשת אבל לא ממופה ל-host. בשורה של juice-local מופיע הקישור ל-host.
עוצרים ומסירים את המעבדה. הקונטיינרים צריכים לרדת לפני הרשת, כי Docker מסרב להסיר רשת שעדיין מחוברים אליה קונטיינרים:
docker rm -f juice juice-local && docker network rm juicenetjuice
juice-local
juicenetDocker מדפיס את השמות שהסיר. אחר כך מאמתים שלא נשאר כלום:
docker ps -a --filter name=juice --format '{{.Names}}'
docker network ls --filter name=juicenet --format '{{.Name}}'שתי הפקודות לא אמורות להדפיס כלום. מסננים תופסים חלק משם, ולכן צריך לקרוא את הפלט לפני שמניחים שהוא שלם, ולהסיר רק את מה שהפעלת. הדגל --rm מנקה רק את קונטיינרי הכלים החד־פעמיים. קונטיינרים שהופעלו עם -d נשארים עד שמסירים אותם, ובשביל זה השלב הזה קיים.
לפרסם בלי כתובת. -p 3000:3000 מאזין על כל ממשקי ה-host. לכל אפליקציית מעבדה שפותחים מאותו מחשב צריך להשתמש בקידומת 127.0.0.1:, ולבדוק את התוצאה עם docker port.
לשכוח את --internal. הרשת עובדת גם בלי הדגל ושום דבר לא מזהיר. צריך להריץ docker network inspect juicenet --format '{{.Internal}}' ולחפש true.
לחבר את האפליקציה לרשת ברירת המחדל. קונטיינר שהופעל בלי --network מצטרף ל-bridge של ברירת המחדל, שמגיע החוצה. אם האפליקציה חייבת להישאר מבודדת, צריך לציין את הרשת בכל פעם.
לסמוך על בדיקה שנכשלה. כישלון בשלב 4 יכול לנבוע מ-DNS, מ-proxy, מ-VPN או מטעות הקלדה. שלב 3 הוא הבקרה שמראה שהבדיקה מודדת את הדבר הנכון.
להשתמש בשם מה-host. השם juice מתורגם רק עבור קונטיינרים ברשת juicenet. הדפדפן שלך לא יכול להשתמש בו. לכן קיים העותק לדפדפן.
להשאיר קונטיינרים רצים. קונטיינר ברקע ממשיך לצרוך משאבים ולהחזיק את הפורט שלו. כל סשן צריך להסתיים בשלב הניקוי ובהרצה של docker ps אחריו.
האפשרות של רשת פנימית היא בקרה, ויש לה מגבלות.
היא לא מונעת מכלי ברשת juicenet להגיע לכל קונטיינר אחר באותה רשת. קונטיינרים באותה רשת יכולים לדבר זה עם זה, ועל זה שלב 3 מתבסס. שירותים לא קשורים צריכים להישאר מחוץ לרשת המעבדה.
פרסום על loopback מגן מפני מחשבים אחרים ב-LAN שלך. הוא לא מגן מפני תוכנות שכבר רצות על המחשב שלך, כי גם הן יכולות להתחבר אל 127.0.0.1.
הבדיקה בודקת שם אחד ו-host חיצוני אחד. היא לא מוכיחה שכל נתיבי היציאה סגורים. ההתנהגות של Docker יכולה להיות שונה בין Docker Desktop, מנועי Linux ותצורות rootless, ולכן כדאי להריץ את הבדיקות מחדש על המחשב שמשתמשים בו בפועל.
ולסיום, המעבדה הזו מראה רק שהבידוד עובד. היא לא מראה ש-Juice Shop בטוחה לחשיפה. היא לא בטוחה, ובגלל זה מבודדים אותה.
כשהמעבדה עובדת, כמה שינויים קטנים הופכים אותה לשימושית יותר:
juicenet, למשל מסד נתונים, ולאמת שהאפליקציה מגיעה אליו לפי שם. כך בונים מעבדות מרובות קונטיינרים.juicenet היא יעד ראשון טוב, כי כל קונטיינר בה בשליטתך.מעבדות האקינג
מודל שכותב את ה-exploit לאתגר שלכם ומסביר למה הוא עובד, triage סטטי לבינארי, ו-solve שרץ במעבדה שלכם, לא אצלנו.
מעבדות האקינג
מבנה של דוח פנטסט וכתיבת ממצא אחד שמישהו אחר יכול לשחזר, עם ראיות, השפעה, וקטור CVSS v4.0 ותיקון.
מעבדות האקינג
בונים מעבדה ביתית שמותר לתקוף: קובץ סקופ, פורטים של Docker שקשורים ל-loopback, רשת מבודדת, יומן עבודה ופירוק נקי.