דילוג לתוכן
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

איך ליצור רשת Docker מבודדת למעבדה שלך

בניית רשת Docker בלי גישה לאינטרנט, הרצת OWASP Juice Shop עליה ופרסום עותק לדפדפן רק על 127.0.0.1, עם אימות של כל שלב.

מתחילים10 דקות קריאהמאת NotHoly

הבטיחות של מעבדה נקבעת לפי הנתיבים אליה וממנה. אם האפליקציה הפגיעה שלך יכולה להגיע לאינטרנט, היא יכולה לפנות למקומות שלא התכוונת לגעת בהם. אם היא מאזינה על כל הממשקים של המחשב, כל מי שמחובר לאותו Wi-Fi יכול להגיע אליה. Docker נותן שליטה בשני הנתיבים בעזרת כמה פקודות. המדריך הזה משתמש ב-OWASP Juice Shop כיעד ועובר על כל אחת מהן.

ניצור רשת בלי דרך החוצה, נפעיל עליה את Juice Shop, נפנה לאפליקציה מקונטיינר שני ונוכיח שהרשת לא מגיעה לאינטרנט. אחר כך נריץ עותק נפרד לדפדפן, שמפורסם רק על ממשק ה-loopback, כך ששום דבר ברשת המקומית לא יראה אותו. בידוד הוא בקרת האבטחה הראשונה בכל מעבדה, וכל תרגיל בהמשך, מסריקה ועד תרגול אקספלויטציה, נשען עליו.

אם עוד לא הרצת את Juice Shop, אפשר להתחיל בOWASP Juice Shop עם Docker. המאמר הזה מניח את יסודות הקונטיינרים מאותו מדריך.

למה בידוד קודם לכול

מעבדת אבטחה מריצה תוכנה שנבנתה כדי להישבר. ב-Juice Shop יש פגמים מכוונים, ובשביל זה היא קיימת. הסיכון הוא לא באפליקציה עצמה אלא במה שקורה כשפגם, הגדרה שגויה או טעות הקלדה נותנים לאפליקציה, או לכלי שמופעל נגדה, נתיב למקום אחר.

שלושה מקרים מראים איך זה משתבש:

  • לאפליקציה יש פיצ׳ר שמביא URL, ו-payload שנבדק גורם לה לבקש host שלא התכוונת אליו. בלי בקרת יציאה, הבקשה עוזבת את המחשב שלך.
  • קונטיינר של כלי מריץ פקודה נגד כתובת עם טעות הקלדה. בלי גבול רשת, הכתובת הזו יכולה להיות של הנתב שלך, של המעסיק שלך או של שרת של מישהו זר.
  • האפליקציה מפורסמת עם -p 3000:3000 פשוט. Docker מקבל אז חיבורים על כל ממשקי ה-host, ומכשירים אחרים ברשת שלך יכולים להגיע לאפליקציה הפגיעה.

בידוד מכסה כל אחד מהמקרים. לרשת פנימית אין נתיב החוצה. לפורט שקשור ל-loopback אין נתיב דרך ה-LAN. רשת ייעודית שומרת את קונטיינרי המעבדה בנפרד מהפרויקטים האחרים שלך. אף אחד מהצעדים האלה לא קשה. קל לשכוח אותם כשממהרים, ולכן המדריך הזה דורש לאמת כל אחד.

רשתות Docker בחמש דקות

שלושה רעיונות נדרשים כדי שהפקודות יהיו מובנות.

רשתות bridge

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 מותקן עם מנוע פעיל. הפקודה docker info אמורה להדפיס פרטי שרת בלי שגיאות. על המחשב ששימש למדריך רץ Docker Engine 29.8.2 על Windows 11 דרך Docker Desktop.
  • פורט 3000 פנוי על ה-host עבור העותק לדפדפן.
  • shell. הדוגמאות משתמשות ב-Git Bash על Windows 11. הפקודות עובדות אותו דבר ב-shell של macOS ושל Linux. ב-PowerShell צריך לכתוב אחרת את מחרוזות הפורמט של -w שבמירכאות בודדות.

כדאי לבדוק שמות לפני שבוחרים אותם. פרויקטים אחרים במחשב שלך אולי כבר משתמשים בשמות כמו juice או juicenet. קודם מריצים את זה:

