Lời nói đầu
Đầu tiên, tôi muốn nói rằng đó không phải là cách đúng đắn để giải quyết vấn đề. Đó là một chút giống như nói rằng " bạn không nên giết người vì nếu không bạn sẽ đi tù ".
Tương tự, bạn không trích dẫn biến của mình vì nếu không, bạn sẽ đưa ra các lỗ hổng bảo mật. Bạn trích dẫn các biến của bạn bởi vì nó không sai (nhưng nếu nỗi sợ của nhà tù có thể giúp đỡ, tại sao không).
Một tóm tắt nhỏ cho những người vừa nhảy lên tàu.
Trong hầu hết các shell, việc mở rộng biến không được trích dẫn (mặc dù điều đó (và phần còn lại của câu trả lời này) cũng áp dụng cho thay thế lệnh ( `...`
hoặc $(...)
) và mở rộng số học ( $((...))
hoặc $[...]
)) có ý nghĩa rất đặc biệt. Cách tốt nhất để mô tả nó là nó giống như gọi một số loại toán tử ẩn + toán tử toàn cầu¹ .
cmd $var
trong một ngôn ngữ khác sẽ được viết một cái gì đó như:
cmd(glob(split($var)))
$var
đầu tiên được chia thành một danh sách các từ theo các quy tắc phức tạp liên quan đến $IFS
tham số đặc biệt (phần tách ) và sau đó mỗi từ tạo ra sự phân tách đó được coi là một mẫu được mở rộng thành một danh sách các tệp khớp với nó (phần toàn cầu ) .
Ví dụ, nếu $var
chứa *.txt,/var/*.xml
và $IFS
chứa ,
, cmd
sẽ được gọi với một số đối số, đối số đầu tiên cmd
và đối số tiếp theo là các txt
tệp trong thư mục hiện tại và các xml
tệp trong /var
.
Nếu bạn muốn gọi cmd
chỉ bằng hai đối số theo nghĩa đen cmd
và *.txt,/var/*.xml
, bạn sẽ viết:
cmd "$var"
đó sẽ là ngôn ngữ quen thuộc khác của bạn:
cmd($var)
Chúng ta có ý nghĩa gì bởi lỗ hổng trong vỏ ?
Rốt cuộc, người ta đã biết từ thời bình minh rằng các kịch bản shell không nên được sử dụng trong các bối cảnh nhạy cảm về bảo mật. Chắc chắn, OK, để lại một biến không được trích dẫn là một lỗi nhưng điều đó không thể làm hại nhiều như vậy, phải không?
Chà, mặc dù thực tế là bất cứ ai cũng sẽ nói với bạn rằng các kịch bản shell không bao giờ được sử dụng cho CGI web, hoặc may mắn là hầu hết các hệ thống không cho phép các tập lệnh shell setuid / setgid hiện nay, một điều mà shellshock (lỗi bash có thể khai thác từ xa đã tạo ra tiêu đề vào tháng 9 năm 2014) tiết lộ rằng các shell vẫn được sử dụng rộng rãi ở những nơi mà chúng có lẽ không nên: trong CGI, trong các kịch bản hook của máy khách DHCP, trong các lệnh sudoers, được gọi bằng (nếu không là ) các lệnh setuid ...
Đôi khi vô tình. Ví dụ, system('cmd $PATH_INFO')
trong tập lệnh php
/ perl
/ python
CGI không gọi shell để diễn giải dòng lệnh đó (không đề cập đến thực tế rằng cmd
chính nó có thể là tập lệnh shell và tác giả của nó có thể chưa bao giờ mong đợi nó được gọi từ CGI).
Bạn đã có một lỗ hổng khi có một con đường leo thang đặc quyền, đó là khi ai đó (hãy gọi anh ta là kẻ tấn công ) có thể làm điều gì đó mà anh ta không có ý định.
Luôn luôn có nghĩa là kẻ tấn công cung cấp dữ liệu, dữ liệu đó được xử lý bởi người dùng / quy trình đặc quyền vô tình làm điều gì đó không nên làm, trong hầu hết các trường hợp vì lỗi.
Về cơ bản, bạn đã gặp sự cố khi mã lỗi của bạn xử lý dữ liệu dưới sự kiểm soát của kẻ tấn công .
Bây giờ, không phải lúc nào dữ liệu đó cũng có thể đến từ đâu và thường rất khó để biết liệu mã của bạn có được xử lý dữ liệu không đáng tin hay không.
Đối với các biến có liên quan, trong trường hợp tập lệnh CGI, khá rõ ràng, dữ liệu là các tham số CGI GET / POST và những thứ như cookie, đường dẫn, máy chủ ... tham số.
Đối với tập lệnh setuid (chạy như một người dùng khi được người khác gọi), đó là đối số hoặc biến môi trường.
Một vector rất phổ biến là tên tệp. Nếu bạn nhận được một danh sách tệp từ một thư mục, có thể các tệp đó đã được kẻ tấn công trồng ở đó .
Về vấn đề đó, ngay cả tại dấu nhắc của trình bao tương tác, bạn có thể dễ bị tổn thương (khi xử lý tệp trong /tmp
hoặc ~/tmp
ví dụ).
Thậm chí một ~/.bashrc
có thể dễ bị tổn thương (ví dụ, bash
sẽ diễn giải nó khi được gọi ssh
để chạy ForcedCommand
like trong git
triển khai máy chủ với một số biến dưới sự kiểm soát của máy khách).
Bây giờ, một tập lệnh có thể không được gọi trực tiếp để xử lý dữ liệu không đáng tin cậy, nhưng nó có thể được gọi bởi một lệnh khác. Hoặc mã không chính xác của bạn có thể được sao chép-dán vào các tập lệnh thực hiện (bởi bạn 3 năm xuống dòng hoặc một trong những đồng nghiệp của bạn). Một nơi đặc biệt quan trọng là trong các câu trả lời trong các trang web Hỏi & Đáp vì bạn sẽ không bao giờ biết bản sao mã của mình có thể ở đâu.
Xuống kinh doanh; nó tệ như thế nào
Để lại một biến (hoặc thay thế lệnh) không được trích dẫn là nguồn lỗ hổng bảo mật số một liên quan đến mã shell. Một phần vì những lỗi đó thường chuyển thành các lỗ hổng nhưng cũng vì nó rất phổ biến để xem các biến không được trích dẫn.
Trên thực tế, khi tìm kiếm các lỗ hổng trong mã shell, điều đầu tiên cần làm là tìm kiếm các biến không được trích dẫn. Thật dễ dàng để nhận ra, thường là một ứng cử viên tốt, thường dễ theo dõi dữ liệu do kẻ tấn công kiểm soát.
Có vô số cách mà một biến không được trích dẫn có thể biến thành một lỗ hổng. Tôi sẽ chỉ đưa ra một vài xu hướng phổ biến ở đây.
Công bố thông tin
Hầu hết mọi người sẽ gặp phải các lỗi liên quan đến các biến không được trích dẫn do phần bị tách (ví dụ: hiện tại các tệp có khoảng trắng trong tên của họ và không gian nằm trong giá trị mặc định của IFS). Nhiều người sẽ bỏ qua phần toàn
cầu . Phần toàn cầu ít nhất là nguy hiểm như phần
tách .
Globbing được thực hiện khi đầu vào bên ngoài không được xác nhận có nghĩa là kẻ tấn công có thể khiến bạn đọc nội dung của bất kỳ thư mục nào.
Trong:
echo You entered: $unsanitised_external_input
nếu $unsanitised_external_input
chứa /*
, điều đó có nghĩa là kẻ tấn công có thể thấy nội dung của /
. Không vấn đề gì. Nó trở nên thú vị hơn mặc dù /home/*
nó cung cấp cho bạn một danh sách tên người dùng trên máy /tmp/*
, /home/*/.forward
để gợi ý về các thực hành nguy hiểm khác, /etc/rc*/*
cho các dịch vụ được kích hoạt ... Không cần phải đặt tên riêng cho họ. Một giá trị /*
/*/* /*/*/*...
sẽ chỉ liệt kê toàn bộ hệ thống tập tin.
Từ chối các lỗ hổng dịch vụ.
Lấy trường hợp trước một chút quá xa và chúng ta đã có DoS.
Trên thực tế, bất kỳ biến không được trích dẫn nào trong ngữ cảnh danh sách với đầu vào không được xác nhận ít nhất là một lỗ hổng DoS.
Ngay cả các chuyên gia viết kịch bản shell thường quên trích dẫn những điều như:
#! /bin/sh -
: ${QUERYSTRING=$1}
:
là lệnh no-op. Cái gì có thể đi sai?
Điều đó có nghĩa là gán $1
cho $QUERYSTRING
nếu $QUERYSTRING
không được đặt. Đó cũng là một cách nhanh chóng để tạo tập lệnh CGI có thể gọi được từ dòng lệnh.
Điều đó $QUERYSTRING
vẫn được mở rộng mặc dù và vì nó không được trích dẫn, toán tử split + global được gọi.
Bây giờ, có một số quả cầu đặc biệt đắt tiền để mở rộng. Các /*/*/*/*
một là đủ xấu vì nó có nghĩa là thư mục niêm yết lên đến 4 cấp độ xuống. Ngoài hoạt động của đĩa và CPU, điều đó có nghĩa là lưu trữ hàng chục nghìn đường dẫn tệp (40k ở đây trên máy chủ VM tối thiểu, 10k trong số các thư mục).
Bây giờ /*/*/*/*/../../../../*/*/*/*
có nghĩa là 40k x 10k và
/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*
đủ để đưa cả cỗ máy mạnh nhất đến đầu gối của nó.
Hãy tự mình thử (mặc dù hãy chuẩn bị cho máy của bạn gặp sự cố hoặc treo):
a='/*/*/*/*/../../../../*/*/*/*/../../../../*/*/*/*' sh -c ': ${a=foo}'
Tất nhiên, nếu mã là:
echo $QUERYSTRING > /some/file
Sau đó, bạn có thể điền vào đĩa.
Chỉ cần thực hiện tìm kiếm google trên shell cgi hoặc bash cgi hoặc ksh cgi , và bạn sẽ tìm thấy một vài trang chỉ cho bạn cách viết CGI bằng shell. Lưu ý rằng một nửa trong số đó là các tham số quá trình dễ bị tổn thương.
Ngay cả cái riêng của David Korn
cũng dễ bị tổn thương (nhìn vào cách xử lý cookie).
lên đến các lỗ hổng thực thi mã tùy ý
Thực thi mã tùy ý là loại lỗ hổng tồi tệ nhất, vì nếu kẻ tấn công có thể chạy bất kỳ lệnh nào, không có giới hạn về những gì anh ta có thể làm.
Đó thường là phần tách ra dẫn đến những điều đó. Việc phân tách đó dẫn đến một số đối số được truyền cho các lệnh khi chỉ có một đối số được mong đợi. Trong khi những cái đầu tiên sẽ được sử dụng trong bối cảnh dự kiến, những cái khác sẽ ở trong một bối cảnh khác nên có khả năng diễn giải khác nhau. Tốt hơn với một ví dụ:
awk -v foo=$external_input '$2 == foo'
Ở đây, ý định là gán nội dung của
$external_input
biến shell cho foo
awk
biến.
Hiện nay:
$ external_input='x BEGIN{system("uname")}'
$ awk -v foo=$external_input '$2 == foo'
Linux
Từ thứ hai dẫn đến việc chia tách $external_input
không được gán cho foo
nhưng được coi là awk
mã (ở đây thực thi một lệnh tùy ý uname
:).
Đó là đặc biệt là một vấn đề đối với các lệnh có thể thực hiện các lệnh khác ( awk
, env
, sed
(GNU một), perl
, find
...) đặc biệt là với các biến thể GNU (mà chấp nhận tùy chọn sau khi tranh cãi). Đôi khi, bạn sẽ không nghi ngờ lệnh để có thể thực hiện khác như ksh
, bash
hoặc zsh
's [
hoặc printf
...
for file in *; do
[ -f $file ] || continue
something-that-would-be-dangerous-if-$file-were-a-directory
done
Nếu chúng ta tạo một thư mục được gọi x -o yes
, thì thử nghiệm sẽ trở nên tích cực, bởi vì đó là một biểu thức điều kiện hoàn toàn khác mà chúng ta đang đánh giá.
Tồi tệ hơn, nếu chúng ta tạo một tệp được gọi x -a a[0$(uname>&2)] -gt 1
, với ít nhất tất cả các triển khai ksh (bao gồm sh
hầu hết các Unice thương mại và một số BSD), sẽ thực thi uname
vì các shell đó thực hiện đánh giá số học trên các toán tử so sánh số của [
lệnh.
$ touch x 'x -a a[0$(uname>&2)] -gt 1'
$ ksh -c 'for f in *; do [ -f $f ]; done'
Linux
Tương tự với bash
một tên tệp như x -a -v a[0$(uname>&2)]
.
Tất nhiên, nếu họ không thể thực hiện tùy ý, kẻ tấn công có thể giải quyết thiệt hại ít hơn (điều này có thể giúp thực hiện tùy ý). Bất kỳ lệnh nào có thể ghi tệp hoặc thay đổi quyền, quyền sở hữu hoặc có bất kỳ tác dụng chính hoặc phụ nào đều có thể được khai thác.
Tất cả các loại điều có thể được thực hiện với tên tập tin.
$ touch -- '-R ..'
$ for file in *; do [ -f "$file" ] && chmod +w $file; done
Và cuối cùng bạn ..
có thể viết được (đệ quy với GNU
chmod
).
Các tập lệnh xử lý tự động các tập tin trong các khu vực có thể ghi công khai như /tmp
được viết rất cẩn thận.
Thế còn [ $# -gt 1 ]
Đó là điều tôi thấy bực tức. Một số người đi xuống tất cả các rắc rối của việc tự hỏi liệu một bản mở rộng cụ thể có thể có vấn đề để quyết định nếu họ có thể bỏ qua các trích dẫn.
Nó giống như nói. Này, có vẻ như $#
không thể chịu sự điều chỉnh của toán tử split + global, hãy yêu cầu shell chia + global nó . Hoặc Hey, chúng ta hãy viết mã không chính xác chỉ vì lỗi không có khả năng bị tấn công .
Bây giờ làm thế nào là không thể? OK, $#
(hoặc $!
, $?
hoặc bất kỳ sự thay thế số học nào) chỉ có thể chứa các chữ số (hoặc -
đối với một số) vì vậy phần toàn cầu bị loại bỏ. Đối với phần tách để làm một cái gì đó mặc dù, tất cả những gì chúng ta cần là $IFS
để chứa các chữ số (hoặc -
).
Với một số vỏ, $IFS
có thể được thừa hưởng từ môi trường, nhưng nếu môi trường không an toàn, dù sao thì đó cũng là trò chơi.
Bây giờ nếu bạn viết một hàm như:
my_function() {
[ $# -eq 2 ] || return
...
}
Điều đó có nghĩa là hành vi của chức năng của bạn phụ thuộc vào bối cảnh mà nó được gọi. Hay nói cách khác, $IFS
trở thành một trong những đầu vào của nó. Nói đúng ra, khi bạn viết tài liệu API cho chức năng của mình, nó sẽ giống như:
# my_function
# inputs:
# $1: source directory
# $2: destination directory
# $IFS: used to split $#, expected not to contain digits...
Và mã gọi hàm của bạn cần đảm bảo $IFS
không chứa chữ số. Tất cả điều đó bởi vì bạn không cảm thấy muốn gõ 2 ký tự trích dẫn kép đó.
Bây giờ, để [ $# -eq 2 ]
lỗi đó trở thành một lỗ hổng, bạn cần bằng cách nào đó để giá trị của $IFS
nó trở thành sự kiểm soát của kẻ tấn công . Có thể hiểu được, điều đó thường sẽ không xảy ra trừ khi kẻ tấn công tìm cách khai thác một lỗi khác.
Điều đó không phải là chưa từng nghe thấy. Một trường hợp phổ biến là khi mọi người quên vệ sinh dữ liệu trước khi sử dụng nó trong biểu thức số học. Chúng ta đã thấy ở trên rằng nó có thể cho phép thực thi mã tùy ý trong một số shell, nhưng trong tất cả chúng, nó cho phép
kẻ tấn công cung cấp cho bất kỳ biến nào một giá trị nguyên.
Ví dụ:
n=$(($1 + 1))
if [ $# -gt 2 ]; then
echo >&2 "Too many arguments"
exit 1
fi
Và với $1
giá trị có (IFS=-1234567890)
, đánh giá số học đó có tác dụng phụ của cài đặt IFS và [
lệnh tiếp theo không thành công, điều đó có nghĩa là việc kiểm tra quá nhiều đối số bị bỏ qua.
Thế còn khi toán tử split + global không được gọi?
Có một trường hợp khác trong đó các trích dẫn là cần thiết xung quanh các biến và các mở rộng khác: khi nó được sử dụng làm mẫu.
[[ $a = $b ]] # a `ksh` construct also supported by `bash`
case $a in ($b) ...; esac
không kiểm tra xem $a
và $b
đều giống nhau (trừ trường hợp zsh
) nhưng nếu $a
phù hợp với mô hình trong $b
. Và bạn cần phải trích dẫn $b
nếu bạn muốn so sánh như dây đàn (điều tương tự trong "${a#$b}"
hoặc "${a%$b}"
hoặc "${a##*$b*}"
nơi $b
nên được trích dẫn nếu nó không được thực hiện như một mô hình).
Điều đó có nghĩa là [[ $a = $b ]]
có thể trở thành sự thật trong trường hợp $a
là khác nhau từ $b
(ví dụ khi $a
là anything
và $b
là *
) hoặc có thể trả về false khi họ là giống hệt nhau (ví dụ khi cả hai $a
và $b
là [a]
).
Điều đó có thể làm cho một lỗ hổng bảo mật? Vâng, giống như bất kỳ lỗi nào. Tại đây, kẻ tấn công có thể thay đổi luồng mã logic của tập lệnh của bạn và / hoặc phá vỡ các giả định mà tập lệnh của bạn đang thực hiện. Chẳng hạn, với một mã như:
if [[ $1 = $2 ]]; then
echo >&2 '$1 and $2 cannot be the same or damage will incur'
exit 1
fi
Kẻ tấn công có thể bỏ qua kiểm tra bằng cách vượt qua '[a]' '[a]'
.
Bây giờ, nếu cả mô hình đó không khớp với toán tử split + global , thì nguy cơ của việc biến một biến không được trích dẫn là gì?
Tôi phải thừa nhận rằng tôi viết:
a=$b
case $a in...
Ở đó, trích dẫn không gây hại nhưng không thực sự cần thiết.
Tuy nhiên, một tác dụng phụ của việc bỏ dấu ngoặc kép trong các trường hợp đó (ví dụ như trong câu trả lời Q & A) là nó có thể gửi một thông điệp sai cho người mới bắt đầu: rằng có thể không được trích dẫn các biến .
Ví dụ, họ có thể bắt đầu nghĩ rằng nếu a=$b
ổn, thì export a=$b
cũng sẽ như vậy (điều này không có trong nhiều shell như trong các đối số của export
lệnh trong ngữ cảnh danh sách) hoặc env a=$b
.
Thế còn zsh
?
zsh
đã sửa chữa hầu hết những vụng về thiết kế. Trong zsh
(ít nhất là khi không sh / ksh chế độ thi đua), nếu bạn muốn tách , hoặc globbing , hoặc phù hợp với mô hình , bạn phải yêu cầu nó một cách rõ ràng: $=var
để phân chia, và $~var
để glob hoặc về nội dung của các biến được đối xử như một mô hình.
Tuy nhiên, việc chia tách (nhưng không phải toàn cầu hóa) vẫn được thực hiện hoàn toàn khi thay thế lệnh không được trích dẫn (như trong echo $(cmd)
).
Ngoài ra, một tác dụng phụ đôi khi không mong muốn của việc không trích dẫn biến là loại bỏ trống . Các zsh
hành vi tương tự như những gì bạn có thể đạt được trong vỏ khác bằng cách tắt globbing hoàn toàn (với set -f
) và tách (với IFS=''
). Tuy nhiên, trong:
cmd $var
Sẽ không có split + global , nhưng nếu $var
trống, thay vì nhận một đối số trống, cmd
sẽ không nhận được đối số nào cả.
Điều đó có thể gây ra lỗi (như hiển nhiên [ -n $var ]
). Điều đó có thể có thể phá vỡ các kỳ vọng và giả định của một kịch bản và gây ra các lỗ hổng, nhưng tôi không thể đưa ra một ví dụ không quá xa vời ngay bây giờ).
Còn khi bạn làm cần chia + glob điều hành?
Có, đó thường là khi bạn không muốn bỏ qua biến của mình. Nhưng sau đó, bạn cần đảm bảo rằng bạn điều chỉnh chính xác các toán tử phân tách và toàn cục trước khi sử dụng nó. Nếu bạn chỉ muốn phần tách chứ không phải phần toàn cầu (đó là trường hợp thường xuyên nhất), thì bạn cần phải vô hiệu hóa Globing ( set -o noglob
/ set -f
) và sửa lỗi $IFS
. Nếu không, bạn cũng sẽ gây ra lỗ hổng (như ví dụ CGI của David Korn đã đề cập ở trên).
Phần kết luận
Nói tóm lại, việc để một biến (hoặc thay thế lệnh hoặc mở rộng số học) không được trích dẫn trong shell có thể rất nguy hiểm, đặc biệt là khi thực hiện trong bối cảnh sai và rất khó để biết đó là những bối cảnh sai.
Đó là một trong những lý do tại sao nó được coi là thực hành xấu .
Cảm ơn đã đọc cho đến nay. Nếu nó đi qua đầu bạn, đừng lo lắng. Người ta không thể mong mọi người hiểu tất cả ý nghĩa của việc viết mã theo cách họ viết. Đó là lý do tại sao chúng tôi có các
khuyến nghị thực hành tốt , vì vậy chúng có thể được theo dõi mà không nhất thiết phải hiểu tại sao.
(và trong trường hợp chưa rõ ràng, vui lòng tránh viết mã nhạy cảm bảo mật bằng shell).
Và xin vui lòng trích dẫn các biến của bạn trên câu trả lời của bạn trên trang web này!
¹Trong ksh93
và pdksh
các dẫn xuất, việc mở rộng dấu ngoặc cũng được thực hiện trừ khi tính năng toàn cầu hóa bị tắt (trong trường hợp ksh93
phiên bản lên đến ksh93u +, ngay cả khi braceexpand
tùy chọn bị tắt).