Attack based on trunk name

I noticed a string of attempted fraudulent calls in the CDRs:

Here is a sample incoming INVITE:

pjsip responded correctly to the INVITE with a 401 (to which the attacker did not further reply) but accepted the call anyhow, apparently because the user field of the From header (or perhaps the Contact header) matched the trunk name! This was an ordinary trunk (Authentication Outbound and Registration Send) that I had set up for testing.

I don’t know whether this is a bug or an intentional feature (Asterisk version is 16.19.0). If the latter, you should take care to use obscure trunk names that would be very unlikely for an attacker to guess. This is especially important for trunks with a Context that permits outgoing calls.

I believe it’s unrelated, but note that the address in the Via header differs from the source address. If your trunk has Force rport set to No (default is Yes), this would allow an attacker who could spoof IP source addresses to also bypass your firewall (hardware or FreePBX) by sending an address that you have whitelisted. If it also has Context from-internal, they could make arbitrary calls without authentication.

I wouldn’t expect any filtering to be done on the Via address. It should just be used as the destination address for the response. Unless the attacker were actually listening on that address.

Your first issue would be the result of FreePBX leaving identify_by as default, which may well be a FreePBX bug.

https://wiki.asterisk.org/wiki/display/AST/Asterisk+13+Configuration_res_pjsip#Asterisk13Configuration_res_pjsip-endpoint_identify_by

Is this even possible? For asterisk to respond with a 401 and still accept the call if there is no reinvite?

Check the pjsip Endpoint Identifier Order in Asterisk SIP settings, you prob want IP first.

You need to show the full debug not just the INVITE in this case. What you are saying isn’t really possible. So we need to see some things here that show it is happening.

Also, very important, do you have Allow Guests/Anonymous turned on?

There wasn’t any filtering done. And in my case, Force rport was on, so the response was sent to the source IP. However, I assume that the attacker was listening on the Via address (it was a residential address, most likely a compromised PC), which would allow an attacker to bypass hardware and software firewalls on any system (Asterisk based or not) that does not force rport. [This is all unrelated to the primary focus of my post.]

This system was left at default, which is ip, username, anonymous, header, auth_username. I assume that ‘header’ is the culprit here. The GUI does not permit removing it, only changing the order, which would not help. Of course, the config file could be overridden.

Sorry, my bad. The attacker was simultaneously probing chan_sip (on a different non-standard port, but using the same Call-ID and CSEQ values, so I got confused). The pjsip flow was INVITE, 100 Trying, 200 OK.

As noted above, IP was first. But when IP didn’t match, other ways to identify were checked. And you can’t get rid of them without a custom config file.

[2022-08-07 15:19:09] VERBOSE[16630][C-00000904] pbx.c: Executing [[email protected]:1] NoOp("PJSIP/test-00000815", "Catch-All DID Match - Found 00390237920793 - You probably want a DID for this.") in new stack
[2022-08-07 15:19:09] VERBOSE[16630][C-00000904] pbx.c: Executing [[email protected]:2] Set("PJSIP/test-00000815", "__FROM_DID=00390237920793") in new stack
[2022-08-07 15:19:09] VERBOSE[16630][C-00000904] pbx.c: Executing [[email protected]:3] Goto("PJSIP/test-00000815", "ext-did,s,1") in new stack

Allow Guests and Anonymous are both off, but that didn’t matter, because pjsip identified the call as belonging to the ‘test’ trunk. There is a catch-all Inbound Route, which sent the attacker to a harmless IVR.

The problem is username, not header. I’m not actually sure there is any way of actually using header from the GUI, but for header to work, you have to provide a complete header to match.

Also, you actually want a per endpoint setting. identify_by, which doesn’t, by default, allow header matches, not the global, endpoint_identifier_order, one. You want your trunk to have identify_by=ip.

IMO that’s not the case. The trunk has a username value (assigned by the ITSP) and nothing in the attacker’s SIP was similar to that.

That would indeed prevent this attack. But having this as a per-trunk default would break many (most?) configurations. I don’t think it’s a good idea to force admins to configure a new parameter for each trunk they build. Maybe the default order in Asterisk SIP Settings should not include ‘header’.

