记一次踩坑记录
背景
之前因为实习的原因,很多学习笔记没法发出来,导致博客长草一段时间了 。现在离职后发篇文章水一下(
事情的起因源自于一次审计遇到的场景,目标是一个java写的CMS,只不过java版本非常低,.class
字节码文件基本都是1.1/1.4版本的。且风格上有点传统php cms的建站风格,存在大量jsp文件。最后审计下来的结果是,只有一处存在反序列化漏洞,且存在可以利用的gadget。其他未授权的洞基本只能做到写文件,sql注入或者是ssrf.
众所周知,只有无文件落地+不出网+未授权RCE+能打内存马才是一个漏洞利用的最佳形态。不过,该场景的一个困境就在于反序列化触发点监听在一个没有对外的端口,导致我们不能直接RCE。这时候想必第一想法就是:“欸,低版本的jdk貌似是可以支持gopher吧?那不是直接可以ssrf打内部端口触发反序列化么?”
但是当我尝试使用gopher ssrf攻击时,发现并没有打通。开了个端口nc看了下传输的数据流,发现居然出现了奇奇怪怪的问题.
以gopher://127.0.0.1:9002/_%AC%EDjava
为例。
反序列化数据的魔数开头\xAC\xED
在这居然变成了问号?这个我自己之前并没有听说过,所以干脆来调试研究下吧
分析
调试要点没有什么好说的。但是这毕竟是“远古时期”的jdk1.4,如果按照正常流程调试一些底层函数的话,大概率会碰到字节码行数对不上的问题导致断点下不下来或者是跟的行数直接错了的问题。所以还是老老实实把环境的jdk拖出来吧 :face_with_finger_covering_closed_lips:
我们知道无论是java的xxe导致的ssrf还是普通的ssrf本质上都是先把我们输入的url用来实例化一个URL
对象,然后URL#openConnection()
或者是URL#openStream()
,其中openStream
就是this.openConnection().getInputStream()
。
所以我们在这里下断点看下
首先关注下,在用URL实例化时,首先会尝试根据url格式来解析出对应的部分。
顺便一提,这里也出现了java ssrf常见的一个绕waf的方式url:
解析出对应的protocol gopher
后,便会尝试调用getURLStreamHandler
为这个协议去获取对应的handler,
getURLStreamHandler
一般情况下handlers默认是空的。
所以通过解析出来的协议,java会动态找得到处理gopher的handlersun.net.www.protocol.gopher.Handler
。顺便把这个handler放到缓存也就是handlers
变量里面去。
在对url解析,检查完成后,就到了openConnection
的阶段。这里则会去调用对应handler的openConnection
gopher handler 所做的也只是把url对象传给GopherURLConnection
实例化
所以实际上在getInputStream
这才真正开始发包。调用的是GopherClient
的openStream
。
发包这都挺正常的:建立连接,然后发包。然后就到了decodePercent
这。(此处gkey即%AC%EDjava
)
char code完全是对的。但就是偏偏最后返回时包裹了一个new String
。。。。
当然其实这里并没有发生转换,这里返回值虽然套上了String但是遍历时仍然能拿到正确的char的。真正的转换发生在后面的this.serverOutput.print
,调用的就是java.io.PrintStream#write(java.lang.String)
outputstream write时会在flushBuffer这一步替换byte,sun.io.CharToByteConverter#convertAny
可以看到这里存在一个微妙的字符范围.同时如果字符ascii码小于128的话,则会直接转char
,一旦字符不在有效范围(128)以内就直接替换该比特为63
,即?
在发现gopher协议的ssrf如此之后,自然会去想能否用crlf
来解决呢?我们都知道java7/8的低版本下尚且存在crlf漏洞,1.4自然也是存在的。但是稍微跟下http协议的发包流程就可以发现,只是所谓的handler
不一样。最后getInputStream
的时候调用的是属于http协议的HTTPUrlConnection
。继续跟到深处的话:
到这一步不难得出一个结论:java url的请求发起基本都是根据输入构建好要发送的buffer,建立连接后用PrintStream#print
来发包的。也就是说无论用什么协议都传不进特殊字符 orz
可能的特例
虽然上面这个情景大概率是没有办法利用了(如果有请务必教教我)。但是java ssrf 在某些特殊场景下应该还是能利用的。
eg.1: 当时发现这个问题后我在google上搜了一大圈貌似都没有看到分析文章。不过后来看到类似的问题balsn的师傅曾经在2018 的pwn2win CTF中遇到过,
当然他这说的不够精准,毕竟这个不是gopher的锅,而是PrintStream
的问题。
pwn2win的场景是直接构造了一个字符集在0x00-0x7f
的war包,通过gopher来部署上去。。。相当厉害
eg2. 我能想到的另一个场景就是用crlf去攻击redis,通过redis触发反序列化.因为低版本的java还是能CRLF的,而redis支持把16进制字符包裹起来,即直接用"\xac\xed"
等等可见字符来设置数据。就算不行也可以用redis的函数来辅助一下。所以假如通过redis存储序列化数据再在服务端触发,完全是可行的
小结
不同的库对于某种功能或者说是协议的实现还是有着不小差别的,这些有的可能带来橘子哥那种ssrf花式绕过,也有的可能就导致我踩的这个坑了。。。当然还是要拒绝想当然,只有动手调过才能知道坑在哪。