首页
社区
课程
招聘
[原创]CS 4.4 二开笔记:增强篇
2023-10-2 17:17 9420

[原创]CS 4.4 二开笔记:增强篇

2023-10-2 17:17
9420

前言

本文为CS 4.4 二开笔记系列第二篇,主要是针对aggressor客户端进行功能分析与二开。感觉现在的查到的文章较多在于beacon的协议的分析和针对特征的修改,在aggressor的分析还是比较少的,而aggressor又是使用者进行一系列操作的入口,只掌握sleep来编写cna脚本是远远不够的,因此本文将着重讨论针对aggressor功能的分析与扩充,文中可能会对部分步骤进行省略,主要是提供足够的思路来分析aggressor的源码和增强。

Event Log 显示连接的 teamserver 的 IP

之前在实战中遇到的问题是经常忘了连接的是哪台服务器的teamserver,后来分析了一下cscat 4.5的源码,他的设置方式为通过aggressor进行设置,因为我在开发时没有打算在aggressor处设置配置文件,因此使用了不同的方式实现显示,先看一下效果:
图片描述
此处涉及到cna脚本的编写及aggressor的源码,cna脚本的编写可以参考下面的官方链接及狼组的中文文档,本文中不再赘述:
cs插件开发 - 先知社区
Aggressor-Script | 狼组安全团队公开知识库
https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as_cobalt-strike.htm
cna脚本会在点击时检索脚本中是否存在相应的函数,如果不存在则进行函数提交,交给java端进行判断及相应,在这里我修改了默认的default.cna脚本,新增了serverip()函数来进行显示:
图片描述
这个函数的实现并不在cna脚本中,因此在触发时脚本会向aggressor发送serverip这个字符串,然后aggressor会判断是否等于这个字符串来进行响应。aggressor的代码与下面两部分有关:

  1. 注册
  2. 响应
    注册在DataBridge.javascriptLoaded()函数中,需要先注册这个函数的键值:
    图片描述
    响应在在DataBridge.javaevaluate()函数中,当接收到这个键值传来信号时则做出响应:
    图片描述
    aggressorRemoteIp这个字符串是我在connect.java中设置的public static变量,当我们在connect界面连接teamserver时则将对应的host传递给event log,最后达到了我们想要的效果:
    图片描述

钉钉上线提醒

钉钉提醒网上常用的方案为调用agscript来执行python操作,再进行提醒。我感觉这样过于麻烦,因此直接将此功能集成至server端中,只需要在server端的配置文件中填入sever端的配置文件,就可以默认启动,类似
图片描述
server端日志显示:
图片描述
创建方法如下:
新建一个类:
图片描述
接着在server/Beacons.java中调用这个类,我这里是通过配置文件进行的设置,其他方式大家可以自己研究:
图片描述
上线效果就是这样:
图片描述

第一次上线时间显示

