实战小程序公众号解密burp插件

渗透技巧 2个月前 admin
146 0 0

一、    需求
 
在渗透测试web/app/小程序/公众号的时候,通过各种途径解决了burp抓包的问题。还经常碰到Request/Response的body加密的情况。这使得我们很难在burp中修改和分析它的流量。

实战小程序公众号解密burp插件

但众所周知,加密过程是在客户端完成的,防君子不防小人。因此前端加密仅仅只是增加了改包的时间成本,只要时间够经验足,总能较快的分析出来。
整个过程的难度,基本取决于如何获取和读懂前端代码。

web,前端就是js,一目了然。最多只会打包和开源混淆,较少商业混淆。
公众号,前端也是js,只是微信浏览器没有F12。
小程序,前端是打包在.wxapkg中的js,不一定能完整还原出来。
app,前端是apk,保护一般比较严格,可能会加壳和商业混淆。

二、    动态调试

对于web,我们天然有着动态调试的优势,因此可以很轻松靠下断点获取加密前的数据。

实战小程序公众号解密burp插件

而公众号和小程序就没那么容易了,可以试用旧版和一些工具配合达到想要的效果。

公众号(微信内置浏览器)F12
https://www.cnblogs.com/conne/p/15884968.html
小程序+微信内置浏览器F12
https://github.com/JaveleyQAQ/WeChatOpenDevTools-Python
小程序解包
https://blog.csdn.net/Linlietao0587/article/details/127255806
app,就更难了,但可以靠一些工具直接hook密钥
https://github.com/monkeylord/XServer


三、    常碰到的加密算法

hash非对称加密。md5,sha1,sha256,通常用来验签或者传输密码。

AES,单密钥对称加密。常见有CBC模式(带iv)和ECB模式,通常使用pkcs7padding填充和128位密钥。
DES,单密钥对称加密。64位密钥(实际用到了56位),已经基本淘汰。
3DES,单密钥对称加密。DES向AES过渡的产物,和AES类似。
RC4,单密钥对称加密。密钥长度可变。
RSA,公私钥非对称加密。常用1024和2048位算法,客户端保存公钥或者N(模数)和E(公钥指数,一般为65537)。然后加密数据给服务端,服务端用私钥解密。公私钥机制会导致我们无法解出加密后的数据,除非动态调试或者篡改公钥。
SM2,公私钥非对称加密。和RSA类似。
SM4,单密钥对称加密。和AES类似。

jdk的javax.crypto.Cipher自带了很多算法,但要实现更多的比如国密系列,可以用hutool。

四、    静态分析

如果动态调试有困难,能否依靠静态分析来实现呢。当然可以,一般来说,只要不是js混淆的太厉害,你总能看懂加密后的body是从哪儿来的。
以前面的图为例,json发送的是dataPackage.business和dataPackage.key,定位一下。

实战小程序公众号解密burp插件

可以看出来,business由des加密且hex化,key为RSA加密且hex化。

deskey=s.a.getters["handshake/getHandshakeInfo"].datakey


rsa用的n/e,为a.modulus/a.exponent
那么继续在js中找。

实战小程序公众号解密burp插件


datakey是随机生成的24字节密钥
n/e是s.MODULUS/ s.PUBLICEXPONENT
然后就可以追踪到原来是首次访问服务器,服务器传过来的模数。

实战小程序公众号解密burp插件

这下整个body的加密流程就明白了。

1,服务器下发n/e。
2,客户端生成随机datakey。
3,n/e加密datakey,生成dataPackage.key。
4,datakey加密body,生成dataPackage.business。
5,客户端发送dataPackage给服务端。
6,服务端用私钥解密dataPackage.key得到datakey。
7,服务端用datakey解密dataPackage.business得到body。

这就是整个Request body的加解密流程,如果Response body或者header中也有加密,也是一样的道理。

五、    解决办法

可以看到,我们想要解密的核心在于获取datakey。datakey是js首次运行一次后在内存中固定下来,所以如果能够动态调试,直接在随机生成24字节密钥的地方下断点,就能获得datakey。然后就能解密body,进而修改body。

