Edited at

Apache における SELinux の効果

More than 1 year has passed since last update.


続編です

SELinux を使おう.使ってくれ. として 2016 年の Linux Advent Calendar 2016 に投稿したところ話が大きくなったので,続編を書くことにしました.


今回のお題

SELinux を有効にした環境と無効にした環境で,Apache で動かす CGI を使ってバッグドア的なものを埋め込んだ時の挙動の差を見ていきます.


環境

OS: CentOS 7.3.1611

SELinux: targeted mode


使う CGI

手間を割くために ncat を使います.

CGI が実行されると 0.0.0.0:9000 を開けて待ちます.

CentOS の場合は apache ユーザで Apache のワーカプロセスが動作するため Well-known Ports は開けないので,適当なポートを選んでいます.

また HTTP 用のポートとして登録されているものにしています.

(まったく見当違いのポートを使うと弾かれるのは目に見えていて面白くないから)

# semanage port -l | grep 9000

http_port_t tcp 80, 81, 443, 488, 8008, 8009, 8443, 9000

#!/bin/sh

echo "content-type: text/plain"
echo ""

nc 0.0.0.0 -l 9000 &


やってみよう


SELinux を無効化もしくは Permissive モードにしている場合

まずは CGI にアクセスします.コンテンツが返ってこないのでブロックします.


ターミナルその1

$ curl localhost/cgi-bin/nc.sh

<<< ここでブロックする >>>

すると nc が 9000/tcp を開けて待ってくれます.


ターミナルその2

# ss -atnp

State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:111 *:* users:(("systemd",pid=1,fd=40))
LISTEN 0 5 192.168.122.1:53 *:* users:(("dnsmasq",pid=1343,fd=6))
LISTEN 0 128 *:22 *:* users:(("sshd",pid=1166,fd=3))
LISTEN 0 128 127.0.0.1:631 *:* users:(("cupsd",pid=1144,fd=12))
LISTEN 0 100 127.0.0.1:25 *:* users:(("master",pid=1291,fd=13))
LISTEN 0 10 *:9000 *:* users:(("nc",pid=2082,fd=3))
ESTAB 0 0 192.168.0.111:22 192.168.0.2:55270 users:(("sshd",pid=1953,fd=3),("sshd",pid=1949,fd=3))
ESTAB 0 52 192.168.0.111:22 192.168.0.2:54375 users:(("sshd",pid=1452,fd=3),("sshd",pid=1447,fd=3))
LISTEN 0 128 :::111 :::* users:(("systemd",pid=1,fd=39))
LISTEN 0 128 :::80 :::* users:(("httpd",pid=1902,fd=4),("httpd",pid=1901,fd=4),("httpd",pid=1900,fd=4),("httpd",pid=1873,fd=4),("httpd",pid=1870,fd=4),("httpd",pid=1869,fd=4),("httpd",pid=1868,fd=4),("httpd",pid=1867,fd=4),("httpd",pid=1866,fd=4),("httpd",pid=1865,fd=4))
LISTEN 0 128 :::22 :::* users:(("sshd",pid=1166,fd=4))
LISTEN 0 128 ::1:631 :::* users:(("cupsd",pid=1144,fd=11))
LISTEN 0 100 ::1:25 :::* users:(("master",pid=1291,fd=14))
ESTAB 0 0 ::1:80 ::1:34120 users:(("httpd",pid=1869,fd=9))
ESTAB 0 0 ::1:34120 ::1:80 users:(("curl",pid=2079,fd=3))

そこで別のターミナルからここにメッセージを送ります.


ターミナル3

$ nc localhost 9000

hogehoge
foobar
foobarbaz
<<<Ctrl-D>>>

nc でテキストを送ると,先ほどブロックしていた(curl した)ターミナルに,ターミナル 3 で送信したメッセージが表示されています.


ターミナルその1

$ curl localhost/cgi-bin/nc.sh

hogehoge
foobar
foobarbaz


なにが起きているのか

CGI を実行されたときに nc が 0.0.0.0:9000 を開いて待ちます.

そこに他のターミナルから適当な文字列を送るとクライアント側に文字列を送信することができます.

今回の例ではただ文字列を送るだけでしたが,本質的な課題は


  • 任意のコマンドを実行できてしまう

  • 場合によっては任意のポートを開くことができてしまう

ということです.


では SELinux を Enforce モードにしてみましょう

まずは CGI にアクセスします.

今回はコンテンツが返ってきもしない,ブロックもしません.


ターミナルその1

$ curl localhost/cgi-bin/nc.sh

$

nc は?


ターミナルその2

# ss -atnp

State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:111 *:* users:(("systemd",pid=1,fd=40))
LISTEN 0 5 192.168.122.1:53 *:* users:(("dnsmasq",pid=1343,fd=6))
LISTEN 0 128 *:22 *:* users:(("sshd",pid=1166,fd=3))
LISTEN 0 128 127.0.0.1:631 *:* users:(("cupsd",pid=1144,fd=12))
LISTEN 0 100 127.0.0.1:25 *:* users:(("master",pid=1291,fd=13))
ESTAB 0 0 192.168.0.111:22 192.168.0.2:55270 users:(("sshd",pid=1953,fd=3),("sshd",pid=1949,fd=3))
ESTAB 0 0 192.168.0.111:22 192.168.0.2:54375 users:(("sshd",pid=1452,fd=3),("sshd",pid=1447,fd=3))
LISTEN 0 128 :::111 :::* users:(("systemd",pid=1,fd=39))
LISTEN 0 128 :::80 :::* users:(("httpd",pid=1902,fd=4),("httpd",pid=1901,fd=4),("httpd",pid=1900,fd=4),("httpd",pid=1873,fd=4),("httpd",pid=1870,fd=4),("httpd",pid=1869,fd=4),("httpd",pid=1868,fd=4),("httpd",pid=1867,fd=4),("httpd",pid=1866,fd=4),("httpd",pid=1865,fd=4))
LISTEN 0 128 :::22 :::* users:(("sshd",pid=1166,fd=4))
LISTEN 0 128 ::1:631 :::* users:(("cupsd",pid=1144,fd=11))
LISTEN 0 100 ::1:25 :::* users:(("master",pid=1291,fd=14))