bash
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'

אם באחת הרשימות מופיע juice, juice-local או juicenet, צריך לבחור שמות אחרים או להסיר קודם את הישנים. אין למחוק שום דבר שלא יצרת.

כדאי לבדוק גם את ה-image שעומדים להריץ. Juice Shop מפרסם תוויות גרסה על ה-image שלו:

bash
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'

הפקודה הזו נכשלת כל עוד ה-image לא נמצא מקומית, כי הפקודה docker run הראשונה היא שמורידה אותו. על המחשב ששימש למדריך ה-image דיווח על הגרסה 20.3.0. אצלך ייתכן שתופיע גרסה אחרת, ולכן כדאי לרשום אותה.

שלב 1: ליצור את הרשת הפנימית

bash
docker network create --internal juicenet

Docker מדפיס את המזהה של הרשת החדשה. אצלך הוא יהיה שונה. זה מה שהודפס על מחשב הבדיקה:

text
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9

לאמת שהדגל נכנס לתוקף:

bash
docker network inspect juicenet --format '{{.Internal}}'
text
true

אם הפלט הוא false, הרשת נוצרה בלי --internal. צריך להסיר אותה עם docker network rm juicenet וליצור אותה מחדש. אין להמשיך לפני שמופיע true, כי כל הבדיקות בהמשך מניחות שהדגל פעיל.

שלב 2: להפעיל את Juice Shop ברשת הפנימית

bash
docker run -d --name juice --network juicenet bkimminich/juice-shop

לכל אפשרות יש תפקיד. -d מריץ את הקונטיינר ברקע. --name juice נותן לו שם DNS שקונטיינרים אחרים ברשת juicenet יכולים לתרגם. --network juicenet מחבר אותו רק לרשת הפנימית הזו. אין כאן -p, ולכן הקונטיינר לא מפרסם שום פורט ל-host.

Docker מדפיס את מזהה הקונטיינר:

text
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8

האפליקציה בפנים מאזינה על פורט 3000. ל-Juice Shop לוקח זמן לעלות אחרי שהקונטיינר פעיל, ולכן אין לבדוק מיד. משתמשים בלולאה שבודקת אם קונטיינר כלי מקבל HTTP 200:

bash
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.

שלב 3: להגיע לאפליקציה מקונטיינר כלי

bash
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000
text
200

כדאי לקרוא כל חלק. --rm מוחק את קונטיינר הכלי כשהוא מסיים. --network juicenet שם את הכלי באותה רשת פנימית כמו האפליקציה. curlimages/curl הוא image קטן שמכיל curl. הכתובת http://juice:3000 משתמשת בשם הקונטיינר, ש-Docker מתרגם בתוך juicenet. הדגל -s מסתיר את פס ההתקדמות, -o /dev/null זורק את גוף התשובה, והדגל -w מדפיס רק את קוד הסטטוס.

זו התבנית שתחזור בהמשך. סורק, proxy או סקריפט מצטרפים לרשת המעבדה ומכוונים אל הקונטיינר לפי שם. שום דבר ב-host שלך לא צריך ניתוב אל האפליקציה.

תג ה-image כאן לא מקובע, ולכן Docker מושך את מה שהוא latest היום. במעבדה שמתכננים לחזור עליה, כדאי לקבע את התג אחרי שבדקת אותו.

שלב 4: להוכיח שאין לרשת אינטרנט

בדיקה שעוברת שווה משהו רק כשלידה יש בדיקה שנכשלת. שלב 3 הראה שקונטיינר הכלי מגיע לאפליקציה, כלומר הרשת עובדת. עכשיו בודקים את הכיוון השני:

bash
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"
text
curl: (28) Resolving timed out after 5000 milliseconds
exit=28

הדגל -m 5 מגביל את הבקשה לחמש שניות. -sS מסתיר את פס ההתקדמות ומשאיר שגיאות גלויות. קוד יציאה 28 הוא קוד ה-timeout של curl. הכישלון קורה בתרגום השם: החיפוש של example.com לא מקבל תשובה, ולכן curl אף פעם לא פותח חיבור. זה מה שמצפים לו מרשת בלי נתיב החוצה.

