Brup插件开发手记

Brup插件开发手记前言在一些攻防演练中,像Shiro、Fastjson等常见高危漏洞一直被高频利用。但在一些情况下,这些漏洞通过几轮的洗刷下来出现的频率会逐渐变少。在打点的时候,一些平时并不会去

大家好,又见面了,我是全栈君,祝每个程序员都可以多学几门语言。

Brup插件开发手记

前言

在一些攻防演练中,像Shiro、Fastjson等常见高危漏洞一直被高频利用。但在一些情况下,这些漏洞通过几轮的洗刷下来出现的频率会逐渐变少。在打点的时候,一些平时并不会去测试的漏洞可能就能在某次中进行利用,并且借助打下一个内网据点。而我本身在真实做法中并不会刻意的去找一些漏洞,如springboot 的一些rce漏洞。遇到的Springboot会比较多,而挨个测时间成本就高了。即便大型扫描器对于这些都能够实现,但个人并不喜欢上这些大型扫描器。 所以在此萌生了一个念头,编写burp的插件挂着后台进行被动扫描。模块化开发,添加多个漏洞进行被动扫描。写好以后后面则是赌运气(阳寿)的时候了。

开发规范

所有的burp插件都必须实现IBurpExtender这个接口
实现类的包名称必须是burp
实现类的名称必须是BurpExtender
实现类比较是public的
实现类必须有默认构造函数(public,无参),如果没有定义构造函数就是默认构造函数

动态调试

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Xbootclasspath/p:burp-loader-keygen-BurpPro.jar -jar burpsuite_pro_v1.7.37.jar

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 -Xbootclasspath/p:BurpHelper.jar -jar burpsuite_pro_v1.7.37.jar

编写

package burp;

import java.io.PrintWriter;


public class BurpExtender implements IBurpExtender{
        public String NANE= "T_Scan";
        public String VERISON ="V1.0";
        public PrintWriter stdout;
        public IBurpExtenderCallbacks callbacks;
    public Tags tags;
      

        @Override
        public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks)  {
            tags = new Tags(callbacks,NANE);
            this.stdout = new PrintWriter(callbacks.getStdout(), true);//获取当前拓展插件得输出流
            this.stdout.println("===================================");
            this.stdout.println(String.format("%s" , new Object[] { NANE }));
            this.stdout.println(String.format("%s", new Object[] { VERISON }));
            this.stdout.println("nice0e3");
            this.stdout.println("nice0e3");
            this.stdout.println("nice0e3");
            this.stdout.println("nice0e3");
            this.stdout.println("nice0e3");
            this.stdout.println("nice0e3");
            this.stdout.println("===================================");
            callbacks.setExtensionName(NANE);//设置当前扩展的显示名称,该名称将显示在Extender工具的用户界面中。
            
        }
        
    }
public class Tags implements ITab{

    public  String tagName;
    public IBurpExtenderCallbacks callbacks;

    public JPanel jPanel;
    public JButton jButton;


    public Tags(final IBurpExtenderCallbacks callbacks, String name) {
        this.callbacks = callbacks;
        this.tagName = name;

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {

                jPanel = new JPanel();

                JButton jButton = new JButton("xxxx");

                // 将按钮添加到 主面板 jPanelMain 中.
                jPanel.add(jButton);

                // 设置自定义组件并添加标签
 callbacks.customizeUiComponent(jPanel);//根据Burp的UI样式自定义UI组件,包括字体大小、颜色、表格行距等。
                callbacks.addSuiteTab(Tags.this);//此方法用于向Burp套件主窗口添加自定义选项卡。
            }
        });
    }
//burp使用该方法获取选项卡名字
    public String getTabCaption() {
        return "T_scan";
    }
//Burp 使用此方法获取显示时应用作自定义选项卡内容的组件。
    public Component getUiComponent() {
        return jPanel;
    }
};

代码其实很简单,在BurpExtenderregisterExtenderCallbacks方法对Tags进行调用即可。

Brup插件开发手记

接下来就是对GUI和Payload的发送,结果的输出。三大部分内容。

消息编辑器

