PHP Scripte: Chrooten und Privileges droppen

Derzeit bin ich dabei, meinen eigenen Mailserver aufzusetzen. Eigentlich hatte ich das bereits vor zwei Jahren geplant, doch irgendwie war Google Apps so viel bequemer, als sich selbst um den ganzen Kram zu kümmern - zu Recht: Microsofts Hotmail-Dienst macht wohl öfter mal Probleme, wenn es darum geht, E-Mails zustellen zu dürfen. Was für ein Krampf.

Jedenfalls habe ich mir vorgenommen, entgegen meiner bisherigen Idee auf Google Apps vollständig zu verzichten - sprich deren Mailserver auch nicht als Backup-MX zu verwenden. Das bedingt, dass ich mindestens zwei SMTP-Server für den Empfang von E-Mails betreiben werde. Dazu habe ich mir V-Server bei verschiedenen Betreibern gemietet, die ich - sollte mal etwas sein - einfach um weitere Server erweitern kann. So kann ich sicherstellen, dass ich bei größeren Problemen schnell wieder dazu komme, E-Mails empfangen zu können.

Damit das funktioniert, muss ich die verschiedenen SMTP-Server irgendwie synchronisieren. Da ich als Mailablage Maildir verwende, habe ich mich beim "irgendwie" für SFTP entschieden. Der eine Server verbindet sich zum anderen, prüft dort, ob neue E-Mails vorliegen und spielt sie dann in die lokalen Mailordner ein. Die lokalen Mailordner wiederum gehören jeweils pro Mailaccount einem eigenen Systemnutzer.

Die Synchronisation selbst habe ich in PHP geschrieben - ja, wirklich, in PHP. Da es mir allerdings zu heikel war, die Synchronisation als Root auszuführen, habe ich mir mal angesehen, ob PHP in der Lage ist, innerhalb eines Scripts Privileges zu droppen und sich eventuell sogar Chrooten zu lassen. Und in der Tat, beides ist möglich!