# ss -atnp | grep nc


いないんです.

Apache のログを見てみましょう.


/var/log/httpd/error_log

[Sun Jan 08 11:30:30.374900 2017] [cgi:error] [pid 1867] [client ::1:34144] AH01215: Ncat: bind to 0.0.0.0:9000: Permission denied. QUITTING.


nc が Permission denied 食らっています.

続いて audit ログを見てみます.

# ausearch -m avc

...(略)...
time->Sun Jan 8 11:30:30 2017
type=SYSCALL msg=audit(1483842630.373:431): arch=c000003e syscall=49 success=no exit=-13 a0=3 a1=658c60 a2=80 a3=7ffddcbf3e10 items=0 ppid=1 pid=31418 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295 comm="nc" exe="/usr/bin/ncat" subj=system_u:system_r:httpd_sys_script_t:s0 key=(null)
type=AVC msg=audit(1483842630.373:431): avc: denied { name_bind } for pid=31418 comm="nc" src=9000 scontext=system_u:system_r:httpd_sys_script_t:s0 tcontext=system_u:object_r:http_port_t:s0 tclass=tcp_socket

system_u:system_r:httpd_sys_script_t:s0 なやつが nc を実行して http_port_t の 9000/tcp をバインドしようとしたから却下したぜ,と audit ログに残っています.


なんで?

まず実行した CGI のコンテキストをみてみます.

CGI の実行には httpd_sys_script_exec_t が必要で,先のとおり CGI 自体は正しく動いているのでファイルコンテキストも正しく付いています.

audit ログにもこのコンテキストの違反が記録されているので間違いはありません.

# ls -Z /var/www/cgi-bin/

-rwxr-xr-x. root root unconfined_u:object_r:httpd_sys_script_exec_t:s0 nc.sh

つづいて SELinux の設定を見てみます.

ポートをバインドして開くためには,name_bind が許可されていなければなりません.

httpd_sys_script_exec_t が http_port_t を name_bind できるかを調べてみます.

# sesearch --allow -s httpd_sys_script_t | grep http_port_t

#

結果は該当なし.

ではどんなポートを name_bind 出来るのかというと

# sesearch --allow -s httpd_sys_script_t | grep name_bind

allow httpd_script_type ephemeral_port_t : udp_socket name_bind ;
allow nsswitch_domain port_t : udp_socket name_bind ;
allow httpd_script_type port_t : udp_socket name_bind ;
allow nsswitch_domain port_t : tcp_socket name_bind ;
allow nsswitch_domain ephemeral_port_t : udp_socket name_bind ;
allow httpd_script_type port_t : tcp_socket name_bind ;
allow nsswitch_domain unreserved_port_t : udp_socket name_bind ;
allow nsswitch_domain ephemeral_port_t : tcp_socket name_bind ;
allow httpd_script_type unreserved_port_t : udp_socket name_bind ;
allow httpd_script_type ephemeral_port_t : tcp_socket name_bind ;
allow nsswitch_domain unreserved_port_t : tcp_socket name_bind ;
allow httpd_script_type unreserved_port_t : tcp_socket name_bind ;

たとえばクライアントとしてエフェメラルポート(32768-61000)を開いたりという感じです.

独自にサービス用ポートを開くことはできません.

そんな訳で,正しく SELinux が設定されている状況下では,不正にポートを開いたりすることは出来ないのです.

ちなみに Apache はどうやって WKP の 80/tcp を開いているのかと言うと...

# ps xafZ | grep httpd | grep -v grep

system_u:system_r:httpd_t:s0 1865 ? Ss 0:00 /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1866 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1867 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1868 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1869 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1870 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1873 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1900 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1901 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND
system_u:system_r:httpd_t:s0 1902 ? S 0:00 \_ /usr/sbin/httpd -DFOREGROUND

# sesearch --allow -s httpd_t -t http_port_t
Found 11 semantic av rules:
allow httpd_t port_type : tcp_socket { recv_msg send_msg } ;
allow httpd_t port_type : udp_socket { recv_msg send_msg } ;
allow httpd_t http_port_t : udp_socket name_bind ;
allow httpd_t http_port_t : tcp_socket name_bind ;
allow httpd_t port_type : tcp_socket name_connect ;
allow nsswitch_domain port_type : udp_socket recv_msg ;
allow nsswitch_domain port_type : udp_socket send_msg ;
allow nsswitch_domain port_type : tcp_socket { recv_msg send_msg } ;
allow httpd_t http_port_t : tcp_socket name_connect ;
allow httpd_t http_port_t : tcp_socket name_connect ;
allow nsswitch_domain reserved_port_type : tcp_socket name_connect ;

上記のとおり,httpd_t なコンテキストから http_port_t を開けているという訳です.


おわりに

今回はポリシに定義されていない不正な挙動をするものをあえて実行し,結果を観察していきました.

もちろんこのような動作が必要な場合は,独自にポートを定義し SELinux にポリシを定義してあげれば正しく動かすことが可能です.

手順は今度書きましょうか...