当然,datakey如果是每次发送body都随机生成一次,每次都要断点就过于麻烦。此时可以直接修改
dataKey: h.a.CBPlugin.encrypt.random.getRandomKey(24)
暴力修改成
dataKey: ‘111111111111111111111111’
甚至都不需要懂代码,而是直接在burp中完成。不过要记得要删除缓存,重新加载js。。

实战小程序公众号解密burp插件

但是如果是在app或者小程序上,我们既没法动态调试(小程序还是可以的),前端又是写死的,没法篡改(实际上还可以用别的办法hook)。

纯静态还有别的办法吗?当然有,还记得第一步服务端传过来的n/e吗?我们当然可以将其篡改成我们自己的n/e,然后客户端会使用我们自己的公钥加密datakey,再用datakey加密body。接着我们用自己的私钥解密dataPackage.key得到datakey,一切就水到渠成了。

六、    burp插件

burp插件的sdk在这儿导出。

实战小程序公众号解密burp插件

新版sdk是这样的,比较复杂,网上的教程都是旧版的,burp做了兼容,推荐大家还是用旧版的。

实战小程序公众号解密burp插件

旧版是这样的,入口为burp.BurpExtender,实现burp.IBurpExtender接口。新版burp可以写成别的类,但旧版burp入口只能为burp.BurpExtender。打包时随便弄个main方法打包即可。

实战小程序公众号解密burp插件

因为我们只需要劫持流量,不用写GUI,因此只需要实现注册功能和HttpListener就行了。

    @Override    public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) {        this.callbacks = callbacks;        this.helpers = callbacks.getHelpers();        this.writer = new PrintWriter(callbacks.getStdout(), true);
callbacks.registerHttpListener(this); callbacks.printOutput("[+]插件加载成功"); String currentPath = System.getProperty("user.dir"); callbacks.printOutput("[+]user.dir: "+currentPath); }

将request/response/url/header/body等取出来。

    @Override    public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) {                byte[] request = messageInfo.getRequest();        byte[] response = messageInfo.getResponse();                IRequestInfo analyzeRequestInfo = this.helpers.analyzeRequest(request);        IResponseInfo analyzeResponseInfo = this.helpers.analyzeResponse(response);                this.url = analyzeRequestInfo.getUrl();                List requestHeaders = analyzeRequestInfo.getHeaders();        List parameters = analyzeRequestInfo.getParameters();        int requestBodyOffset = analyzeRequestInfo.getBodyOffset();        String requestBody = helpers.bytesToString(request).substring(requestBodyOffset).trim();                List responseHeaders = analyzeResponseInfo.getHeaders();        int responseBodyOffset = analyzeResponseInfo.getBodyOffset();        String responseBody = helpers.bytesToString(response).substring(responseBodyOffset).trim();                }

parameters只适合简单的传参,复杂一点的最好自己截取用json处理。

HttpListener实际上进行了两次监听,一次是Request发出去之前,此时没有Response,一次是收到Response之前。也就是说整个流程是这样的。

1,浏览器发送:/index.php?id=12,Burp Proxy Request: /index.php?id=1【可劫持】3,http流量 Request: /index.php?id=24,http流量 Response: admin【可劫持】5,Burp Proxy Response: system6,浏览器收到:system

其中劫持后的http流量 可以在Logger中看到。

实战小程序公众号解密burp插件