Da ich diese Fähigkeit für sehr sinnvoll halte, habe ich mir das ganze direkt in einer kleinen Bibliothek zusammen gesammelt. Diese enthält zwei wichtige Funktionen "force_chroot()" und "force_unroot()". Sollte ein entsprechender Aufruf fehlschlagen, wird das gesamte Script direkt mit einer Fehlermeldung beendet. Lieber nichts tun, als mit zu vielen Rechten unterwegs sein, war hier meine Devise.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
<?php

  function allow_concurrency($timeout_or_lockfilepath) {
    $result = false;

    if (is_int($timeout_or_lockfilepath)) {
      set_time_limit(0);
      $result = true;
    } else {
      if (is_string($timeout_or_lockfilepath)) {
        if (is_file($timeout_or_lockfilepath)) {
          $content = file_get_contents($timeout_or_lockfilepath);
          if ((false !== $content) && (!empty($content))) {
            if (posix_getpid() == $content) {
              $result = unlink($timeout_or_lockfilepath);
            }
          }
        }
      }
    }

    return $result;
  }

  function disallow_concurrency($timeout_or_lockfilepath) {
    $result = false;

    if (is_int($timeout_or_lockfilepath) && (0 < $timeout_or_lockfilepath)) {
      set_time_limit($timeout_or_lockfilepath);
      $result = true;
    } else {
      if (is_string($timeout_or_lockfilepath)) {
        // we may run endlessly
        set_time_limit(0);

        // we're able to aquire a lock if the file does not exist
        $writeLockfile = (!is_file($timeout_or_lockfilepath));

        if (!$writeLockfile) {
          $content = file_get_contents($timeout_or_lockfilepath);

          // we're able to aquire a lock if the file is empty...
          if ((false !== $content) && (!empty($content))) {
            // ...and we're the owner of the lock file...
            if (posix_getpid() == $content) {
              $result        = true;
              $writeLockfile = false;
            } else {
              // ...or the owner of the lock file does not exist anymore...
              if (!file_exists("/proc/" . $content)) {
                // ...and we're able to delete the orphaned lock file
                $writeLockfile = unlink($timeout_or_lockfilepath);
              }
            }
          } else {
            // ...and we're able to delete the empty lock file
            $writeLockfile = unlink($timeout_or_lockfilepath);
          }
        }

        if ($writeLockfile) {
          $result = (false !== file_put_contents($timeout_or_lockfilepath, posix_getpid()));
        }
      }
    }

    return $result;
  }

  function chroot_normalize_path($path, $trailing_slash = true) {
    $slash     = '/';
    $previous  = '..';
    $empty     = '';
    $current   = '.';
    $backslash = '\\';

    // preset $result
    $result = false;

    // prepare $path as array
    $path = explode($slash, str_replace($backslash, $slash, $path));
    if (0 < count($path)) {
      // prepare $cwd as empty array
      $cwd = array();
      switch ($path[0]) {
        case $empty:
          array_push($cwd, $empty);
          break;

        case $current:
          $cwd = explode($slash, str_replace($backslash, $slash, getcwd()));
          break;

        case $previous:
          $cwd = explode($slash, str_replace($backslash, $slash, getcwd()));
          array_pop($cwd);
          break;

        default:
          $cwd = explode($slash, str_replace($backslash, $slash, getcwd()));
          array_push($cwd, $path[0]);
      }

      // normalize $path
      for ($index = 1; $index < count($path); $index++) {
        switch ($path[$index]) {
          case $empty:
            break;

          case $current:
            break;

          case $previous:
            array_pop($cwd);
            break;

          default:
            array_push($cwd, $path[$index]);
        }
      }

      if ($trailing_slash) {
        if ((1 < count($path)) && ($empty === $path[count($path)-1])) {
          array_push($cwd, $empty);
        }
      }

      $result = implode($slash, $cwd);
    }

    return $result;
  }

  function unroot_get_gid($group) {
    $result = -1;

    if (is_int($group)) {
      $result = $group;
    } else {
      $temp = posix_getgrnam($group);
      if (false !== $temp) {
        $result = $temp["gid"];
      }
    }

    return $result;
  }

  function unroot_get_uid($user) {
    $result = -1;

    if (is_int($user)) {
      $result = $user;
    } else {
      $temp = posix_getpwnam($user);
      if (false !== $temp) {
        $result = $temp["uid"];
      }
    }

    return $result;
  }

  function force_chroot($path) {
    $result = false;

    $path = chroot_normalize_path($path);
    if ((false !== $path) && (is_dir($path))) {
      $result = chroot($path);
    }

    if (false === $result) {
      exit("force chroot failed: path=$path");
    }

    return $result;
  }

  function force_unroot($user, $group) {
    $result = (0 < posix_getuid());

    if (!$result) {
      $groupid = unroot_get_gid($group);
      $userid  = unroot_get_uid($user);

      if ((0 < $groupid) && (0 < $userid)) {
        $result = posix_setgid($groupid);
        if ($result) {
          $result = posix_setuid($userid);
        }
      }
    }

    if (!$result) {
      exit("force unroot failed: user=$user group=$group");
    }

    return $result;
  }

  function force_chroot_unroot($path, $user, $group) {
    $gid = unroot_get_gid($group);
    $uid = unroot_get_uid($user);

    $result = force_chroot($path);
    if ($result) {
      $result = force_unroot($uid, $gid);
    }

    return $result;
  }

?>

Ich habe inzwischen auch schon meine anderen Scripte zumindest mit "force_unroot()" ausgestattet. Ich denke, damit ist bereits einiges an Sicherheit gewonnen - zumindest, wenn man unter Linux arbeitet. 😉

Vielleicht kann ja sonst noch jemand was damit anfangen. 🙂

Unprivilegierte Grüße, root Kenny

Schreibe einen Kommentar

Um Ihnen beim weiteren Kommentieren auf dieser Webseite die erneute Eingabe Ihrer Daten zu ersparen, wird beim Absenden Ihres Kommentars ein Cookie an Ihren Browser gesendet und von diesem gespeichert. Mit dem Absenden eines Kommentars auf dieser Webseite stimmen Sie der Speicherung und Übertragung dieses Cookies explizit zu.

Pflichtfelder sind mit * markiert.