阐述 SELinux 在 Apache 中的作用

这是续集

请使用SELinux。使用吧。由于在2016年的Linux Advent Calendar 2016上发布了这个消息后,事情变得很大,所以我决定写续篇。

这次的主题

我们将在启用SELinux环境和禁用SELinux环境下,使用Apache运行CGI来观察嵌入后门的行为差异。

环境

操作系统: CentOS 7.3.1611
SELinux: 目标模式

使用 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。由于没有返回内容,我们将其阻塞。

$ curl localhost/cgi-bin/nc.sh
<<< ここでブロックする >>>

然后,nc会打开并等待9000/tcp端口。

# 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))

我会从另一个终端向这里发送一条消息。

$ nc localhost 9000
hogehoge
foobar
foobarbaz
<<<Ctrl-D>>>

当我在 nc 中发送文字时,之前被封锁(通过 curl)的终端会显示终端 3 发送的消息。

$ curl localhost/cgi-bin/nc.sh
hogehoge
foobar
foobarbaz

发生了什么事情?

当执行CGI时,nc会打开0.0.0.0:9000并等待。从其他终端发送任意字符串到这个地址,可以将字符串发送到客户端。

在这个例子中,我们只是发送了一个字符串,但是真正的问题是

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

 

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

这是指的。 (Zhè shì zhǐ de.)

让我们将SELinux切换到Enforce模式。

首先要访问 CGI。
这次不会返回任何内容,也不会阻塞。

$ curl localhost/cgi-bin/nc.sh
$ 

nc 是什么?

# 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 的日志。

[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 被拒绝许可。接下来,我们来查看审计日志。

# 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的进程试图在http_port_t的9000/tcp上绑定nc,所以被拒绝了,并在审计日志中记录下来。

为什么?

首先我们来看一下执行的CGI上下文。
CGI的执行需要httpd_sys_script_exec_t,正如之前所说,CGI本身可以正常运行,所以文件上下文也正确附加。
审计日志中也有这个上下文的违规记录,所以没有错误。

# 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是否可以在name_bind权限下绑定http_port_t。

# 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定义策略,以确保正确运行。

我们下次写一下步骤吧…

广告
将在 10 秒后关闭
bannerAds