package burp;

import javax.swing.*;
import javax.swing.table.AbstractTableModel;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.annotation.Target;

public class Tags implements ITab,IMessageEditorController{

    public  String tagName;
    public IBurpExtenderCallbacks callbacks;

    public JPanel jPanel;
    JTabbedPane jTabbedPane;

    IMessageEditor  request;
    IMessageEditor response;

    JTabbedPane tabs;
   public JTabbedPane jTabbedPane1;
   public JSplitPane splitPanel;


    public Tags(final IBurpExtenderCallbacks callbacks, String name) {
        this.callbacks = callbacks;
        this.tagName = name;

        SwingUtilities.invokeLater(new Runnable() {


            @Override
            public void run() {


                jPanel = new JPanel();

               splitPanel = new JSplitPane(0);
                splitPanel.setDividerLocation(0.5D);
                jTabbedPane = new JTabbedPane();
                jTabbedPane1 = new JTabbedPane();
                JButton giao = new JButton("giao");
                jTabbedPane1.add(giao);
                request  = callbacks.createMessageEditor(Tags.this, false);
                response = callbacks.createMessageEditor(Tags.this,false);
                jTabbedPane.addTab("Request",request.getComponent());
                jTabbedPane.addTab("Response",response.getComponent());
                splitPanel.add(jTabbedPane);
                splitPanel.setTopComponent(jTabbedPane1);
                splitPanel.setBottomComponent(jTabbedPane);
                jPanel.add(splitPanel);

//                // 设置自定义组件并添加标签
                callbacks.customizeUiComponent(jPanel);//根据Burp的UI样式自定义UI组件,包括字体大小、颜色、表格行距等。
                callbacks.addSuiteTab(Tags.this);

            }
        });
    }

//burp使用该方法获取选项卡名字
    public String getTabCaption() {
        return "T_scan";
    }
//Burp 使用此方法获取显示时应用作自定义选项卡内容的组件。
    public Component getUiComponent() {
        return jPanel;
    }


    @Override
    public IHttpService getHttpService() {
        return null;
    }

    @Override
    public byte[] getRequest() {
        return new byte[0];
    }

    @Override
    public byte[] getResponse() {
        return new byte[0];
    }
};



  Tags.this.mjSplitPane = new JSplitPane(0);
                Tags.this.Utable = new Tags.URLTable(Tags.this);
//                JTable jTable = new JTable();
                Tags.this.UscrollPane = new JScrollPane(Tags.this.Utable);
//                Tags.this.UscrollPane = new JScrollPane(jTable);
                Tags.this.HjSplitPane = new JSplitPane();
                Tags.this.HjSplitPane.setDividerLocation(0.5D);
                Tags.this.Ltable = new JTabbedPane();
                Tags.this.HRequestTextEditor = Tags.this.callbacks.createMessageEditor(Tags.this, false);
                Tags.this.Ltable.addTab("Request", Tags.this.HRequestTextEditor.getComponent());
                Tags.this.Rtable = new JTabbedPane();
                Tags.this.HResponseTextEditor = Tags.this.callbacks.createMessageEditor(Tags.this, false);
                Tags.this.Rtable.addTab("Response", Tags.this.HResponseTextEditor.getComponent());
                Tags.this.HjSplitPane.add(Tags.this.Ltable, "left");
                Tags.this.HjSplitPane.add(Tags.this.Rtable, "right");
                Tags.this.mjSplitPane.add(Tags.this.UscrollPane, "left");
                Tags.this.mjSplitPane.add(Tags.this.HjSplitPane, "right");
                Tags.this.callbacks.customizeUiComponent(Tags.this.mjSplitPane);
                Tags.this.callbacks.addSuiteTab(Tags.this);

Brup插件开发手记

插件入口和帮助接口类:

IBurpExtender、IBurpExtenderCallbacks、 IExtensionHelpers、IExtensionStateListener

HTTP处理接口类:

ICookieIHttpRequestResponsePersistedIHttpRequestResponseWithMarkersIHttpServiceIRequestInfoIParameterIResponseInfo

Burp工具组件接口类

