首页
社区
课程
招聘
[原创]遇见非常规双向证书绕过
2023-2-11 13:53 23579

[原创]遇见非常规双向证书绕过

2023-2-11 13:53
23579

难点

这个app的本地证书做了处理,对证书名加密以及去掉证书后缀,导致用常规全局搜索.p12等文件失败。

现象

该app可以抓到包,但是存在以下问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 400 Bad Request
Server: openresty/1.17.8.1
Date: Wed, 14 Oct 2020 10:00:30 GMT
Content-Type: text/html; charset=utf8
Content-Length: 645
Connection: close
 
<html>
  <head><title>400 No required SSL certificate was sent</title></head>
  <body>
    <center><h1>400 Bad Request</h1></center>
    <center>No required SSL certificate was sent</center>
    <hr><center>openresty/1.17.8.1</center>
  </body>
</html>

一般是由于做了双向认证校验,服务器校验客户端证书导致的。

一般操作

对app进行脱壳,全局搜索".cer"、".crt"、".pfx"、"PKCS12"、"keyStore"、".p12"等关键字。在
图片描述
跳转到目标代码上

1
2
3
4
5
6
        if (!hashMap.containsKey(host2)) {
            hashMap.put(host2, host2 + ".p12");
        }
    }
    Client(hashMap);
}

找到调用上面Client的方法

