sudo für Arme

CGI-Scripte laufen typischerweise unter den Rechten des Webservers. Was also tun, wenn so ein Script erhöhte Rechte benötigt?

Der triviale Ansatz ist, ein zweites Programm aufzurufen, dass das suid Bit gesetzt hat. Unglücklicherweise müßte dieses Programm dann in ein kompilierten Form vorliegen, die schwer zu warten ist. Der geneigte Sysadmin schreibt seine Wartungsprogramme aber lieber in Perl. Und Perl erwartet einen Wrapper, der dem Script die Rechte gibt.

So ein Wrapper ist eine einfache Angelegenheit:

int main /* insecure */ (int argc, char *argv[])
{
  char prog[PROGLEN], *p1;

  if (getuid() != UID) error("%s: Wrong UID\n", argv[0]);

  if (setregid(0,0) != 0)         error("%s: Warning, cannot set GID\n", argv[0]);
  if (setreuid(0,0) != 0)         error("%s: Warning, cannot set UID\n", argv[0]);

  strncpy(prog, argv[0], PROGLEN-1);
  if ((p1 = strstr(prog, ".cgi")) != NULL) *p1 = '\0';
  strcat(prog, ".pl");

  execv(prog, argv);
}

Der Wrapper prüft, ob er von einem berechtigten Nutzer ausgeführt wird und ermittelt dann den Namen des Scriptes durch Manipulation des Aufrufnamens.

Was ist daran falsch?

Im Prinzip besteht das Problem wieder einmal in einer unzureichender Prüfung der vom Nutzer bereitgestellten Daten.

$ cat > test.pl <<END
#! /usr/bin/perl

print "($<,$>) <$(,$)>\n";
END
$ chmod a+x test.pl
$ ./test.pl
(101,101) <50 50,50 50>
$ ln -s /usr/local/sbin/getroot test
$ ./test
(0,0) <0 50,0 50>

Was ist passiert? Der Wrapper /usr/local/sbin/getroot wurde per symbolischen Link mit einem neuen Namen versehen. Den von der Shell übergebenen Namen überreicht Unix dem verlinkten Programm. Es kann dann - je nach Aufrufnamen - andere Aktionen ausführen. Das Programm gzip wird so beispielsweise als gunzip oder zcat angesprochen. Im Wrapper wurde dann der vom der Shell, also vom Nutzer, bereitgestellte Name abgewandelt und das zugehörige Perl-Script mit erhöhten Rechten ausgeführt.

Ganz offenbar darf man nicht einem Link folgen. Es ist aber auch anders möglich, den aufgerufenen Programm einen anderen Namen vorzugeben. Der Unix-Systemruf execve bietet - als Teil des Konzept von Unix - explizit diese Möglichkeit. Es genügt also nicht, auf die Abwesenheit von Links zu prüfen.

Man muß sicherstellen, dass die benannten Dateien wirklich dem Nutzer Root gehören, aufgerufene und auszuführende Datei. Der passende Code sieht dann etwa so aus:

int main /* much better */ (int argc, char *argv[])
{
  char prog[PROGLEN], *p1;
  struct stat sbuf;

  if (getuid() != UID) error("%s: Wrong UID\n", argv[0]);

  if ('/' != argv[0][0])          error("%s: Absolute path required\n", argv[0]);
  if (0 != lstat(argv[0], &sbuf)) error("%s: Can't stat\n", argv[0]);
  if (!S_ISREG(sbuf.st_mode))     error("%s: Not a regular file\n", argv[0]);
  if (0 != sbuf.st_uid)           error("%s: Not owned by user root\n", argv[0]);
  if (0 != sbuf.st_gid)           error("%s: Not owned by group root\n", argv[0]);

  if (setregid(0,0) != 0)         error("%s: Warning, cannot set GID\n", argv[0]);
  if (setreuid(0,0) != 0)         error("%s: Warning, cannot set UID\n", argv[0]);

  strncpy(prog, argv[0], PROGLEN-1);
  if ((p1 = strstr(prog, ".cgi")) != NULL) *p1 = '\0';
  strcat(prog, ".pl");

  if (0 != lstat(prog, &sbuf))    error("%s: Can't stat\n", prog);
  if (!S_ISREG(sbuf.st_mode))     error("%s: Not a regular file\n", prog);
  if (0 != sbuf.st_uid)           error("%s: Not owned by user root\n", prog);
  if (0 != sbuf.st_gid)           error("%s: Not owned by group root\n", prog);

  execv(prog, argv);
}

Nun kann der Angreifer die Rechtevergrößerung nur noch auf passende Paare von Dateien übertragen, die ohnehin schon Root gehören. Allerdings kann der Angreifer immer noch "die falschen" setuid Wrapper anwenden, also einem Programm andere Rechte geben, als urspünglich vorgesehen.

Was kann man nun noch tun? Man kann sicherstellen, dass die aufzurufende Datei vom real aufgerufenen Programm abgeleitet wird. Dazu kann man den Link /proc/self/exe analysieren. Oder gleich sudo nehmen.

Post a comment

Verwandter Inhalt