IIntruderPayloadGeneratorIIntruderPayloadGeneratorFactoryIIntruderPayloadProcessorIProxyListenerIScanIssueIScannerCheck IScannerInsertionPointIScannerInsertionPointProviderIScannerListenerIScanQueueItemIScopeChangeListener

写插件常用的接口

IBurpExtender

registerExtenderCallbacks是IBurpExtender接口的实现类,与Burp的其他组件(Scanner Intruder Spider)及通信对象连接(HttpRequestResponse HttpService SessionHandlingAction)之间的连接。所有拓展插件必须实现这个接口。

IHttpRequestResponse

此接口用于检索和更新有关HTTP消息的详细信息。注意:setter方法通常只能在处理消息之前使用,而不能在只读上下文中使用。与响应细节相关的getter方法只能在发出请求之后使用

方法

java.lang.String	getComment()
This method is used to retrieve the user-annotated comment for this item, if applicable.
    
java.lang.String	getHighlight()
This method is used to retrieve the user-annotated highlight for this item, if applicable.
    
IHttpService	getHttpService()
This method is used to retrieve the HTTP service for this request / response.
    
byte[]	getRequest()
This method is used to retrieve the request message.
    
byte[]	getResponse()
This method is used to retrieve the response message.
    
void	setComment(java.lang.String comment)
This method is used to update the user-annotated comment for this item.
    
void	setHighlight(java.lang.String color)
This method is used to update the user-annotated highlight for this item.
    
void	setHttpService(IHttpService httpService)
This method is used to update the HTTP service for this request / response.
    
void	setRequest(byte[] message)
This method is used to update the request message.
    
void	setResponse(byte[] message)
This method is used to update the response message.

IHttpListener

可通过调用IBurpExtenderCallbacks.registerHttpListener()注册一个HTTP监听器,Burp的任何一个接口发起HTTP请求或者收到HTTP响应都会通知此监听器。该接口可得到这些交互数据,进行分析和修改。

方法如下:

void processHttpMessage(int toolFlag,boolean messageIsRequest,IHttpRequestResponse messageInfo);

发起HTTP请求或者收到HTTP响应都会通知此监听器

参数:
//toolFlag:指示了发起请求或收到响应的 Burp 工具的 ID,所有的 toolFlag 定义在 IBurpExtenderCallbacks 接口中。
//messageIsRequest:指示该消息是请求消息(值为True)还是响应消息(值为False)
//messageInfo:被处理的消息的详细信息,是一个 IHttpRequestResponse 对象    

IContextMenuFactory

用来实现菜单效果

public class BurpExtender implements IBurpExtender, IContextMenuFactory{

@Override
public void registerExtenderCallbacks(final IBurpExtenderCallbacks callbacks){

    //插件名称
    callbacks.setExtensionName("T_SCAN");

    //注册菜单
    callbacks.registerContextMenuFactory(this);
}


@Override
public List<JMenuItem> createMenuItems(final IContextMenuInvocation invocation) {

    List<JMenuItem> listMenuItems = new ArrayList<JMenuItem>();

    //子菜单
    JMenuItem menuItem;
    menuItem = new JMenuItem("子菜单");  

    //父级菜单
    JMenu jMenu = new JMenu("父菜单");

    //菜单操作
    jMenu.add(menuItem);        
    listMenuItems.add(jMenu);


    return listMenuItems;
}


ICookie

方法:

// 此方法用于获取 Cookie 的域
java.lang.String    getDomain()

// 此方法用于获取 Cookie 的过期时间
java.util.Date  getExpiration()

// 此方法用于获取 Cookie 的名称
java.lang.String    getName()

// 此方法用于获取 Cookie 的路径
java.lang.String    getPath()

// 此方法用于获取 Cookie 的值
java.lang.String    getValue()

IExtensionHelpers

此接口提供很多常用的辅助方法,可通过调用IBurpExtenderCallbacks.getHelpers获得此接口的实例。

方法:

byte[]  addParameter(byte[] request, IParameter parameter) //添加参数到指定的请求中,并更新Content-Length

IRequestInfo analyzeRequest(byte[] request)  //分析request的请求信息
IResponseInfo analyzeResponse(byte[] response)  //分析response的响应消息

byte[]  buildHttpMessage(java.util.List<java.lang.String> headers, byte[] body)  //构建请求包,返回响应报

byte[]  buildHttpRequest(java.net.URL url) //向指定的url发起get请求
    
java.lang.String    bytesToString(byte[] data)   //bytes到String的转换
    
java.lang.String    bytesToString(byte[] data)   //String到bytes的转换

IHttpRequestResponse

java.lang.String getComment()          //获取用户的标注信息
java.lang.String getHighlight()       //获取用户标注的高亮信息
IHttpService getHttpService()   //获取请求响应的http服务信息    

byte[]  getRequest()  // 获取 HTTP 请求信息
byte[]  getResponse()  // 获取 HTTP 响应信息

void    setHttpService(IHttpService httpService)  //更新请求/响应HTTP服务信息
void    setRequest(byte[] message)    // 更新 HTTP 请求信息
void    setResponse(byte[] message)  // 更新 HTTP 响应信息

IHttpService

此接口用于提供关于 HTTP 服务信息的细节

方法:

java.lang.String getHost()    
int getPort()    
java.lang.String getProtocol()    

IInterceptedProxyMessage

该接口不能被扩展实现,它表示已被Burp代理拦截的HTTP消息。我们可以利用接口注册一个IProxyListener以得到代理消息的细节。

callbacks.registerProxyListener(this);  //注册代理监听器

IHttpRequestResponse message1 = message.getMessageInfo();  //请求的详细信息
int action = message.getInterceptAction();      //当前的拦截操作类型    

//获取客户端的Ip,即代理Ip
stdout.println(message.getClientIpAddress());

//获取当前的拦截操作类型  
stdout.println(action);

//获取请求的详细信息
stdout.println(message1);

// Drop 掉所有请求
//message.setInterceptAction(IInterceptedProxyMessage.ACTION_DROP);

Itab

此接口用于使用IBurpExtenderCallbacks.addSuiteTab()等方法向Burp提供将添加到Burp UI的自定义选项卡的详细信息。

IScannerCheck

扩展可以实现此接口,然后调用IBurpExtenderCallbacks.registerScannerCheck()来注册自定义扫描程序检查。执行扫描时,Burp将要求支票对基本请求执行主动或被动扫描,并报告发现的任何扫描仪问题。

方法:

int	consolidateDuplicateIssues(IScanIssue existingIssue, IScanIssue newIssue)
当自定义扫描程序检查报告了同一URL路径的多个问题时,扫描程序调用此方法。
    
java.util.List<IScanIssue>	doActiveScan(IHttpRequestResponse baseRequestResponse, IScannerInsertionPoint insertionPoint)

扫描器为每个被主动扫描的插入点调用此方法。
    
    
java.util.List<IScanIssue>	doPassiveScan(IHttpRequestResponse baseRequestResponse)

扫描器为被动扫描的每个基本请求/响应调用此方法。

IScanIssue

此接口用于检索扫描仪问题的详细信息。扩展可以通过注册IsCannelListener或调用IBurpExtenderCallbacks.getScanIssues()获取问题的详细信息。扩展还可以通过注册IScannerCheck或调用IBurpExtenderCallbacks.addScanIssue()并提供自己的此接口实现来添加自定义扫描程序问题。请注意,由扩展生成的问题描述和其他文本受HTML白名单的约束,该白名单只允许格式化标记和简单的超链接。

IRequestInfo

此接口用于检索有关HTTP请求的关键详细信息。扩展可以通过调用IExtensionHelpers.AnalyzerRequest()为给定请求获取IRequestInfo对象。

而这个IExtensionHelpers实例对象可靠callbacks.getHelpers()获取。

IMessageEditorController

IMessageEditor使用此接口获取有关当前显示消息的详细信息。创建Burp的HTTP消息编辑器实例的扩展可以选择性地提供IMessageEditorController的实现,当编辑器需要关于当前消息的进一步信息(例如,将其发送到另一个Burp工具)时,它将调用IMessageEditorController。通过IMessageEditorTabFactory提供自定义编辑器选项卡的扩展将为它们生成的每个选项卡实例接收对IMessageEditorController对象的引用,如果选项卡需要有关当前消息的更多信息,则可以调用该引用。

IHttpService getHttpService() 
此方法用于检索当前消息的HTTP服务。 
byte[] getRequest() 
此方法用于检索与当前消息关联的HTTP请求(它本身可能是响应)。 
byte[] getResponse() 
此方法用于检索与当前消息(本身可能是请求)关联的HTTP响应。 

AbstractTableModel

java提供的AbstractTableModel是一个抽象类,这个类帮我们实现大部份的TableModel方法,除了getRowCount(),getColumnCount()getValueAt()这三个方法外。因此我们的主要任务就是去实现这三个方法.利用这个抽象类就可以设计出不同格式的表格