זו התוצאה הרצויה. המעבדה מגיעה לקונטיינרים שלה ולא מגיעה החוצה. כדאי לשמור את הפלט של שלבים 3 ו-4 בהערות המעבדה, עם התאריך וגרסת Docker. התיעוד הזה עוזר כשחוזרים ומשהו השתנה.

שלב 5: להריץ עותק לדפדפן על loopback בלבד

הדפדפן שלך לא יכול לתרגם את השם juice, והקונטיינר juice לא מפרסם שום פורט, ולכן אי אפשר להגיע אליו מה-host. כדי לגלוש, מפעילים עותק שני ומפרסמים אותו על loopback:

bash
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:

bash
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 בניסיון העשירי. עכשיו מאמתים היכן הפורט קשור:

bash
docker port juice-local
text
3000/tcp -> 127.0.0.1:3000

בשלב הבא בודקים את צד ה-LAN. את כתובת ה-IPv4 של המחשב ברשת המקומית מוצאים עם ipconfig ב-Windows או ip addr ב-Linux. אחר כך מבקשים את האפליקציה בכתובת הזו:

bash
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 כדי לראות את שני הקונטיינרים זה לצד זה:

bash
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'
text
NAMES         PORTS
juice-local   127.0.0.1:3000->3000/tcp
juice         3000/tcp

בשורה של juice מופיע 3000/tcp בלי כתובת host. הפורט קיים בתוך הרשת אבל לא ממופה ל-host. בשורה של juice-local מופיע הקישור ל-host.

שלב 6: ניקוי

עוצרים ומסירים את המעבדה. הקונטיינרים צריכים לרדת לפני הרשת, כי Docker מסרב להסיר רשת שעדיין מחוברים אליה קונטיינרים:

bash
docker rm -f juice juice-local && docker network rm juicenet
text
juice
juice-local
juicenet

Docker מדפיס את השמות שהסיר. אחר כך מאמתים שלא נשאר כלום:

bash
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 בטוחה לחשיפה. היא לא בטוחה, ובגלל זה מבודדים אותה.

איך להמשיך

כשהמעבדה עובדת, כמה שינויים קטנים הופכים אותה לשימושית יותר:

  • לכתוב סקריפט קצר שמריץ את הבדיקות משלבים 3 עד 5 ומדפיס pass או fail לכל אחת. להריץ אותו בכל פעם שמעלים את המעבדה.
  • להוסיף שירות שני לרשת juicenet, למשל מסד נתונים, ולאמת שהאפליקציה מגיעה אליו לפי שם. כך בונים מעבדות מרובות קונטיינרים.
  • לקרוא בתיעוד הרשתות של Docker על אפשרויות דרייבר שעוד לא השתמשת בהן, למשל user-defined bridges עם subnets מותאמים.
  • ללמוד את יסודות גילוי ה-hosts לפני שסורקים משהו. המאמר הבא עוסק ביסודות Nmap במעבדה שלך. לסרוק רק רשתות ו-hosts שהם שלך או שיש לך הרשאה בכתב לבדוק. הרשת juicenet היא יעד ראשון טוב, כי כל קונטיינר בה בשליטתך.

מקורות

מקורות

  1. Docker Engine networking overview
  2. docker network create (CLI reference)
  3. Docker port publishing and mapping
  4. OWASP Juice Shop (GitHub repository)

עוד בנושא מעבדות האקינג

מעבדות האקינג

למה להשתמש ב-NotHoly ל-CTF ולמעבדות

מודל שכותב את ה-exploit לאתגר שלכם ומסביר למה הוא עובד, triage סטטי לבינארי, ו-solve שרץ במעבדה שלכם, לא אצלנו.

ביניים2 דקות קריאה

מעבדות האקינג

איך כותבים דוח פנטסט טוב

מבנה של דוח פנטסט וכתיבת ממצא אחד שמישהו אחר יכול לשחזר, עם ראיות, השפעה, וקטור CVSS v4.0 ותיקון.

ביניים7 דקות קריאה

מעבדות האקינג

איך לבנות מעבדה ביתית חוקית

בונים מעבדה ביתית שמותר לתקוף: קובץ סקופ, פורטים של Docker שקשורים ל-loopback, רשת מבודדת, יומן עבודה ופירוק נקי.

מתחילים3 דקות קריאה