Linux וכלי פיתוח
הרשאות ב-Linux: משתמשים, קבוצות, chmod ו-umask
בעלים וקבוצות ב-Linux, ביטי rwx, chmod, umask ו-setuid, עם מעבדה שבודקת גישה כמשתמש שני ומוצאת הרשאות קבצים מסוכנות.
pipes, הפניות, משתנים, לולאות, קודי יציאה ו-set -euo pipefail, ואז שימוש בהם כדי לספור התחברויות כושלות ולחשב hash לתיקייה.
Bash הוא ה-shell שמאחורי רוב הטרמינלים של Linux. הוא רץ גם ב-macOS, בתוך קונטיינרים רבים וב-Git Bash ב-Windows, הסביבה שעליה נבדק המאמר הזה. למי שלומד אבטחה זו הדרך המהירה ביותר לשאול קובץ לוג שאלה, לבדוק שינויים בתיקייה או להריץ את אותה בדיקה על קבצים רבים. לא צריך לדעת לתכנת כדי להשתמש בו. צריך טרמינל, עורך טקסט וכשעה.
המאמר מתחיל במה שה-shell עושה, ואז מכסה את ההרגלים שהופכים עבודת shell לאמינה: pipes, הפניות, משתנים, לולאות וקודי יציאה. אחר כך מהדקים סקריפט עם set -euo pipefail. בסוף יש שני תרגילים קטנים. בראשון סופרים התחברויות SSH כושלות לכל כתובת IP בלוג לדוגמה. בשני כותבים סקריפט קצר שרושם hash של SHA-256 לכל קובץ בתיקייה.
הכול כאן רץ על קבצים שיוצרים בעצמך. שום דבר במאמר לא נוגע במערכת אחרת.
Bash הוא קיצור של Bourne Again SHell. shell קורא את השורות שמקלידים, מריץ תוכנות אחרות ומדפיס את מה שהן מפיקות. Bash הוא אחד מכמה shells, לצד sh, zsh וגם fish. הוא ברירת המחדל בהפצות Linux רבות, ורוב הסקריפטים שנמצאים ברשת מניחים אותו.
שני מונחים חוזרים בכל סעיף. פקודה היא שם תוכנה יחד עם הארגומנטים שלה, כמו grep -c Failed sample.log. סקריפט הוא קובץ טקסט שמפרט פקודות, ו-Bash קורא אותו מלמעלה למטה. סקריפט מאפשר לחזור על עבודה בלי להקליד אותה מחדש, ומאפשר למישהו אחר לקרוא בדיוק מה נעשה.
המקור לכל התנהגות שמתוארת במאמר הזה הוא GNU Bash Reference Manual. כשתוצאה מפתיעה, בודקים במדריך לפני שסומכים על ניחוש.
רוב נתוני האבטחה הם טקסט. לוגי אימות, לוגי גישה של שרתי web, לוגי שאילתות DNS, דוחות סורקים ורשימות hash מגיעים כולם כשורות. חלק גדול מהעבודה היומיומית הוא לשאול את השורות האלה שאלות. כמה פעמים הכתובת הזאת נכשלה בהתחברות? אילו קבצים השתנו מאז השבוע שעבר? איזה חשבון הופיע היום לראשונה?
גיליון אלקטרוני או תוכנית Python יכולים לענות על כל אחת מהשאלות האלה. Bash עונה עליהן לעיתים קרובות מהר יותר, בשורה אחת, וכל שלב נשאר גלוי. למתחילים הנראות הזאת חשובה יותר מהמהירות. כשתוצאה נראית שגויה, מריצים לבד את החלק הראשון של הצינור ורואים מה הוא הפיק.
פקודות shell מופיעות גם במדריכי התקנה, בנקודות כניסה של קונטיינרים ובמשימות אוטומציה. קריאה שלהן היא חלק מהעבודה, גם כשמעדיפים לכתוב Python.
pipe, שנכתב בסימן |, מחבר את הפלט הסטנדרטי של הפקודה השמאלית לקלט הסטנדרטי של הפקודה הימנית. רוב כלי שורת הפקודה קוראים שורות, עושים עבודה אחת ומדפיסים שורות. ה-shell מחבר ביניהם.
echo "one two three" | wc -wפלט:
3wc -w סופר מילים. הוא לא צריך שם קובץ כי ה-pipe מזין לו את הטקסט מהפקודה echo. תוכנות קטנות שמחוברות ב-pipes הן ההרגל המרכזי של עבודת shell.
הפניה משנה מאיפה פקודה קוראת או לאן היא כותבת. > כותב פלט סטנדרטי לקובץ ומחליף את התוכן שלו. >> מוסיף לסוף. 2> שולח שגיאה סטנדרטית, הערוץ שבו תוכנות מדפיסות את התלונות שלהן, לקובץ. 2>&1 שולח שגיאה סטנדרטית לאותו מקום כמו הפלט הסטנדרטי.
echo first > notes.txt
echo second >> notes.txt
cat notes.txtפלט:
first
secondה-shell פותח ומרוקן את הקובץ לפני שהפקודה רצה. לכן הפקודה הזאת מוחקת את הנתונים שהיא הייתה אמורה למיין:
sort d.txt > d.txtאחרי השורה הזאת d.txt ריק. בקובץ בדיקה, התוצאה הייתה אפס בתים. כותבים לקובץ חדש, בודקים את התוצאה, ואז מחליפים את הישן.
שגיאה סטנדרטית חשובה בעבודת אבטחה כי שגיאות מחזיקות לעיתים קרובות את התשובה. קובץ חסר או הודעת permission denied הם ראיה. הפניית שגיאות עם 2>/dev/null הופכת את הפלט לשקט יותר, אבל אז קובץ חסר נראה כמו קובץ בלי התאמות. בזמן הלמידה עדיף להשאיר שגיאות גלויות.
ls missing-file 2>/dev/null
echo "status=$?"פלט:
status=2טקסט השגיאה הלך לקובץ ההשלכה, ולכן הטרמינל מציג רק את קוד הסטטוס.
משתנה שומר ערך. מקצים אותו בלי רווחים סביב סימן השוויון. כדי לקרוא אותו, שמים סימן דולר לפני השם.
target="10.0.0.5"
echo "Checking $target"עוטפים משתנים במירכאות כפולות. משתנה בלי מירכאות מתפצל ברווחים, והחלקים יכולים להתרחב כ-wildcards. ההבדל מופיע ברגע שערך מכיל רווח:
name="a b"
printf "[%s]\n" $name
printf "[%s]\n" "$name"פלט:
[a]
[b]
[a b]הפקודה printf הראשונה קיבלה שתי מילים, כי ה-shell פיצל את הערך. השנייה קיבלה אחת. אותו דבר קורה עם קובץ בשם my report.txt כשמקלידים cat $file בלי מירכאות.
סקריפטים מקבלים ארגומנטים כמשתנים ממוספרים. $1 הוא הארגומנט הראשון, $2 השני. הביטוי "${1:-.}" פירושו "להשתמש בארגומנט הראשון אם הוא מוגדר ולא ריק, אחרת בתיקייה הנוכחית". סקריפט ה-hashing משתמש בו.
לולאת for מריצה את הגוף שלה פעם אחת לכל מילה ברשימה. בדיקה בסוגריים מרובעים, כמו [ "$n" -eq 2 ], מחזירה 0 כשהתנאי נכון ו-1 כשהוא שגוי. משפט if משתמש בסטטוס הזה כדי לבחור ענף.
for n in 1 2 3; do
if [ "$n" -eq 2 ]; then
echo "stop at $n"
break
fi
echo "ok $n"
doneפלט:
ok 1
stop at 2break יוצא מהלולאה מוקדם, ולכן הפריט השלישי אף פעם לא רץ.
כל פקודה מסתיימת בסטטוס יציאה, מספר מ-0 עד 255. אפס פירושו הצלחה. כל דבר אחר פירושו כישלון, וכל תוכנה מגדירה את המספרים שלה. grep של GNU מחזיר 0 כשהוא בוחר לפחות שורה אחת, 1 כשאין התאמה, ו-2 בשגיאה. קוראים את הסטטוס עם $? בשורה הבאה.
grep -q Failed sample.log
echo "match=$?"
grep -q nomatch sample.log
echo "nomatch=$?"פלט:
match=0
nomatch=1האפשרות -q מדכאת פלט וקובעת רק את הסטטוס, וכך אפשר להשתמש בפקודה grep בתוך if בלי להדפיס כלום.
לצינור יש סטטוס אחד, וכברירת מחדל זה הסטטוס של הפקודה האחרונה שלו. הסעיף הבא משנה את זה.
Bash ממשיך לרוץ אחרי שגיאות אלא אם אומרים לו אחרת. שלוש אפשרויות גורמות לסקריפט להיכשל מוקדם. הן מתועדות תחת ה-builtin set במדריך.
set -e עוצר את הסקריפט כשפקודה נכשלת. פקודות בבדיקת if, ופקודות ברשימת && או || מלבד האחרונה, לא עוצרות אותו, כי Bash מצפה שהן ייכשלו לפעמים.set -u מתייחס למשתנה לא מוגדר כשגיאה, כך ששגיאת הקלדה בשם משתנה עוצרת את ההרצה במקום להפוך בשקט למחרוזת ריקה.set -o pipefail גורם לצינור להיכשל אם פקודה כלשהי בו נכשלת, ולא רק האחרונה.קודם התנהגות ברירת המחדל:
bash -c 'false | true; echo "status=$?"'פלט:
status=0הפקודה false נכשלה, אבל true הגיעה אחרונה והצליחה, ולכן הצינור דיווח על הצלחה. עם pipefail:
bash -c 'set -o pipefail; false | true; echo "status=$?"'פלט:
status=1עכשיו set -e עם פקודה שנכשלת:
bash -c 'set -e; echo one; false; echo two'פלט:
oneהסקריפט נעצר לפני two. הקריאה bash -c עצמה יצאה עם סטטוס 1.
set -u תופס ארגומנט חסר:
bash -c 'set -u; echo "$1"' _פלט:
_: line 1: $1: unbound variableהצורה המשולבת, set -euo pipefail, היא מה שרוב האנשים מתכוונים אליו במצב קפדני. יש לזה מחיר. חיפוש שלא מוצא כלום הוא תוצאה רגילה, אבל grep מדווח עליה בסטטוס 1, ולכן במצב קפדני הסקריפט נעצר. בודקים את המקרה לפני שמסתמכים עליו:
bash -c 'set -euo pipefail; grep "Nope" sample.log | wc -l; echo "after"'פלט:
0הסקריפט הדפיס את הספירה ואז נעצר בלי להדפיס after. אם "אין התאמות" היא תוצאה תקינה עבור הסקריפט שלך, מוסיפים || true אחרי הצינור, או בודקים את הסטטוס בעצמך.
בונים קובץ לוג קטן, ואז סופרים כמה פעמים כל כתובת מקור מופיעה בשורות של סיסמה שגויה. הלוג סינתטי. הכתובות שלו מגיעות מהטווח הפרטי 10.0.0.0/8, והשורות הולכות לפי הפורמט של הודעות OpenSSH במארח מעבדה. לוגי אימות אמיתיים מכילים כתובות של אנשים אמיתיים, ולכן מתייחסים אליהם כמידע פרטי וקוראים רק לוגים שמותר לך לקרוא.
יוצרים תיקייה למעבדה הזאת, נכנסים אליה וכותבים את הקובץ. המחרוזת 'EOF' במירכאות מונעת מ-Bash להרחיב משהו בתוך הטקסט.
cat > sample.log <<'EOF'
Jan 10 10:00:01 lab sshd[1]: Failed password for root from 10.0.0.5 port 5000 ssh2
Jan 10 10:00:03 lab sshd[2]: Failed password for admin from 10.0.0.5 port 5001 ssh2
Jan 10 10:01:00 lab sshd[3]: Accepted publickey for alice from 10.0.0.9 port 5002 ssh2
Jan 10 10:02:00 lab sshd[4]: Failed password for bob from 10.0.0.7 port 5003 ssh2
EOFבודקים את מספר השורות עם wc -l sample.log. הוא מדווח 4.
grep 'Failed password' sample.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -nrפלט:
2 10.0.0.5
1 10.0.0.7קוראים את הצינור שלב אחר שלב. מריצים לבד את שתי הפקודות הראשונות כדי לראות מה כל שלב תורם.
grep 'Failed password' sample.log שומר את שלוש השורות הכושלות ומשמיט את השורה עם Accepted. מאשרים את הספירה עם grep -c 'Failed password' sample.log, שמדפיס 3.awk '{print $(NF-3)}' מדפיס שדה אחד מכל שורה. NF מחזיק את מספר השדות בשורה הנוכחית. הסיומת של כל שורה היא from ADDRESS port PORT ssh2, ולכן הכתובת היא השדה הרביעי מהסוף, כלומר $(NF-3). מאשרים את זה בשורה הראשונה:sed -n '1p' sample.log | awk '{print NF, $(NF-3)}'פלט:
14 10.0.0.5לשורה יש 14 שדות, ושדה 11 הוא הכתובת.
sort מניח כתובות זהות זו ליד זו. uniq -c סופר כפילויות סמוכות ומדפיס את הספירה לפניהן. בלי הפקודה sort, uniq היה סופר רק חזרות שכנות, ולכן המיון תמיד בא קודם.sort -nr ממיין את הספירות מספרית, מהגדולה לקטנה.מספר השדה של awk שייך לפורמט הלוג הזה. אם מכוונים את אותה פקודה ללוג אחר, בודקים קודם את מיקום השדה עם השורה של sed שלמעלה. מספר שעבד פעם אחת הוא לא הוכחה שהוא מתאים לקובץ הבא.
אפשר לקבל את אותה תוצאה מתוכנית awk אחת, כי מערכים של awk סופרים בשבילך:
grep 'Failed password' sample.log | awk '{c[$(NF-3)]++} END {for (ip in c) print c[ip], ip}'פלט:
2 10.0.0.5
1 10.0.0.7awk לא מבטיח סדר עבור for (ip in c), ולכן ממיינים את התוצאה אם הסדר חשוב. GNU Awk User's Guide מכסה מערכים ואת הכלל END.
hash הוא טביעת אצבע באורך קבוע של תוכן קובץ. אם רושמים היום את ה-hash של SHA-256 לכל קובץ בתיקייה, אפשר להריץ את אותה פקודה בשבוע הבא ולראות אילו קבצים השתנו. hash לא מוכיח מי ביצע שינוי או אם קובץ בטוח. הוא מראה רק אם הבתים השתנו מאז שנרשמו.
יוצרים את hashall.sh באותה תיקייה, באותו דפוס של heredoc במירכאות.
cat > hashall.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do [ -f "$f" ] && sha256sum "$f"; done
EOFקוראים את הסקריפט שורה אחר שורה:
env.set -euo pipefail מפעיל מצב קפדני, כפי שתואר קודם."${1:-.}"/* הוא התיקייה שמעבירים, או התיקייה הנוכחית כשלא מעבירים כלום. הסימן * הוא glob. ה-shell מחליף אותו בשמות שבתיקייה, ממוינים. שמות שמתחילים בנקודה מדולגים כברירת מחדל, כפי שסעיף filename expansion במדריך מתאר.[ -f "$f" ] && sha256sum "$f" מחשב hash רק לקבצים רגילים. sha256sum על תיקייה היה מדפיס שגיאה, ולכן הבדיקה מסננת תיקיות.נותנים לסקריפט הרשאת הרצה ומריצים אותו על התיקייה הנוכחית:
chmod +x hashall.sh
./hashall.sh .chmod +x מוסיף הרשאת הרצה. הקידומת ./ אומרת ל-shell להריץ את הקובץ בתיקייה הזאת, כי כברירת מחדל התיקייה הנוכחית לא נמצאת במשתנה PATH.
פלט בתיקייה שהחזיקה רק את hashall.sh ואת sample.log:
288797e3d2baa72b086c5fca9821282d06726cb44272647d41128ababf255c94 *./hashall.sh
2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893 *./sample.logGit Bash מדפיס כוכבית לפני כל שם, וזה מסמן מצב בינארי ב-GNU coreutils. מערכות אחרות עשויות להציג רווח במקום. ה-hash עצמו נשאר זהה. אם הערכים אצלך שונים, בודקים שהקבצים שלך תואמים לטקסט בית אחר בית, כולל סיומות שורה.
מאשרים ערך אחד ידנית:
sha256sum sample.logזה מדפיס עבור sample.log את אותו hash שהסקריפט הדפיס.
מוסיפים שורה אחת ללוג, ואז מחשבים שוב את ה-hash.
echo "x" >> sample.log
sha256sum sample.logבהרצת הבדיקה ה-hash החדש היה 3084d4c5a7659a2f3636b6079636b081b7ab1b6fca6a5e691c3afdd160959687, כשקודם היה 2c086498f19a2a8ca7fe112417ad8113449bf640bceabd4795808bd00a996893. הקובץ גדל בשני בתים וטביעת האצבע השתנתה איתו. לפני שממשיכים, משחזרים את הקובץ המקורי מהעותק שלך.
משתנים ונתיבים בלי מירכאות. cat $file נשבר על שמות עם רווחים. כברירת מחדל שמים מירכאות, ומסירים אותן רק כשברור שהערך לא יכול להכיל רווחים או wildcards.
דריסת קובץ קלט עם >. sort d.txt > d.txt מרוקן את הקובץ לפני שהפקודה sort קוראת אותו. כותבים לקובץ חדש, בודקים את הפלט ואז מעבירים אותו למקום.
אמון במספר שדה. $(NF-3) עבד על הדוגמה. בודקים את מיקום השדה על כמה שורות אמיתיות לפני שבונים עליו דוח.
מצב קפדני שנעצר על "אין התאמה" תקין. כפי שהודגם למעלה, grep שמחזיר 1 מסיים צינור תחת pipefail. מחליטים אם היעדר התאמות הוא שגיאה עבור הסקריפט, ומטפלים בזה בכוונה.
לולאה שמדווחת על סטטוס שגוי. הסקריפט hashall.sh שלמעלה מחשב hash לכל קובץ רגיל. אבל כשהשם האחרון בתיקייה הוא תת־תיקייה, הסקריפט מדפיס את כל ה-hashes ואז יוצא עם סטטוס 1. הסטטוס הגיע מהבדיקה האחרונה [ -f ], שנכשלה. הצורה המפורשת עם if עושה את אותה עבודה ויוצאת עם 0:
#!/usr/bin/env bash
set -euo pipefail
for f in "${1:-.}"/*; do
if [ -f "$f" ]; then sha256sum "$f"; fi
doneבתיקיית בדיקה שבה תת־תיקייה הייתה הרשומה האחרונה, הגרסה הזאת הדפיסה hash לכל קובץ ויצאה עם סטטוס 0. הגרסה עם && יוצאת גם עם סטטוס 1 על תיקייה ריקה, ולכן הצורה עם if היא ברירת המחדל העדיפה.
הרצת סקריפטים שלא כתבת. סקריפט יכול למחוק או לשלוח קבצים באותה קלות שבה הוא מחשב להם hash. קוראים קודם את כל הקובץ, בודקים כל פקודה שהוא קורא לה ומריצים אותו על עותק של התיקייה.
קוראים את הסעיפים של GNU Bash Reference Manual על pipelines, redirections, shell parameter expansion ועל ה-builtin set. הם מכסים את רוב מה שהופיע כאן. החומר על סטטוס יציאה שימושי כהפניה כשסקריפט מתנהג בצורה לא צפויה.
אחר כך קוראים את המדריך של GNU grep. כדאי לנסות grep -i, grep -v וגם grep -E על הלוג לדוגמה ולרשום איך כל אפשרות משנה את סטטוס היציאה.
GNU Awk User's Guide מכסה שדות, תבניות ומערכים. המונה בשורה אחת שלמעלה הוא דוגמה קטנה למה ש-awk יכול לעשות. כשזה מרגיש טבעי, כותבים תבנית שנייה שסופרת התחברויות מוצלחות לכל משתמש.
סקריפטים צריכים להישאר קצרים. סקריפט של עשר שורות שאפשר להסביר שורה אחר שורה שימושי יותר מסקריפט של מאה שורות שהועתק ואי אפשר לשנות. כשסקריפט גדל מעבר למסך, מפצלים אותו לפונקציות, או שוקלים שפה עם מבני נתונים טובים יותר לעבודה.
Linux וכלי פיתוח
בעלים וקבוצות ב-Linux, ביטי rwx, chmod, umask ו-setuid, עם מעבדה שבודקת גישה כמשתמש שני ומוצאת הרשאות קבצים מסוכנות.
Linux וכלי פיתוח
איך מפתחות מארח ומשתמש של SSH עובדים, למה Ed25519 עם passphrase הוא ברירת המחדל, ואיך מגדירים ומקשיחים התחברות במפתח על VM של מעבדה בבעלותך.
Linux וכלי פיתוח
GitHub למתחילים: מה Git ו-GitHub עושים, המונחים הבסיסיים, תרגול Git מקומי ואיך בודקים פרויקט לפני שמריצים אותו.