	void addTableModelListener(TableModelListener l):使表格具有处理TableModelEvent的能力.当表格的Table Model有所变化时,会发出TableModelEvent事件信息。

  int findColumn(String columnName):寻找在行名称中是否含有columnName这个项目.若有,则返回其所在行的位置;反之则返回-1表示未找到。

  void fireTableCellUpdated(int row, int column):通知所有的Listener在这个表格中的(row,column)字段的内容已经改变了。

  void fireTableChanged(TableModelEvent e):将所收的事件通知传送给所有在这个table model中注册过的TableModelListeners。

  void fireTableDataChanged():通知所有的listener在这个表格中列的内容已经改变了.列的数目可能已经改变了,因此JTable可能需要重新显示此表格的结构。

  void fireTableRowsDeleted(int firstRow, int lastRow):通知所有的listener在这个表格中第firstrow行至lastrow列已经被删除了。

  void fireTableRowsUpdated(int firstRow, int lastRow):通知所有的listener在这个表格中第firstrow行至lastrow列已经被修改了。

  void fireTableRowsInserted(int firstRow, int lastRow):通知所有的listener在这个表格中第firstrow行至lastrow列已经被加入了。

  void fireTableStructureChanged():通知所有的listener在这个表格的结构已经改变了.行的数目,名称以及数据类型都可能已经改变了

  Class getColumnClass(int columnIndex):返回字段数据类型的类名称。

  String getColumnName(int column):若没有设置列标题则返回默认值,依次为A,B,C,...Z,AA,AB,..;若无此column,则返回一个空的String

  Public EventListener[] getListeners(Class listenerType):返回所有在这个table model所建立的listener中符合listenerType的listener,并以数组形式返回。

  boolean isCellEditable(int rowIndex, int columnIndex):返回所有在这个table model所建立的listener中符合listenerType形式的listener,并以数组形式返回。

  void removeTableModelListener(TableModelListener l):从TableModelListener中移除一个listener。

  void setValueAt(Object aValue, int rowIndex, int columnIndex):设置某个cell(rowIndex,columnIndex)的值

Brup插件开发手记

建立一个监听器来获取每次请求的参数

package burp.Vuln;

import burp.*;

import java.io.PrintWriter;
import java.util.List;

public class test implements IHttpListener {
     PrintWriter stdout;
    IBurpExtenderCallbacks callbacks;

    public test(IBurpExtenderCallbacks callbacks, PrintWriter stdout) {
        this.stdout = stdout;
        this.callbacks = callbacks;
        callbacks.registerHttpListener(this);
    }

    @Override
    public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) {
        IExtensionHelpers helpers = this.callbacks.getHelpers();
        IRequestInfo iRequestInfo = helpers.analyzeRequest(messageInfo);
        List<IParameter> parameters = iRequestInfo.getParameters();
        for (IParameter parameter : parameters) {
            String name = parameter.getName();
            String value = parameter.getValue();

            this.stdout.println(name+"="+value);


        }

    }
}