所以在处理的时候要用messageIsRequest来区分Request/Response。比如这个劫持Response的代码。

        if (url.toString().contains("140.x.x.x")) {                        if (!messageIsRequest) {                                IResponseInfo analyzeResponseInfo   = helpers.analyzeResponse(messageInfo.getResponse());                List<String> headers = analyzeResponseInfo.getHeaders();                int bodyOffset = analyzeResponseInfo.getBodyOffset();                String body = helpers.bytesToString(new_response).substring(bodyOffset).trim();                writer.println(body);                                body = "admin";                new_response = helpers.buildHttpMessage(headers, body.getBytes());                messageInfo.setResponse(new_response);                                            }

那么整个burp解密插件的方案就出来了。
1,服务端下发真n/e。
2,Burp插件劫持,setResponse,修改成假n/e,并将真n/e保存。
3,客户端随机生成datakey,datakey加密body得到business,假n/e加密datakey得到key。
4,客户端发送business和key到Burp。
5,Burp插件劫持,setRequest,用私钥解密key得到datakey,用datakey解密business得到body。
6,Burp将body,key,business重新打包个json输出在日志中。
7,Burp用真n/e加密datakey,datakey加密body,发送给服务端。
8,Repeater改包时手动从日志中复制json。
9,Burp插件劫持,setRequest,检测到多了个body键,用私钥解密key得到datakey。
10,Burp用真n/e加密datakey, datakey加密body,发送给服务端。

至此,除了手动从日志中复制json的那一步,基本实现了全自动化。而之所以那一步不能自动化,主要还是因为浏览器到Burp Request这一步从Burp插件层无法劫持。

全程我们都使用的是客户端生成的随机datakey,但其实可以直接使用固定的24个1,服务端也不会拒绝。
但有的服务端会校验随机datakey是不是仅使用了一次,防止包重放。
还有的服务端,在header中增加经过加密的时间戳,来防止包重放。
以及Response body进行了加密。


这些都可以通过同样的方案解决。

七、    nodejs

有的时候还要对面一个问题,那就是客户端使用的js加密是自己实现的魔改算法,表面上也叫RSA/AES/SM2/SM4,实际上你写出来的java版和js版就不一样。又或者是个古早冷门的加密算法,将其移植到java短时间不现实。

这个时候就需要java调js,直接跑js代码去实现。但不管是java自带的Nashorn,还是Rhino,甚至java调python的一些js实现,去运行那些几百kb的js sdk,都是比较慢的。什么最快呢?经过我的测试,直接起命令行调nodejs最快。

提前在本地安装好nodejs,然后在注册插件的过程中在temp目录写入所需的js脚本。

        boolean isWin = java.lang.System.getProperty("os.name").toLowerCase().contains("win");        if (isWin) {            SM.isWin = true;            SM.tempdir = "C:\windows\temp\";            callbacks.printOutput("[+]windows 设置C:\windows\temp\");        } else {            SM.isWin = false;            SM.tempdir = "/tmp/";            callbacks.printOutput("[+]linux 设置/tmp/");        }
if (SM.isNode()) { callbacks.printOutput("[+]存在node"); } else { callbacks.printOutput("[-]不存在node, 请安装node, 如果已安装, 可能存在沙箱"); }
try { SM.writeJS(); callbacks.printOutput("[+]temp目录写入js成功"); } catch (Exception e) { callbacks.printOutput("[-]temp目录写入js失败"); e.printStackTrace(); }

最后加解密的时候调一下就好。

    public static String node_exec(String cmd) {        cmd = "node " + cmd;        return exec(cmd);    }            public static String exec(String cmd) {        String[] cmds = new String[]{"cmd.exe", "/c", cmd} ;        if (!isWin) {            cmds = new String[]{"/bin/sh", "-c", cmd};        }                InputStream in = null;        try {            in = Runtime.getRuntime().exec(cmds).getInputStream();        } catch (IOException e) {            return "runtime error";        }        Scanner s = new Scanner(in).useDelimiter("\a");        String output = s.hasNext() ? s.next() : "";        output = output.replace("r", "").replace("n", "");        return output;    }

此时又衍生了新的问题,既然要用node xxxx.js去加解密数据,必然涉及传参的问题。如果将data传到cmd中执行

node xxxx.js data

data如果超过了cmd的最大限度8191,就会直接报错。因此data最好写死在xxxx.js中,那么可以实现每运行一次,就生成一个缓存xxxx_temp.js。

以及安全问题,由于我们使用了客户端/服务端的数据,拼接在cmd命令和xxxx_temp.js中,就有着被反制的风险。不过这个很好解决,因为我们传输的数据要么是base64,要么是hex,字符是固定的,只要入参时检测一下是否只有那些字符就行了。

原文始发于微信公众号(珂技知识分享):实战小程序公众号解密burp插件

版权声明:admin 发表于 2024年7月16日 下午6:12。
转载请注明:实战小程序公众号解密burp插件 | CTF导航

相关文章