这个功能需要涉及到数据库的保存,这里使用的是jdbc,效果如下:
图片描述
在新增这个功能时,发现了cscat 4.5的一个bug,就是无法正确的显示last,后来发现主要原因在于server/Beacons.java的checkin函数循环导致无法正确刷新map。目前更改后的checkin代码如下:

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
public void checkin(ScListener request, BeaconEntry var2) {
    synchronized (this) {
        if (!var2.isEmpty()) {
            BeaconEntry var4 = (BeaconEntry) this.privateMap.get(var2.getId());
            if (var4 == null || var4.isEmpty()) {
                ServerUtils.addTarget(this.resources, var2.getInternal(), var2.getComputer(), (String) null, var2.getOperatingSystem(), var2.getVersion());
                ServerUtils.addSession(this.resources, var2.toMap());
                if (!var2.isLinked() && request != null) {
                    ServerUtils.addC2Info(this.resources, request.getC2Info(var2.getId()));
                }
 
                this.resources.broadcast("eventlog", LoggedEvent.BeaconInitial(var2));
                this.initial.add(var2.getId());
                this.resources.process(var2);
            }
        }
 
        // 2023-09-19 TOP
        this.Cmp = var2.getComputer();
        if (var2.isSSH() && this.Cmp.contains(SVGSyntax.OPEN_PARENTHESIS)) {
            this.Cmp = var2.getComputer().replace(SVGSyntax.OPEN_PARENTHESIS, "");
            this.Cmp = this.Cmp.replace(")", "");
            this.Cmp = this.Cmp.replace(var2.getPid(), "");
        }
        String BeaconHash = this.hash(var2.getInternal(), var2.getUser(), var2.getProcess(), this.Cmp, var2.getListenerName(), var2.arch(), var2.getPid());
        info BeaconInfo = new info();
        BeaconInfo.BeaconId = var2.getBeaconId();
        BeaconInfo.Internal = var2.getInternal();
        BeaconInfo.External = var2.getExternal();
        BeaconInfo.Process = var2.getProcess();
        BeaconInfo.Arch = var2.arch();
        BeaconInfo.Computer = this.Cmp;
        BeaconInfo.User = var2.getUser();
        BeaconInfo.Hash = BeaconHash;
        try {
            Connection conn = SqliteSave.OpenDb();
            try {
                HashMap<String, String> beacons = SqliteSave.CheckBeaconHash(conn, BeaconHash);
                if (beacons == null) { // 如果这个机器第一次上线: beacons为null,则打开数据库连接,将BeaconInfo添加到数据库中,然后尝试从beacons中获取"StartTime"并赋值给 BeaconInfo.StartTime
                    Connection conn4 = SqliteSave.OpenDb();
                    SqliteSave.AddBeacon(conn4, BeaconInfo);
                    var2.start = utils.BeijingTime.formatToBeijingTime();
                    ;
                    // 2023-09-18 dingTalk TOP
                    try {
                        String token = TeamServer.globalDingtalkToken;
 
                        String[] args = new String[2];
                        args[0] = token;
                        args[1] = "CobaltStrike主机上线提醒+1" + "\\n";
                        args[1] += "计算机名:" + var2.getComputer() + "\\n";
                        args[1] += "IP地址:" + var2.getExternal() + "\\n";
                        args[1] += "归属地:" + var2.getIpAddress() + "\\n";
                        args[1] += "用户名:" + var2.getUser() + "\\n";
                        args[1] += "进程名:" + var2.getProcess() + "\\n";
                        args[1] += "PID:" + var2.getPid() + "\\n";
 
                        utils.DingtalkSendMsg.send(args);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                    // 2023-09-18 END
                } else {
                    Connection conn2 = SqliteSave.OpenDb();
                    HashMap<String, String> beacons2 = SqliteSave.CheckBeacon(conn2, BeaconHash, var2.getBeaconId());
                    if (beacons2 == null) {
                        Connection con3 = SqliteSave.OpenDb();
                        BeaconInfo.StartTime = beacons.get("StartTime");
                        SqliteSave.AddBeacon2(con3, BeaconInfo);
                    } else {
                        var2.start = beacons2.get("StartTime");
                    }
                }
                if (conn != null) {
                    conn.close();
                }
                this.privateMap.put(var2.getId(), var2);  // <-- core code 此处实时更新
                // CommonUtils.print_info("[1] var2.getId(): " + var2.getId() + "\nvar2: " + var2.getLastCheckin());
            } catch (Throwable th) {
                if (conn != null) {
                    try {
                        conn.close();
                    } catch (Throwable th2) {
                        th.addSuppressed(th2);
                    }
                }
                throw th;
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
        // 2023-09-19 END
    }
}

数据库的实现:

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
//    InitDb(): 初始化数据库。创建一个新的SQLite数据库,并在其中创建一个名为Beacon的表。
//    OpenDb(): 打开数据库连接。连接到SQLite数据库并返回连接。
//    CheckBeaconHash(): 检查特定哈希值的beacon(信标)是否存在。
//    CheckBeacon(): 检查特定哈希值和beacon ID的beacon是否存在。
//    CheckSShBeacon(): 检查SSH beacon是否存在。
//    AddBeacon(): 将一个新的beacon添加到数据库。
//    AddBeacon2(): 将一个新的beacon添加到数据库,但这个方法允许更多的参数。
//    UpBeaconNote(): 更新特定beacon的注释。
//    UpBeaconLastTime(): 更新特定beacon的最后活动时间。
//    UpBeaconId(): 更新特定哈希值的beacon的ID。
public class SqliteSave {
    public static void InitDb() {
        try {
            Class.forName("org.sqlite.JDBC");
 
            Connection c = DriverManager.getConnection("jdbc:sqlite:sqlite/beacon.db");
            CommonUtils.print_good("[DB] The sqlite/beacon.db is created successfully!");
            Statement stmt = c.createStatement();
            stmt.executeUpdate("CREATE TABLE Beacon (Id INTEGER PRIMARY KEY AUTOINCREMENT, BeaconId        CHAR(50),  Hash        CHAR(50),  StartTime        CHAR(50),  External       CHAR(50),  Internal       CHAR(50),  Computer       CHAR(50),  Process        CHAR(50),  User        CHAR(50),  Arch        CHAR(50),  Note         CHAR(50), UpdateNoteTime         CHAR(50))");
            if (stmt != null) {
                stmt.close();
            }
            if (c != null) {
                c.close();
            }
        } catch (Exception e) {
            System.err.println(e.getClass().getName() + ": " + e.getMessage());
            System.exit(0);
        }
        CommonUtils.print_info("[DB] The database is initialized successfully!");
    }
 
    public static Connection OpenDb() {
        Connection c = null;
        try {
            Class.forName("org.sqlite.JDBC");
            c = DriverManager.getConnection("jdbc:sqlite:sqlite/beacon.db");
        } catch (Exception e) {
            System.err.println(e.getClass().getName() + ": " + e.getMessage());
            System.exit(0);
        }
        return c;
    }
 
    public static HashMap<String, String> CheckBeaconHash(Connection c, String hash) {
        HashMap<String, String> Beacon = new HashMap<>();
        try {
            PreparedStatement ps = c.prepareStatement("SELECT * FROM Beacon WHERE Hash = (?) ORDER BY UpdateNoteTime DESC;");
            ps.setString(1, hash);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                if (rs.getString("Hash").equals(hash)) {
                    Beacon.put("StartTime", rs.getString("StartTime"));
                    Beacon.put("Note", rs.getString("Note"));
                    ps.close();
                    c.close();
                    return Beacon;
                }
            }
            rs.close();
            ps.close();
            c.close();
            return null;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
 
    public static HashMap<String, String> CheckBeacon(Connection c, String hash, String BeaconId) {
        HashMap<String, String> Beacon = new HashMap<>();
        try {
            PreparedStatement ps = c.prepareStatement("SELECT * FROM Beacon WHERE Hash = (?) AND BeaconId = (?) ORDER BY UpdateNoteTime DESC;");
            ps.setString(1, hash);
            ps.setString(2, BeaconId);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                if (rs.getString("Hash").equals(hash)) {
                    Beacon.put("StartTime", rs.getString("StartTime"));
                    Beacon.put("Note", rs.getString("Note"));
                    ps.close();
                    c.close();
                    return Beacon;
                }
            }
            rs.close();
            ps.close();
            c.close();
            return null;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
 
    public static HashMap<String, String> CheckSShBeacon(Connection c, info BeaconInfo) {
        HashMap<String, String> Beacon = new HashMap<>();
        try {
            PreparedStatement ps = c.prepareStatement("SELECT * FROM Beacon WHERE Computer = (?) and User = (?) and Arch = (?) and Process = (?) and External = (?) ORDER BY id DESC;");
            ps.setString(1, BeaconInfo.Computer);
            ps.setString(2, BeaconInfo.User);
            ps.setString(3, BeaconInfo.Arch);
            ps.setString(4, BeaconInfo.Process);
            ps.setString(5, BeaconInfo.External);
            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                if (rs != null) {
                    Beacon.put("Hash", rs.getString("Hash"));
                    Beacon.put("StartTime", rs.getString("StartTime"));
                    Beacon.put("Note", rs.getString("Note"));
                    Beacon.put("Id", rs.getString("Id"));
                    Beacon.put("External", rs.getString("External"));
                    Beacon.put("Internal", rs.getString("Internal"));
                    Beacon.put(DOMKeyboardEvent.KEY_PROCESS, rs.getString(DOMKeyboardEvent.KEY_PROCESS));
                    Beacon.put("Arch", rs.getString("Arch"));
                    Beacon.put("User", rs.getString("User"));
                    Beacon.put("Computer", rs.getString("Computer"));
                    ps.close();
                    c.close();
                    return Beacon;
                }
            }
            rs.close();
            ps.close();
            c.close();
            return null;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
 
    public static void AddBeacon(Connection c, info BeaconInfo) {
        String times = utils.BeijingTime.formatToBeijingTime();
        try {
            PreparedStatement ps = c.prepareStatement("INSERT INTO Beacon (Hash,StartTime,Note,BeaconId,External,Process,Arch,User,Computer,Internal,UpdateNoteTime) VALUES (?, ?, \"\",?,?,?,?,?,?,?,?);");
            ps.setString(1, BeaconInfo.Hash);
            ps.setString(2, times);
            ps.setString(3, BeaconInfo.BeaconId);
            ps.setString(4, BeaconInfo.External);
            ps.setString(5, BeaconInfo.Process);
            ps.setString(6, BeaconInfo.Arch);
            ps.setString(7, BeaconInfo.User);
            ps.setString(8, BeaconInfo.Computer);
            ps.setString(9, BeaconInfo.Internal);
            ps.setString(10, times);
            ps.execute();
            if (ps != null) {
                ps.close();
            }
            if (c != null) {
                c.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
 
    public static void AddBeacon2(Connection c, info BeaconInfo) {
        try {
            PreparedStatement ps = c.prepareStatement("INSERT INTO Beacon (Hash,StartTime,Note,BeaconId,External,Process,Arch,User,Computer,Internal,UpdateNoteTime) VALUES (?, ?, ?,?,?,?,?,?,?,?,\"\");");
            ps.setString(1, BeaconInfo.Hash);
            ps.setString(2, BeaconInfo.StartTime);
            ps.setString(3, BeaconInfo.Note);
            ps.setString(4, BeaconInfo.BeaconId);
            ps.setString(5, BeaconInfo.External);
            ps.setString(6, BeaconInfo.Process);
            ps.setString(7, BeaconInfo.Arch);
            ps.setString(8, BeaconInfo.User);
            ps.setString(9, BeaconInfo.Computer);
            ps.setString(10, BeaconInfo.Internal);
            ps.execute();
            if (ps != null) {
                ps.close();
            }
            if (c != null) {
                c.close();
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
 
    public static void UpBeaconNote(Connection c, String BeaconId, String Note) {
        String times = utils.BeijingTime.formatToBeijingTime();;
        try {
            PreparedStatement ps = c.prepareStatement("UPDATE Beacon set Note = ? , UpdateNoteTime = ? where BeaconId=?;");
            ps.setString(1, Note);
            ps.setString(2, times);
            ps.setString(3, BeaconId);
            ps.executeUpdate();
            ps.close();
            c.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    public static void UpBeaconLastTime(Connection c, String BeaconId, String Note) {
        String times = utils.BeijingTime.formatToBeijingTime();;
        try {
            PreparedStatement ps = c.prepareStatement("UPDATE Beacon set LastTime = ? where BeaconId=?;");
            ps.setString(1, times);
            ps.setString(2, BeaconId);
            ps.executeUpdate();
            ps.close();
            c.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
 
    public static void UpBeaconId(Connection c, String Hash, String BeaconId) {
        try {
            PreparedStatement ps = c.prepareStatement("UPDATE Beacon set BeaconId = ? where Hash=?;");
            ps.setString(1, BeaconId);
            ps.setString(2, Hash);
            ps.executeUpdate();
            ps.close();
            c.close();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

随机用户名后缀

在护网时遇到比较多的一个问题,就是开着热点上的cs,中途去吃饭了结果回来再连上热点就会显示该用户已使用,导致就要重启客户端,因此在此处新增用户名后缀防止重复登录。
主要修改点在server/ManageUser.java中,在process函数中新增:
图片描述
这个随机字符串我控制在长度为3,对应实现的类为:
图片描述
最终效果就是这样,终于解决了这个烦人的问题:
图片描述

aggressor 新增监听器时默认显示远程 IP

在原版CS中,每次新增监听器时默认值为本地的IP地址,还需要手动改为远程IP地址,因此在这里对这部分代码进行修改:
Connect.java:新增public static String aggressorRemoteIp = "";
图片描述
图片描述
然后在ScListenerDialog.java修改显示参数:
图片描述
show_httpshow_https都改掉:
图片描述
最后更改效果为:
图片描述

CNA 脚本触发java端内置函数

这个操作就跟上面 Event log显示远程IP差不多,主要单独列出来方便直接看。
default.cna:
图片描述

1
item("&New Connection", { openConnectDialog(); });

调用的是openConnectDialog(),这个在java端的aggressor/bridges/AggressorBridges.java中进行判断,如果点击了这个,则调用这个文件中的代码。AggressorBridges.java先是注册,再是响应:
注册:
图片描述
响应:
图片描述
最后执行的代码为:

1
(new ConnectDialog(this.window)).show();

这个代码是在aggressor/dialogs/ConnectDialog.java中的类。实际上aggressor界面上的那些按钮的触发都是这个套路,因此我们后续如果不想用纯粹的cna脚本,就可以用cna脚本触发,函数写死在java端,好处就是把一些不改动的cna脚本写死在里面,不用拷贝给队友时还需要额外附带脚本。cna脚本写死并加载的地方在aggressor/AggressorClient.java中:
图片描述

aggressor 提取监听器信息

这个没什么绕的地方,主要就是分析代码,我直接贴提取方式了:

1
2
3
String lname = DialogUtils.string(var2, "listener"); // 监听器名称,通过 lname 可获得 lhost 和 lport
String lhost = ListenerUtils.getListener(this.client, lname).getCallbackHosts();    // 监听器IP
String lport = String.valueOf(ListenerUtils.getListener(this.client, lname).getPort()); // 监听器端口

我之所以弄这个,是之前想将免杀模块放在本地来进行操作,后来这个方案被我废弃了,打算将免杀流程放在teamserver端进行操作,再回传至aggressor。

aggressor 发送信号

发送信号我是分析的钓鱼模块,aggressor交互都在aggressor/dialogs里面,发送信号使用的是TeamQueue类,CS的通信主要用的就是这个类:

1
2
protected TeamQueue conn = null;
this.conn.call("SendSign.test", CommonUtils.args(var1, var2, var3, checksum));

其中"SendSign.test"代表的是发送数据的标签,服务端需要根据这个标签进行判断,后面的几个都是传递的参数。我自己实现的调用逻辑为:
dialogAction() -> send() -> final_send()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void dialogAction(ActionEvent var1, Map var2) {
    String lname = DialogUtils.string(var2, "listener"); // 监听器名称,通过 lname 可获得 lhost 和 lport
    String lhost = ListenerUtils.getListener(this.client, lname).getCallbackHosts();    // 监听器IP
    String lport = String.valueOf(ListenerUtils.getListener(this.client, lname).getPort()); // 监听器端口
     
    String[] stringArray = {lname,lhost,lport};
    this.send(var1, var2, stringArray); // var1 与 var2 固定,var3 传递传给 final_send 的参数
}
 
public void send(ActionEvent var1, Map var2,String[] var3) {
    this.final_send(var3[0],var3[1],var3[2]);
}
private void final_send(String var1,String var2,String var3) {
    this.conn.call("SendSign.test", CommonUtils.args(var1, var2, var3, checksum));
    System.out.println("发送的checksum为:"+ checksum);
}

server 接收信号

与dialog.java成对存在的就是server目录中的文件,我这自己新建了一个类,具体接收信号的函数为:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void call(Request var1, ManageUser var2) { // 实际运行的函数
    String var4;
    if (var1.is("SendSign.test", 4)) {  // 前三个参数跟编译相关,第四个为 checksum 校验值
        synchronized(this) {
            int result = genCrossC2((String) var1.arg(0), (String) var1.arg(1), (String) var1.arg(2)); // 打印输入的参数,需要将 object 强制转为 string
            CommonUtils.print_info("result: "+ result);
            if (result == 0){
                this.resources.broadcast("genCrossC2", (String) var1.arg(3), true);
                System.out.println("send checksum: "+ (String) var1.arg(3));
            }
        }
    }
}

其中比较重要的就是var1.is("SendSign.test", 4),这个与aggressor传来的数据是相对应的,4为四个参数。

server 进行广播

广播的代码为:

1
this.resources.broadcast("genCrossC2", (String) var1.arg(3), true);

这个true的含义貌似是aggressor重连后还会接收到这个信息,我改为false也没什么变化,广播的数据为Map类型。

aggressor 接收广播

接收广播部分有个巨坑,如果直接使用TeamQueue提供的setSubscriber回调函数来接收广播,则会导致CS的UI不会刷新。存在问题的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
conn.setSubscriber(new Callback() {
    @Override
    public void result(String call, Object content) { //
        if ("genCrossC2".equals(call)) {
            System.out.println("接收到 genCrossC2 广播!");
            if (content instanceof String) {
                String contentString = (String) content;
                if ("OK".equals(contentString)) {
                    System.out.println("广播的内容是 'OK'!");
                }
            }
        }
    }
});

这个代码虽然可以正确接收,但是UI不刷新肯定是存在问题的,后来换了一个方案:

1
2
3
4
5
6
this.conn.setSubscriber(this.data);
String response = data.getDataSafe("genCrossC2").toString();
if(checksum.equals(response)){
    System.out.println("接收到服务端广播的内容是:" + checksum);
    break;
}

在使用这个时,一定要注意是否data中存在这个键值对,如果不存在就会空指针异常,因此我在dialogAction中去检索data数据前先使用this.data.put("key","value");进行了置空操作,问题就解决了。

总结

以上是基于下面的模型进行讲解的,掌握这个模型一系列操作方式后CS的可玩性还是非常高的。
图片描述
本人也是刚刚学习java开发,文中可能存在诸多问题,欢迎大家指出,最后祝大家国庆快乐~


[培训]科锐软件逆向50期预科班报名即将截止,速来!!! 50期正式班报名火爆招生中!!!

最后于 2023-10-2 17:21 被bwner编辑 ,原因:
收藏
免费 1
打赏
分享
最新回复 (1)
雪    币: 20876
活跃值: (30203)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-10-2 22:10
2
1
感谢分享
游客
登录 | 注册 方可回帖
返回