漏洞探测返回结果

  public Cas_Vuln(IBurpExtenderCallbacks callbacks, PrintWriter stdout) {
        this.stdout = stdout;
        this.callbacks = callbacks;


        callbacks.registerHttpListener(this);
    }

    @Override
    public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) {
        IExtensionHelpers helpers = this.callbacks.getHelpers();
        IRequestInfo iRequestInfo = helpers.analyzeRequest(messageInfo);
        url = iRequestInfo.getUrl();
        method = iRequestInfo.getMethod();


        List<IParameter> parameters = iRequestInfo.getParameters();



        for (IParameter parameter : parameters) {
            this.parameter = parameter;
            String parameterName = parameter.getName();
            boolean is_vuln = Is_Vuln(messageInfo, this.stdout, parameterName);
            if (is_vuln){
                Vuln_Data vuln_data = new Vuln_Data();
                vuln_data.setId(BurpExtender.Vuln_Data.size());
                vuln_data.setUrl(GetURL(messageInfo));
                vuln_data.setExtensionMethod("null");
                vuln_data.setRequestMethod(method);
                vuln_data.setIssue(Vuln_Name);

                BurpExtender.Vuln_Data.add(vuln_data);

                BurpExtender.tags.fireTableDataChanged();

                this.stdout.println(GetURL(messageInfo)+" "+"IS_Vuln:"+is_vuln);
            }
            this.stdout.println(GetURL(messageInfo)+" "+"IS_Vuln:"+is_vuln);

            }



        }

内容实时显示在界面中

AbstractTableModel中代码

    @Override
    public int getRowCount() {
        return BurpExtender.Vuln_Data.size();
    }

    @Override
    public int getColumnCount() {
        return 8;
    }
    @Override
    public String getColumnName(int columnIndex) {
        switch (columnIndex) {
            case 0:
                return "#";
            case 1:
                return "extensionMethod";
            case 2:
                return "requestMethod";
            case 3:
                return "url";
            case 4:
                return "statusCode";
            case 5:
                return "issue";
            case 6:
                return "startTime";
            case 7:
                return "endTime";
        }
        return null;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        Vuln_Data vuln_data = BurpExtender.Vuln_Data.get(rowIndex);
        switch (columnIndex) {
            case 0:
                return vuln_data.getId();
            case 1:
                return vuln_data.getExtensionMethod();
            case 2:
                return vuln_data.getRequestMethod();
            case 3:
                return vuln_data.getUrl();
            case 4:
                return vuln_data.getStatusCode();
            case 5:
                return vuln_data.getIssue();
            case 6:
                return new Date();
            case 7:
                return new Date();
        }
        return null;
    }
    public class URLTable extends JTable {
        public URLTable(TableModel tableModel) {
            super(tableModel);
        }
    }
    }

调用BurpExtender.tags.fireTableDataChanged();对该界面进行刷新。

Brup插件开发手记

最后要做的就是request/response中显示对应的包。

request/response消息实现

每次漏洞扫描中将每次对应的IHttpRequestResponse requestResponse对象也给存储起来。在changeSelection方法进行设置HRequestTextEditor.setMessage

  public class URLTable extends JTable {
        public URLTable(TableModel tableModel) {
            super(tableModel);
        }
        public void changeSelection(int row, int col, boolean toggle, boolean extend) {
            Vuln_Data vuln_data = BurpExtender.Vuln_Data.get(convertRowIndexToModel(row));
            Tags.this.HRequestTextEditor.setMessage(vuln_data.requestResponse.getRequest(), true);
            Tags.this.HResponseTextEditor.setMessage(vuln_data.requestResponse.getResponse(), false);
            Tags.this.currentlyDisplayedItem= vuln_data.requestResponse;
            super.changeSelection(row, col, toggle, extend);
        }

实现IMessageEditorController接口,重写一下三个方法即可。

  @Override
    public IHttpService getHttpService() {
        return currentlyDisplayedItem.getHttpService();
    }

    @Override
    public byte[] getRequest() {
        return currentlyDisplayedItem.getRequest();
    }

    @Override
    public byte[] getResponse() {
        return currentlyDisplayedItem.getResponse();
    }

最终效果

Brup插件开发手记

Github:https://github.com/nice0e3/Brup_plugin_TScan1/

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/119861.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号