Exploiting an insecure cipher in the wild
Thu 6 July 2023Tagged: software, cryptography
I found that the topology_hiding module for OpenSIPS encodes data using an insecure cipher, such that it can be decoded without knowing the key, leaking both the plaintext and the key.
Context
OpenSIPS is an open source SIP proxy. The topology_hiding module hides information about your network topology.
From the Topology Hiding tutorial:
Topology hiding is usually utilized as an approach to enhance SIP network security. Since, in regular SIP traffic, critical IP address data is forwarded to other networks, the concern is that third parties can use that information in order to direct attacks at your internal SIP network.
If topology hiding is used as a security measure, then you might naively think it is a security vulnerability if it is not hiding anything.
However, I reported my findings to the opensips-security mailing list, and was informed that this is not a security issue. I think I just don't know enough about SIP, but that's fine. It's still fun to exploit it.
The encoding
If you're using topology_hiding without the dialog module, then the topology information is encoded into a URI parameter of the outgoing Contact header. (If you are using the dialog module, then my understanding is that OpenSIPS stores the information internally and does not pass it on even in an encoded form).
The 3 things that are encoded are the incoming packet's Record-route set, Contact header, and socket address. This is done by the build_encoded_contact_suffix() function.
The information is serialised into a string, the string is encrypted, and the result is encoded with a slight variation of base64, before being passed out as a URI parameter in the outgoing Contact header.
Here's an example serialisation (my example has no Record-route set)
\x00 \x00 Record-route length (0)
\x23 \x00 Contact length (35)
sip:foo@10.0.0.5:9000;transport=udp Contact
\x12 \x00 Socket length (18)
udp:127.0.0.1:8000 Socket
The lengths are big-endian.
And here is the code that encrypts the string:
for (i=0;i<(int)(p-suffix_plain);i++)
suffix_plain[i] ^= topo_hiding_ct_encode_pw.s[i%topo_hiding_ct_encode_pw.len];
(suffix_plain is the string we're encrypting, p points just past the end, and topo_hiding_ct_encode_pw is the password from the config file).
It XORs each byte by the corresponding byte of the password, looping the password when it is too short. This is the Vigenere cipher, but with XOR instead of addition.
Example
Here's an example of how you might configure the topology_hiding module in your OpenSIPS config:
loadmodule "topology_hiding.so";
modparam("topology_hiding", "th_contact_encode_param", "thinfo");
modparam("topology_hiding", "th_contact_encode_passwd", "m6d90nmvg645fh");
This will put the encoded data in a Contact URI parameter called "thinfo", encrypted with the password "m6d90nmvg645fh".
Here is an example of a Contact header that OpenSIPS could transmit:
Contact: <sip:jes_test_account@10.0.0.9:6000;thinfo=bTZUOUMHHUwNU0dqEg0eQjtYUw0CAwlCdARWRl0YVBcJVFtGVwYPQRQJA0UUVkIaUAIERiY1EwwdDFULB0BdWFcYBQ9TWF0G>
Cryptanalysis
Given that the incoming and outgoing Contact headers have the same user identifier (probably not true in every case, but common), and given that the incoming Contact header is part of the plaintext that was encrypted, we have quite a large fragment of known plaintext.
If we assume the absence of a Record-route set, then we know that this fragment starts at the 3rd byte, and we also know that the first 2 bytes are both zero (i.e. the ciphertext equals the key for those bytes). So it is possible to simply XOR the start of the ciphertext by the fragment that we know, and we retrieve a repeating copy of the key, which we can then use to decode the rest of the ciphertext.
Even if there is a Record-route set, all is not lost! We know that our known plaintext fragment is in there somewhere, so we just need to try all possible key lengths and locations of the fragment until we find one that does not present a contradiction. This runs in milliseconds.
I have written a proof of concept.
$ time ./th-decode.pl
key="m6d90nmvg645fh" msg="\x00\x000\x00sip:jes_test_account@10.0.0.9:6000;transport=tcp\x12\x00udp:127.0.0.1:5000"
real 0m0.015s
user 0m0.011s
sys 0m0.004s
Even if we didn't have a long known plaintext fragment, there is plenty of scope for an attack. You at least know the form of the plaintext (e.g. you know the lengths have to add up, you know what a socket address looks like, you know that a Contact header begins "sip:" and has an "@" sign in it).
Even if the key is longer than the message, we just move into the realm of key reuse with a one-time pad, which is still no bueno. If you can collect two ciphertexts and XOR them together then you are left with the XOR of the two corresponding plaintexts, at which point you don't care about the key at all.
Mitigation
Since I learnt from the opensips-security mailing list that this is not a security issue, no mitigation is necessary.
That said, if you were inclined to mitigate the problem, the best solution is probably to switch to using the dialog module so that your topology information is kept inside OpenSIPS.
Also, while I have your attention, OpenSIPS does not verify hostnames in SSL certificates. I wrote a fix but it's a bit clunky, and nobody who knows better seems to care to fix it, so it's probably not going to get fixed any time soon.
If you like my blog, please consider subscribing to the RSS feed or the mailing list: