آموزش اسکریپت نویسی

آموزش اسکریپت نویسی پوسته گنو-لینوکس

آموزش اسکریپت نویسی

آموزش اسکریپت نویسی پوسته گنو-لینوکس

پرسش و پاسخ شماره ۴۸

پرسش و پاسخ شماره ۴۸

فرمان eval و مسائل امنیت

فرمان eval برای سوءاستفاده به شدت قدرتمند و بینهایت آسان است.

باعث می‌شود کُد شما به جای یکبار دوبار تجزیه بشود، این به معنی آنست که برای مثال، اگر کُد شما دارای متغیر مرجع باشد، تفکیک کننده پوسته، محتوای آن متغیر را ارزیابی خواهد نمود. اگر متغیر محتوی فرمان پوسته باشد، پوسته می‌تواند آن فرمان را اجرا کند، آیا شما می‌خواهید اینطور باشد یا خیر. این مطلب می‌تواند به نتایج غیر منتظره منجر گردد، مخصوصاً موقعی که متغیرها بتوانند از منابع غیرقابل اعتماد خوانده شوند(مانند کاربران یا فایلهای تولید شده کاربران).

مثالهای استفاده نامناسب eval

"eval" غلط املایی رایج evil(مترجم: زیانبار، ناشناخته) است. بخشی از این پرسش و پاسخ که به فاصله‌ها در نام فایلها می‌پردازد، قبلاً شامل اسکریپت زیر تحت عنوان «ابزار مفید(که احتمالاً به اندازه تکنیک ‎\0‎ امن نمی‌باشد)» آمده بود.

    Syntax : nasty_find_all <path> <command> [maxdepth]

    # !این کد زیانبار است و  هرگز نباید به کار برود
    export IFS=" "
    [ -z "$3" ] && set -- "$1" "$2" 1
    FILES=`find "$1" -maxdepth "$3" -type f -printf "\"%p\" "`
    # هشدار، شرارت
    eval FILES=($FILES)
    for ((I=0; I < ${#FILES[@]}; I++))
    do
        eval "$2 \"${FILES[I]}\""
    done
    unset IFS

این اسکریپت جستجوی بازگشتی فایلها و اجرای فرمان معین شده کاربر روی آنها، حتی اگر نام آنها شامل سطر جدید یا فاصله یا هردو باشد را فرض کرده بود. مؤلف گمان کرده بود که ‎find -print0 | xargs -0‎ برای مقاصدی از قبیل فرمانهای چندگانه مناسب نمی‌باشد. این اسکریپت با یک توضیح تعلیمی درهریک از تمام سطرهای در بر گیرنده همراه بود، که ما از آنها صرفنظر می‌کنیم.

در دفاع از اسکریپت، به این شکل کار می‌کند:

$ ls -lR
.:
total 8
drwxr-xr-x  2 vidar users 4096 Nov 12 21:51 dir with spaces
-rwxr-xr-x  1 vidar users  248 Nov 12 21:50 nasty_find_all

./dir with spaces:
total 0
-rw-r--r--  1 vidar users 0 Nov 12 21:51 file?with newlines
$ ./nasty_find_all . echo 3
./nasty_find_all
./dir with spaces/file
with newlines
$

اما این را ملاحظه نمایید:

$ touch "\"); ls -l $'\x2F'; #"

شما درست فایلی به نام ‎  "); ls -l $'\x2F'; #‎ ایجاد نموده‌اید.

حالا FILES شامل ‎ ""); ls -l $'\x2F'; #‎ خواهد بود. موقعی که ما ‎eval FILES=($FILES)‎ را اجرا می‌کنیم، تبدیل می‌شود به

FILES=(""); ls -l $'\x2F'; #"

که خودش می‌شود دوجمله ‎ FILES=(""); ‎ و ‎ ls -l / ‎ تبریک، شما دقیقاً اجرای دستورات دلخواه را مجاز نموده‌اید.

$ touch "\"); ls -l $'\x2F'; #"
$ ./nasty_find_all . echo 3
total 1052
-rw-r--r--   1 root root 1018530 Apr  6  2005 System.map
drwxr-xr-x   2 root root    4096 Oct 26 22:05 bin
drwxr-xr-x   3 root root    4096 Oct 26 22:05 boot
drwxr-xr-x  17 root root   29500 Nov 12 20:52 dev
drwxr-xr-x  68 root root    4096 Nov 12 20:54 etc
drwxr-xr-x   9 root root    4096 Oct  5 11:37 home
drwxr-xr-x  10 root root    4096 Oct 26 22:05 lib
drwxr-xr-x   2 root root    4096 Nov  4 00:14 lost+found
drwxr-xr-x   6 root root    4096 Nov  4 18:22 mnt
drwxr-xr-x  11 root root    4096 Oct 26 22:05 opt
dr-xr-xr-x  82 root root       0 Nov  4 00:41 proc
drwx------  26 root root    4096 Oct 26 22:05 root
drwxr-xr-x   2 root root    4096 Nov  4 00:34 sbin
drwxr-xr-x   9 root root       0 Nov  4 00:41 sys
drwxrwxrwt   8 root root    4096 Nov 12 21:55 tmp
drwxr-xr-x  15 root root    4096 Oct 26 22:05 usr
drwxr-xr-x  13 root root    4096 Oct 26 22:05 var
./nasty_find_all
./dir with spaces/file
with newlines
./
$

تعویض‎  ls -l ‎ با ‎ rm -rf ‎ یا وخیم‌تر، قدرت خلاقه زیادی نمی‌خواهد.

کسی ممکن است با خود بگوید اینها رویدادهای مشکوک است، اما کسی این نیرنگ را به کار نمی‌برد. تمام آن فقط یک کاربر بداندیش می‌خواهد، یا شاید محتمل‌تر، کاربر مبتدی که موقع رفتن به حمام ترمینال خود را قفل نشده ترک می‌کند، یا اسکریپت PHP نوشته شده برای ارسال فایل که سلامت نام فایل را کنترل نمی‌کند، یا فردی مرتکب اشتباهی مانند شخصی بشود که خودش اجازه اجرای کُد اختیاری را جایز نموده است(اکنون به جای محدود بودن به کاربران وب ، یک ضارب می‌تواند از ‎ nasty_find_all‎ برای عبور به محبس chroot و به دست آوردن امتیازات اضافی استفاده کند)، یا استفاده از یک سرویس گیرنده IRC یا IM که در پذیرش نام فایلها برای انتقال فایل یا لاگ‌های مکالمه بیش از حد آزادی‌خواه است، و غیره.

مثالهای استفاده مناسب eval

رایج‌ترین استفاده صحیح از فرمان eval خواندن متغیرها از خروجی برنامه‌ایست که مخصوصاً طراحی شده برای استفاده به این طریق است. برای مثال،

# در سیتم‌های قدیمی، بعد از تغییر اندازه پنجره، شخص  این را باید اجرا کند‎
eval `resize`

#          SSH کمتر ابتدایی: گرفتن  عبارت عبور برای یک کلید محرمانه ‎
#       اجرا می‌شود .xsession یا .profile این به طور نوعی از  یک نوع ‎
#  به تمام پردازشهای نشست کاربر ssh متغیرهای تولید شده توسط کارگزار‎
#           .احتمالی از آن ارث می‌برد ssh صادر می‌شوند، به طوری که یک ‎
eval `ssh-agent -s`

eval کاربردهای دیگری دارد، مخصوصاً موقع ایجاد متغیرهای فوق‌العاده(متغیرهای غیرمستقیم مرجع). این هم یک مثال از یک روش تجزیه گزینه‌های خط فرمان که پارامترها را نمی‌پذیرد:

# POSIX
#
# تولید متغیرهای گزینه به طور پویا، احظار ش را امتحان کنید
#
#    sh -x example.sh --verbose --test --debug

for i in "$@"
do
    case "$i" in
       --test|--verbose|--debug)
            shift                   # حذف گزینه خط فرمان
            name=${i#--}            # حذف پیشوند گزینه
            eval "$name='$name'"    # از نو ساختن  متغیر
            ;;
    esac
done

echo "verbose: $verbose"
echo "test: $test"
echo "debug: $debug"

بنابراین، چرا این نگارش قابل پذیرش است؟ قابل پذیرش است به دلیل آنکه ما فرمان eval را به طوری محدود کرده‌ایم که فقط موقعی اجرا خواهد شد که ورودی یکی از مقادیر معلوم مجموعه محدود باشد. بنابراین، هرگز نمی‌تواند توسط کاربر برای اجرای دستور اختیاری مورد سوءاستفاده قرار گیرد -- هر ورودی توأم با دغلبازی، با یکی از سه ورودی مجاز از پیش تعریف شده مطابقت نخواهد کرد. این مغایرت قابل قبول نخواهد بود:

# !کُد خطرناک، این را به کار نبرید
for i in "$@"
do
    case "$i" in
       --test*|--verbose*|--debug*)
            shift                   # حذف گزینه خط فرمان ‎
            name=${i#--}            # حذف پیشوند گزینه‎
            eval "$name='$name'"    # درست کردن گزینه از نو‎
            ;;
    esac
done

تمام آنچه تغییر نموده آنست که ما سعی نموده‌ایم مثال خوب قبلی(که کار خیلی زیادی انجام نمی‌دهد) را به این طریق ، با اجازه دادن به دریافت مواردی مانند ‎ --test=foo‎ سودمند کنیم. اما نگاه کنید که چه چیزی را فعال می‌سازد:

$ ./foo --test='; ls -l /etc/passwd;x='
-rw-r--r-- 1 root root 943 2007-03-28 12:03 /etc/passwd

یکبار دیگر: با مجاز نمودن آنکه فرمان eval با ورودی فیلتر نشده کاربر استفاده بشود، ما اجرای فرمان دلخواه را مجاز کرده‌ایم.

اگر چه، تخصیص یک متغیر اختیاری توسط eval با استفاده از این ترکیب دستوری کاملاً بی خطر می‌باشد:

eval "$varname=\$whatever"

البته، این ترکیب فرض می‌کند که ‎$varname‎ نام یک متغیر معتبر می‌باشد.

جایگزین‌ها برای eval

  • آیا این نمی‌توانست با declare بهتر انجام بشود؟ به عنوان مثال:

     for i in "$@"
     do
        case "$i" in
           --test|--verbose|--debug)
                shift                   # حذف گزینه خط فرمان‎
                name=${i#--}            # حذف پیشوند گزینه‎
                declare $name=Yes       # تنظیم مقدار پیش فرض
                ;;
           --test=*|--verbose=*|--debug=*)
                shift
                name=${i#--}
                value=${name#*=}        #  کمیت جایی بعد از اولین کلمه و ‏=‏ است‎
                name=${name%%=*}        #  محدود نمودن نام به تنها یک کلمه ‎
    				    #  (حتی اگر یک  = دیگر در مقدار باشد)‎
                declare $name="$value"  #  درست کردن گزینه از نو‎
                ;;
        esac
     done

    توجه کنید که ‎ --name‎ برای حالت پیش فرض ، و ‎ --name=value قالب‌های لازم هستند.

    به نظر می‌رسد declare نوعی تفکیک کننده جادویی دارد، بیشتر مانند‎ [[ ‎. در اینجا آزمایشی هست که من در bash نگارش 3.1.17 انجام داده‌ام:

     griffon:~$ declare foo=x;date;x=Yes
     Sun Nov  4 09:36:08 EST 2007
     
     griffon:~$ name='foo=x;date;x'
     griffon:~$ declare $name=Yes
     griffon:~$ echo $foo
     x;date;x=Yes

    آشکار است که، حداقل در bash, فرمان declare خیلی مطمئن‌تر از eval است.

     attoparsec:~$ echo $BASH_VERSION 
     4.2.24(1)-release
     attoparsec:~$ danger='( $(printf "%s!\n" DANGER >&2) )'
     attoparsec:~$ declare safe=${danger}
     attoparsec:~$ declare -a unsafe
     attoparsec:~$ declare unsafe=${danger}
     DANGER!
     attoparsec:~$ 
    متغیرهای عادی ممکن است با declare بی خطر باشند، اما متغیرهای آرایه‌ای بی‌خطر نیستند.

برای لیستی از روشهای ارجاع یا مقداردهی غیر مستقیم متغیرها بدون استفاده از eval، لطفاً پرسش و پاسخ شماره 6 را ببینید. (این بخش قبل از آن پاسخ نوشته شده بود، اما من آن را به عنوان یک منبع در اینجا قرار داده‌ام.)

کاربرد قوی eval

یک رویکرد دیگر آن است که کُد خطرناک می‌تواند در یک تابع پوشانیده بشود. برای مثال به جای انجام کاری مانند این:

    eval "${ArrayName}"'="${Value}"'

حال آنکه مثال فوق به طور قابل قبولی صحیح است، اما هنوز قابلیت آسیب‌پذیری دارد. توجه کنید چه اتفاقی می‌افتد اگر به صورت زیر انجام بدهیم.

    ArrayName="echo rm -rf /tmp/dummyfolder/*; tvar"
    eval "${ArrayName}"'="${Value}"'

راه پیشگیری از این گونه حفره امنیتی ایجاد تابعی است که اجرایش مقدار معینی از امنیت را به شما می‌دهد و کُد پاکیزه‌تری را جایز می‌نماید.

  # check_valid_var_name VariableName
  function check_valid_var_name {
    case "${1:?Missing Variable Name}" in
      [!a-zA-Z_]* | *[!a-zA-Z_0-9]* ) return 3;;
    esac
  }
  # set_variable VariableName [<Variable Value>]
  function set_variable {
    check_valid_var_name "${1:?Missing Variable Name}" || return $?
    eval "${1}"'="${2:-}"'
  }
  set_variable "laksdpaso" "dasädöas# #-c,c pos 9302 1´ " 
  set_variable "echo rm -rf /tmp/dummyfolder/*; tvar" "dasädöas# #-c,c pos 9302 1´ " 
  # return Error

توجه: set_variable یک مزیت اضافه بر کاربرد declare دارد. مورد پایین را ملاحظه کنید.

   VariableName="Name=hhh"
   declare "${VariableName}=Test Value"         # Valid code, unexpected behavior
   set_variable "${VariableName}" "Test Value"  # return Error

چند مثال دیگر برای ارجاع

  # get_array_element VariableName ArrayName ArrayElement
  function get_array_element {
    check_valid_var_name "${1:?Missing Variable Name}" || return $?
    check_valid_var_name "${2:?Missing Array Name}" || return $?
    eval "${1}"'="${'"${2}"'["${3:?Missing Array Index}"]}"'
  }
  # set_array_element ArrayName ArrayElement [<Variable Value>]
  function set_array_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    eval "${1}"'["${2:?Missing Array Index}"]="${3:-}"'
  }
  # unset_array_element ArrayName ArrayElement
  function unset_array_element {
    unset "${1}[${2}]"
  }
  # unset_array_element VarName ArrayName
  function get_array_element_cnt {
    check_valid_var_name "${1:?Missing Variable Name}" || return $?
    check_valid_var_name "${2:?Missing Array Name}" || return $?
    eval "${1}"'="${#'"${2}"'[@]}"'
  }
  # push_element ArrayName <New Element 1> [<New Element 2> ...]
  function push_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    local ArrayName="${1}"
    local LastElement
    eval 'LastElement="${#'"${ArrayName}"'[@]}"'
    while shift && [ $# -gt 0 ] ; do
      eval "${ArrayName}"'["${LastElement}"]="${1}"'
      let LastElement+=1
    done
  }
  # pop_element ArrayName <Destination Variable Name 1> [<Destination Variable Name 2> ...]
  function pop_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    local ArrayName="${1}"
    local LastElement
    eval 'LastElement="${#'"${ArrayName}"'[@]}"'
    while shift && [[ $# -gt 0 && ${LastElement} -gt 0 ]] ; do
      let LastElement-=1
      check_valid_var_name "${1:?Missing Variable Name}" || return $?
      eval "${1}"'="${'"${ArrayName}"'["${LastElement}"]}"'
      unset "${ArrayName}[${LastElement}]" 
    done
    [[ $# -eq 0 ]] || return 8
  }
  # shift_element ArrayName [<Destination Variable Name>]
  function shift_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    local ArrayName="${1}"
    local CurElement=0 LastElement
    eval 'LastElement="${#'"${ArrayName}"'[@]}"'
    while shift && [[ $# -gt 0 && ${LastElement} -gt ${CurElement} ]] ; do
      check_valid_var_name "${1:?Missing Variable Name}" || return $?
      eval "${1}"'="${'"${ArrayName}"'["${CurElement}"]}"'
      let CurElement+=1
    done
    eval "${ArrayName}"'=("${'"${ArrayName}"'[@]:${CurElement}}")'
    [[ $# -eq 0 ]] || return 8
  }
  # unshift_element ArrayName <New Element 1> [<New Element 2> ...]
  function unshift_element {
    check_valid_var_name "${1:?Missing Array Name}" || return $?
    [ $# -gt 1 ] || return 0
    eval "${1}"'=("${@:2}" "${'"${1}"'[@]}" )'
  }

 # 1000 x { declare "laksdpaso=dasädöas# #-c,c pos 9302 1´ "       }  0m0.069s مدت زمان اجرا‎
 # 1000 x { set_variable laksdpaso "dasädöas# #-c,c pos 9302 1´ "  }  0m0.141s مدت زمان اجرا‎
 # 1000 x { get_array_element TestVar TestArray 1                  }  0m0.199s مدت زمان اجرا‎
 # 1000 x { set_array_element TestArray 1 "dfds  edfs fdf df"      }  0m0.174s مدت زمان اجرا‎
 # 1000 x { set_array_element TestArray 0                          }  0m0.167s مدت زمان اجرا‎
 # 1000 x { get_array_element_cnt TestVar TestArray                }  0m0.171s مدت زمان اجرا‎

 # با یک آرایه دو هزار عنصری اتجام شده‌اند push,pops,shifts,unshifts همه نوابع ‎
 # 1000 x { push_element TestArray "dsf sdf ss s"                  }  0m0.274s مدت زمان اجرا‎
 # 1000 x { pop_element TestArray TestVar                          }  0m0.380s مدت زمان اجرا‎
 # 1000 x { unshift_element TestArray "dsf sdf ss s"               }  0m9.027s مدت زمان اجرا‎
 # 1000 x { shift_element TestArray TestVar                        }  0m5.583s مدت زمان اجرا‎

توجه، shift_element و unshift_element کارایی ضعیفی دارند و مخصوصاً روی آرایه‌های بزرگ باید از آنها اجتناب گردد. مابقی کارایی قابل قبولی دارند و من به طور منظم آنها را به کار می‌برم.


CategoryShell

پرسش و پاسخ 48 (آخرین ویرایش ‎2013-03-07 03:51:23‎ توسط ChrisJohnson)