Research: Kill the Router with one request
Introduction
I discoverd a bug while playing around with the ZXHN H168N V3.5
home router device which result in killing the router and make it go down with one request. So, If you want to get the device alive again, You have to restart it or wait for hours. Let’s go and explain it.
Spotting the Bug
I was doing the test as a blackbox without looking into hte firmware (We will look at the firmware later
). First go to your device dashboard and login. Then, go to Local network
tab. After that move to WLAN
. Finally for sure don’t forget your Burp Suite
tool, Since we doing the test on the web Application
interface level. So, we be able to intercept the requests and play around with it. As the following step showed in the bellow picture: open any of the WLAN
SSID and click on save.
But, before this open burp suite to intercept the request.
Now, i will send the request to the repeater to explain the vulnerability, by press right click and choose send to repeater :
Let’s go to the repeater tab and play with the request, You will see the request then click go:
So, Let’s explain what actual happened and what the request doing and what is the response for it. The request is about modifying the WLAN
settings and You can see in the response tab, In the body you can see the XML
response from server and has many objects, as example the <INSTIDENTITY>
you will see that this object has a value “DEV.WIFI.AP1.PSK1”, it looks like a value refering to WIFI Access point 1
, Casue as you saw before we had 2 Access points in the router.
Now, we have XML
response in this web application, We will try XXE
(iNJECTING SOME xml
body to check for it). I even take the response and send it back as a request.
When, I did that it’s actually back to me with more information about the Parameters
and each value
for each Paramenter
as you can see in the below screenshot.
I tried other payloads related to XXE
,. But, non of payloads worked. So, This bug is limited. Now, this bug is authenticated, When we are not authenticated it will not show any information. But, it will parse our request without any problems.
Under The Hood
Now, We will take a static look at the backend code to understand more. So, what we need now is the Firmware
for the device and we have many ways to do it:
- You can search for the firmware on the official website for the vendor.
- Download it from any other source (
after someone already dump it from the device and published it
). - Dump the firmware through URAT, You could read a detailed blog from Here.
- Finally dumping the firmware using
CH341A
Mini programmer USB, You could read a detailed blog from Here.
In my case, I found that someone dump it before, Therefor, i download it. Now, Let’s extract the firmware using Binwalk
tool and discover the firmware files. Specially the vulnerable end-point file.
- Extracting the firmware:
binwalk -e firmware
. The-e
is for extracting.
In the above screenshot it’s done successfully and we extracted the firmware as we can see in the following picature, we moved to the firmware extracted directory:
Now, if we go to the /squashfs-root
directory, We can see the directory to all the Linux
system directories which the firmware router system
built-on. Under the /home
directory we will find the /httpd
directory which home directory for the web service
user that hosting the web-interface for the router. (You can see on the following picature
)
Now, backing to the URL
of the end-point we were testing, /common_page/Localnet_WlanBasicAd_WLANSSIDConf_EncryOption_lua.lua
. It’s clearly the interface written in Lua
programming language. Let’s move on to our vulnerable page under /common_page
directory. If we list the files in the directory and grep the first words from the page name we can see the following:
We can see that the page full name is not here (It may be a one old step realse from the actual one we do have
). So, I will pick up the Localnet_WlanBasicAd_WLANSSIDConf_lua.lua
which is the closer one to our page and if we checked the file using file
command, We will be able to see that it’s a Lua, ByteCode, Version 5.1
:
If we tried to read the file using cat
command, the output will be non-understandable representation:
Lua ByteCode
As we saw before the file is Lua ByteCode. After a couple hours of search, I was able to find this website to decompile Lua ByteCode
. Therefor, we will be able to analysis and understand what happen in the background.
We can see that we have a lot of code here and if we scroll down you gonna see some instructions looks like Assembly
, After searching for the Lua Bytecode
. I found the follwoing reference with the instructions and description for each instruction:
So, Now we can start to read and understand. If we take a look at the first few lines it’s giving us information about the language version, etc. But, the starting part where we can see the main function and it’s information:
************ Main Function
Source: @/home/xialei/Builds/H168NV31_Develop_BaseCSP3005P01/chip_mtk7510/product/H168NV31_BELT/scripts/../basefilesystem/luapages/html//common_page/Localnet_WlanBasicAd_WLANSSIDConf_lua.lua
Parameters: 0
Is vararg: yes
Local variable 'arg': absent
VM registers needed: 36
Source
: The source code file directory (Which also seems from the firmware team developers under /home
).Parameters
: Number of functions parameters.Is Vrarg
: Meaning it can accept a variable number of arguments.Local variable 'arg': absent
:is a special variable in Lua that refers to a list of arguments passed to a function.VM registers needed: 36
: Requirted VM Registers to execute.]
Under the main functions we can find 2 things Constants
which are variables and can not be changed, Also Locals
which are defined local variables.
- Constants
Constants: 61 +0658:Size=7: 72 65 71 75 69 72 65 Const#1 string "require" +0665:Size=E: 6C 69 62 2F 63 6F 6D 6D 6F... Const#2 string "lib/common_lua" +0679:Size=0: Const#3 string "" +067F:Size=A: 49 46 5F 45 52 52 4F 52 49 44 Const#4 string "IF_ERRORID" +068B:Size=8: 00 00 00 00 00 00 00 00 Const#5 float 0.0 +0694:Size=8: 3F F0 00 00 00 00 00 00 Const#6 float 1.0 +06A1:Size=6: 63 67 69 6C 75 61 Const#7 string "cgilua" +06AD:Size=B: 72 65 6D 6F 74 65 5F 61 64... Const#8 string "remote_addr" +06BE:Size=B: 73 65 73 73 69 6F 6E 5F 67... Const#9 string "session_get" +06CF:Size=5: 52 69 67 68 74 Const#10 string "Right" +06DA:Size=11: 4F 42 4A 5F 57 4C 41 4E 4... Const#11 string "OBJ_WLANCONFIG_ID" +06F1:Size=1: 33 Const#12 string "3" +06F8:Size=5: 45 53 53 49 44 Const#13 string "ESSID" +0703:Size=A: 42 65 61 63 6F 6E 54 79 70 65 Const#14 string "BeaconType" +0713:Size=B: 57 45 50 41 75 74 68 4D 6F... Const#15 string "WEPAuthMode" +0724:Size=B: 57 45 50 4B 65 79 49 6E 64... Const#16 string "WEPKeyIndex" +0735:Size=B: 57 50 41 41 75 74 68 4D 6F... Const#17 string "WPAAuthMode" +0746:Size=E: 57 50 41 45 6E 63 72 79 70... Const#18 string "WPAEncryptType" +075A:Size=B: 31 31 69 41 75 74 68 4D 6F... Const#19 string "11iAuthMode" +076B:Size=E: 31 31 69 45 6E 63 72 79 70... Const#20 string "11iEncryptType" +077F:Size=6: 45 6E 61 62 6C 65 Const#21 string "Enable" +078B:Size=D: 57 50 41 47 72 6F 75 70 52... Const#22 string "WPAGroupRekey" +079E:Size=F: 45 53 53 49 44 48 69 64 65... Const#23 string "ESSIDHideEnable" +07B3:Size=12: 56 61 70 49 73 6F 6C 61 7... Const#24 string "VapIsolationEnable" +07CB:Size=8: 50 72 69 6F 72 69 74 79 Const#25 string "Priority" +07D9:Size=A: 4D 61 78 55 73 65 72 4E 75 6D Const#26 string "MaxUserNum" +07E9:Size=11: 4F 42 4A 5F 57 4C 41 4E 5... Const#27 string "OBJ_WLANWEPKEY_ID" +0800:Size=6: 57 45 50 4B 65 79 Const#28 string "WEPKey" +080C:Size=E: 4F 42 4A 5F 57 4C 41 4E 50... Const#29 string "OBJ_WLANPSK_ID" +0820:Size=D: 4B 65 79 50 61 73 73 70 68... Const#30 string "KeyPassphrase" +0833:Size=19: 51 75 65 72 79 57 43 61 7... Const#31 string "QueryWCardAndSSIDIdentity" +0852:Size=4: 50 4F 53 54 Const#32 string "POST" +085C:Size=9: 49 46 5F 41 43 54 49 4F 4E Const#33 string "IF_ACTION" +086B:Size=9: 5F 57 45 50 43 4F 4E 49 47 Const#34 string "_WEPCONIG" +087A:Size=9: 5F 50 53 4B 43 4F 4E 49 47 Const#35 string "_PSKCONIG" +0889:Size=7: 5F 49 6E 73 74 49 44 Const#36 string "_InstID" +0896:Size=C: 5F 49 6E 73 74 49 44 5F 57... Const#37 string "_InstID_WEP0" +08A8:Size=C: 5F 49 6E 73 74 49 44 5F 57... Const#38 string "_InstID_WEP1" +08BA:Size=C: 5F 49 6E 73 74 49 44 5F 57... Const#39 string "_InstID_WEP2" +08CC:Size=C: 5F 49 6E 73 74 49 44 5F 57... Const#40 string "_InstID_WEP3" +08DE:Size=B: 5F 49 6E 73 74 49 44 5F 50... Const#41 string "_InstID_PSK" +08EF:Size=E: 74 72 61 6E 73 54 6F 50 6F... Const#42 string "transToPostTab" +0903:Size=10: 74 72 61 6E 73 54 6F 46 6... Const#43 string "transToFilterTab" +0919:Size=5: 41 70 70 6C 79 Const#44 string "Apply" +0924:Size=1: 59 Const#45 string "Y" +0927:Size=0: Const#46 nil nil +092C:Size=13: 61 70 70 6C 79 4F 72 4E 6... Const#47 string "applyOrNewOrDelInst" +0945:Size=8: 57 45 50 4B 65 79 30 30 Const#48 string "WEPKey00" +0953:Size=8: 57 45 50 4B 65 79 30 31 Const#49 string "WEPKey01" +0961:Size=8: 57 45 50 4B 65 79 30 32 Const#50 string "WEPKey02" +096F:Size=8: 57 45 50 4B 65 79 30 33 Const#51 string "WEPKey03" +097D:Size=6: 43 61 6E 63 65 6C Const#52 string "Cancel" +0989:Size=12: 67 65 74 53 70 65 63 69 6... Const#53 string "getSpecificInstXML" +09A1:Size=5: 74 61 62 6C 65 Const#54 string "table" +09AC:Size=6: 69 6E 73 65 72 74 Const#55 string "insert" +09B8:Size=F: 53 53 49 44 46 69 6C 74 65... Const#56 string "SSIDFilterByNIC" +09CD:Size=D: 67 65 74 41 6C 6C 49 6E 73... Const#57 string "getAllInstXML" +09E0:Size=3: 49 47 44 Const#58 string "IGD" +09E9:Size=6: 63 6F 6E 63 61 74 Const#59 string "concat" +09F5:Size=E: 6F 75 74 70 75 74 45 72 72... Const#60 string "outputErrorXML" +0A09:Size=9: 6F 75 74 70 75 74 58 4D 4C Const#61 string "outputXML"
As you can see we have 3 fields:
Const#<number>
: the Constant variable number.<Type>
: The variable data type (e.x:Float,stringm etc
).<Name>
: The variable name.
Now, to the Locals
variables:
- Locals
Locals: 29 Local#1 R0 def:<4> scope:<5..349> name: InstXML Local#2 R1 def:<5> scope:<6..349> name: ErrorXML Local#3 R2 def:<6> scope:<7..349> name: OutXML Local#4 R3 def:<7> scope:<9..349> name: tError Local#5 R4 def:<9> scope:<10..349> name: need2Get Local#6 R5 def:<10> scope:<11..349> name: xmlStr Local#7 R6 def:<12> scope:<13..349> name: sess_id Local#8 R7 def:<16> scope:<17..349> name: uRight Local#9 R8 def:<17> scope:<18..349> name: PARA Local#10 R9 def:<18> scope:<19..349> name: FP_OBJNAME Local#11 R10 def:<50> scope:<51..349> name: WEP_OBJNAME Local#12 R11 def:<51> scope:<54..349> name: WEP_PARA Local#13 R12 def:<54> scope:<55..349> name: PSK_OBJNAME Local#14 R13 def:<55> scope:<58..349> name: PSK_PARA Local#15 R14 def:<58> scope:<59..349> name: WLANNICNum Local#16 R15 def:<59> scope:<60..349> name: WNICIdentity Local#17 R16 def:<60> scope:<61..349> name: WLANBasicIDTable Local#18 R17 def:<68> scope:<69..349> name: FP_ACTION Local#19 R18 def:<71> scope:<72..349> name: _WEPCONIG Local#20 R19 def:<74> scope:<75..349> name: _PSKCONIG Local#21 R20 def:<77> scope:<78..349> name: FP_IDENTITY Local#22 R21 def:<80> scope:<81..349> name: WEP_IDENTITY0 Local#23 R22 def:<83> scope:<84..349> name: WEP_IDENTITY1 Local#24 R23 def:<86> scope:<87..349> name: WEP_IDENTITY2 Local#25 R24 def:<89> scope:<90..349> name: WEP_IDENTITY3 Local#26 R25 def:<92> scope:<93..349> name: PSK_IDENTITY Local#27 R26 def:<93> scope:<94..349> name: xmlTable Local#28 R27 def:<96> scope:<97..349> name: WEP_PARA_POST Local#29 R28 def:<99> scope:<100..349> name: WEP_PARA_FILTER
Here we can see that there are 5 fields:
Local#<number>
: The Local variable number.R<number>
: The register that the variable assigned to.def:<number>
: Line that the variable defined on, In the code.scope:<ramge>
: indicates that the variable is in scope (can be accessed and modified) from a range of lines in the code.<Name>
: Finally the variable name.
If we take a look at the Constants
& Local
variable names including the same parameters in our request.:
IF_ACTION | Const#33 string "IF_ACTION"
Enable | Const#21 string "Enable"
_InstID | Const#36 string "_InstID"
_PSKCONIG | Local#20 R19 def:<74> scope:<75..349> name: _PSKCONIG
BeaconType | Const#14 string "BeaconType"
WPAAuthMode | Const#17 string "WPAAuthMode"
11iAuthMode | Const#19 string "11iAuthMode"
WPAEncryptType | Const#18 string "WPAEncryptType"
11iEncryptType | Const#20 string "11iEncryptType"
_InstID_PSK | Const#41 string "_InstID_PSK"
ESSID | Const#13 string "ESSID"
ESSIDHideEnable | Const#23 string "ESSIDHideEnable"
KeyPassphrase | Const#30 string "KeyPassphrase"
and many more parameters......
Note
: you could find differents in files becuase of the firmwareversion & realse
Also there are some cariables has the XML
name in as the following which explains the XML
format type:
Local#1 R0 def:<4> scope:<5..349> name: InstXML
Local#2 R1 def:<5> scope:<6..349> name: ErrorXML
Local#3 R2 def:<6> scope:<7..349> name: OutXML
Local#6 R5 def:<10> scope:<11..349> name: xmlStr
Local#27 R26 def:<93> scope:<94..349> name: xmlTable
Now, backing to the request again as the _InstID
is a Const
variable. It has a value in the request as we can see, But when we try to change the value. It’s giving us error:
-
Before changing the value
-
After Changing the value, It’s giving an error.
-
Here Trying to flow it with huge value to get any crash, andIt didn’t work.
Final Thoughts
As we didn’t do any dynamic analysis. Cause it will require us to emulate the firmware and debug it,To make everything clear. But, there are a logical posability that the bug in the following line:
GETTABLE 20 20 -36 <77> R20@FP_IDENTITY = R20["_InstID"]
Let’s explain the line first the GETTABLE
operation is used to retrieve a value from a table which in this case is the XMLtable
and it will retrive the value stored in the key _InstID
and store it into FP_IDENTITY
that holded at R20
. In this case, As we pass a huge value it will crash the Lua VM which running the app.
Conclusion
We can not fully admit that the Final Thoughts
we discuss is the right or final one as we didn’t dig deep into and perform dynamic analysis. But, we would be analyzing it dynamically to fully verify the Final Thoughts
. Also, it could be the same way but in another place or line of code.