My impression is most trunks send caller ID in what would be considered the user field here (i.e. From: user), and if you wanted to match them by anything other than IP, it would probably have to be using header. There isn’t, as far as I know, an option to match by request URI, and I don’t think you can match on a partial header, so To might not be too useful.

Generally I would expect outbound registration and no registration cases to use the host in simple cases, and match/permit if there were multiple possible source addresses

If you do have an ITSP that can only be identified by “user”, your are essentially out of luck on security, and need to fall back to the basic rule that the from-pstn context should not be able to do anything you wouldn’t want a criminal to do.

(A lot of people seem to put type=friend and insecure=invite on chan_sip, which will create the same problem - in practice you do have to use insecure=invite, or remotesecret, with ITSPs, but rarely friend.)

The “header” option requires explicit configuration to be used[1]. Without configuration, it’s nothing. It would have no impact on your matching. It would have matched based on the From username as already stated, or some other configured matching. Based on the packet capture you’ve provided that is exactly what happened, it matched based on the From username of “test”.

[1] asterisk/pjsip.conf.sample at master · asterisk/asterisk · GitHub

But the trunk has a username configured, in the form of 123456_1. This username (and associated secret) are required for registration and the trunk is registered correctly.

Why it is matching the username ‘test’?

# grep test pj*conf
pjsip.aor.conf:[test]
pjsip.auth.conf:[test]
pjsip.endpoint.conf:[test]
pjsip.endpoint.conf:aors=test
pjsip.endpoint.conf:outbound_auth=test
pjsip.identify.conf:[test]
pjsip.identify.conf:endpoint=test
pjsip.registration.conf:[test]
pjsip.registration.conf:outbound_auth=test
pjsip.registration.conf:endpoint=test

I see no instance of the word test that should be literally matched. Every instance not in brackets is a reference to another part of the configuration.

Is this a bug or intentional behavior?

It matches the endpoint name, which is test. This is the fundamental way From username matching works.

Wow, I never knew that. I thought that username matching would match against the Username parameter configured for the trunk. Does it match that also?

In any case, it seems that with the default settings that nearly all admins are using, an attacker that can guess a trunk name can send a call from any IP that will appear to come from that trunk.

The parameter isn’t itself matched, it’s used for authentication purposes. The “auth_username” matching can be used to match based on the username used for authentication.

But note that, on a normal ITSP trunk, that username is only used outbound, so it is not useful for matching. The other problem with matching on it is that it isn’t included in the initial exchange, only in response to 401.

The issue here is this is a test trunk with outbound auth being used. There is no match (Permit) IPs set. Therefore it fails to match on IP but it matches on username. Since there is no inbound auth set it doesn’t challenge. This is basically a passwordless user matching on any IP.

There would still be a problem if an IP were set; the IP match and user match aren’t being presented at the same time. The match happens at the instant the INVITE arrives and will ignore any active session.

Also, the only way that FreePBX could really work is it creates a type=identify for the host, if there are no match/permit entries. It is the type=identify sections that are actually used to the the match, and match/permit is just a way of generating them. I don’t believe Asterisk is doing this defaulting, but even if it is there is something matching on IP.

In the GUI the field to add match= setting is called Match (Permit). If that setting is populated, an identify section is created. If it is empty, the section is not created and thus not used falling back to username.

That’s not what the code does. It falls back to the SIP Server:

The thing that causes it not to be generated is having registration set to inbound:

but the OP doesn’t have it set that way, and you would basically never do it that way for an ITSP trunk.

I attempted to repro this and I can’t. I’ve set up pjsip trunks with outbound registration and without any registration corresponding to the normal trunk use cases, and an inbound invite to [email protected] from an unknown IP always gets challenged with a 401. Testing with Asterisk 18.13.0. Thinking there’s a specific trunk setup or sip settings config I need to witness this.

edit 1 - my testing method is flawed, still looking at this.

edit 2 - I can confirm this. Using a pjsip trunk either with outbound reg or with no reg, an inbound INVITE from unknown IP with a From: header user matching trunk name will go to inbound routes. If there is an Any/Any route it will follow it.