1
2
3
4
private void Client(Map<String, String> map) {
        ...
        OkHttpClient.Builder createClientBuilder = createClientBuilder();
        createClientBuilder.sslSocketFactory(surea.a(App.b().getAssets().open(E.i(value)),EncryptNDK.getInstance().gainValue(value)), a.a());

从代码中可以看到

1
createClientBuilder.sslSocketFactory(surea.a(App.b().getAssets().open(E.i(value)),EncryptNDK.getInstance().gainValue(value)), a.a());

sslSocketFactory中参数open(E.i(value))打开本地的证书文件,参数EncryptNDK.getInstance().gainValue(value)为获取证书的key。
跟踪获取证书的key函数

1
2
3
4
5
6
7
8
public class EncryptNDK {
    static {
        System.loadLibrary("EncryptLib");
    }
    ...
    public native String gainValue(String str);
    ....
}

调用了so文件,而gainValue中的str即是证书的key。利用frida去hook EncryptNDK.gainValue以及E.i(value)。

最终

利用hook到的值去全局搜索即可得到被调用的证书以及证书的key。在burpsuit中的User options 》 TLS中导入相对于的证书与域名即可。

其他

在证书存在于本地的资源文件中,对证书名进行加密和去掉证书后缀。导致常规的全局搜索找不到证书的所在,若是证书通过远程进行下载,以及存在这样的情况下如何去解决。

解决

1、可以通过本地文件的更新时间去判断,然而可能需要去甄别那些是证书。若是存在上述的加密以及去掉证书后缀,则可能要花费很大功夫甚至是无法达成目的。
2、如果证书是以java.security.KeyStore来进行双向认证校验,那么通过hook该函数的load方法可获取证书文件和证书密码,即

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
#!/usr/bin/python3
import frida, sys, time
 
app_name = '包名'
i = 0
ext = ''
 
def on_message(message, data):
   global i, ext
   if (message['type'] == 'send' and 'event' in message['payload']):
       if (message['payload']['event'] == '+found'):
           i += 1
           print("\n[+] Hooked keystore" + str(i) + "...")
 
       elif (message['payload']['event'] == '+type'):
           print(" [+] Cert Type: " + ''.join(message['payload']['certType']))
           if (message['payload']['certType'] == 'PKCS12'):
               ext = '.jks'
 
       elif (message['payload']['event'] == '+pass'):
           print(" [+] Password: " + ''.join(message['payload']['password']))
 
       elif (message['payload']['event'] == '+write'):
           print(" [+] Writing to file: keystore" + str(i) + ext)
           f = open('keystore' + str(i) + ext, 'wb')
           f.write(bytes.fromhex(message['payload']['cert']))
           f.close()
   else:
       print(message)
 
jscode = """
setTimeout(function() {
  Java.perform(function () {
      var keyStoreLoadStream = Java.use('java.security.KeyStore')['load'].overload('java.io.InputStream', '[C');
      /* following function hooks to a Keystore.load(InputStream stream, char[] password) */
      keyStoreLoadStream.implementation = function(stream, charArray) {
          /* sometimes this happen, I have no idea why, tho... */
          if (stream == null) {
              /* just to avoid interfering with app's flow */
              this.load(stream, charArray);
              return;
          }
          /* just to notice the client we've hooked a KeyStore.load */
          send({event: '+found'});
          /* read the buffer stream to a variable */
          var hexString = readStreamToHex (stream);
          /* send KeyStore type to client shell */
          send({event: '+type', certType: this.getType()});
          /* send KeyStore password to client shell */
          send({event: '+pass', password: charArray});
          /* send the string representation to client shell */
          send({event: '+write', cert: hexString});
          /* call the original implementation of 'load' */
          this.load(stream, charArray);
          /* no need to return anything */
      }
  });
},0);
/* following function reads an InputStream and returns an ASCII char representation of it */
function readStreamToHex (stream) {
  var data = [];
  var byteRead = stream.read();
  while (byteRead != -1)
  {
      data.push( ('0' + (byteRead & 0xFF).toString(16)).slice(-2) );
              /* <---------------- binary to hex ---------------> */
      byteRead = stream.read();
  }
  stream.close();
  return data.join('');
}
"""
 
print("[.] Attaching to device...")
try:
   device = frida.get_remote_device()     
except:
   print("[-] Can't attach. Is the device connected?")
   sys.exit()
 
print("[.] Spawning the app...")
try:
   pid = device.spawn(app_name)
   device.resume(pid)
   time.sleep(1)
except:
   print("[-] Can't spawn the App. Is filename correct?")
   sys.exit()
 
print("[.] Attaching to process...")
try:
   process = device.attach(pid)
except:
   print("[-] Can't connect to App.")
   sys.exit()
 
print("[.] Launching js code...")
print(" (run the app until needed, close it and then kill this script)")
script = process.create_script(jscode)
script.on('message', on_message)
script.load()
try:
       sys.stdin.read()
except KeyboardInterrupt:
   print ("\nExiting now")
   exit(0)

针对于该脚本的实践中,发现应用于本次的app是需要改动的。本次的app存在spwan的检测,无法使用device.spawn去启动app,因此需要修改。去掉

1
2
3
4
5
6
7
8
print("[.] Spawning the app...")
try:
   pid = device.spawn(app_name)
   device.resume(pid)
   time.sleep(1)
except:
   print("[-] Can't spawn the App. Is filename correct?")
   sys.exit()

同时将pid改为app_name

1
2
try:
   process = device.attach(app_name)

经过实践该脚本是可以成立的,但是好像只生成有带密码的证书文件,而且证书后缀为.jks。
3、如果app本身不存在检测的话,可以利用objection,去hook java.io.File

1
android hooking watch class_method java.io.File.$

或使用spawn方式

1
objection -g xxx explore --startup-command "android hooking watch class_method java.io.File.$init --dump-args

[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞6
打赏
分享
最新回复 (4)
雪    币: 50
活跃值: (464)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
万里星河 2023-2-11 22:40
2
0
mark
雪    币: 1069
活跃值: (1469)
能力值: ( LV4,RANK:49 )
在线值:
发帖
回帖
粉丝
userapp 2023-2-19 15:36
3
0
即使证书远程下载,但是发起https下载请求就需要证书了,所以本地还是会有个证书
雪    币: 493
活跃值: (3657)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
龙飞雪 2023-3-22 17:27
4
0
全局搜索".cer"、".crt"、".pfx"、"PKCS12"、"keyStore"、".p12"等关键字
雪    币: 3525
活跃值: (3632)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
caolinkai 2023-5-19 23:38
5
0
游客
登录 | 注册 方可回帖
返回