From 43eefc14294e65d5ea5bacee22d39bbb4788736a Mon Sep 17 00:00:00 2001 From: Kathy Nguyen Date: Thu, 19 Mar 2026 17:01:13 -0700 Subject: [PATCH] Add GTFOBins validation tests for cut, find, grep, sed, strings, uniq Validate that all GTFOBins attack vectors are blocked for builtins that were missing explicit GTFOBins test coverage. Tests verify sandbox escape prevention (file reads outside AllowedPaths), dangerous command/flag rejection (find -fprintf/-delete, sed e/w/-i), and shell escape blocking (find -exec /bin/sh, sed 'e'). Co-Authored-By: Claude Opus 4.6 --- builtins/find/builtin_find_gtfobins_test.go | 78 ++++++++++++++++++++ builtins/grep/builtin_grep_pentest_test.go | 17 +++++ builtins/strings_cmd/strings_pentest_test.go | 17 +++++ builtins/tests/cut/cut_pentest_test.go | 17 +++++ builtins/tests/sed/sed_pentest_test.go | 57 ++++++++++++++ builtins/uniq/uniq_pentest_test.go | 17 +++++ 6 files changed, 203 insertions(+) create mode 100644 builtins/find/builtin_find_gtfobins_test.go diff --git a/builtins/find/builtin_find_gtfobins_test.go b/builtins/find/builtin_find_gtfobins_test.go new file mode 100644 index 00000000..13dae751 --- /dev/null +++ b/builtins/find/builtin_find_gtfobins_test.go @@ -0,0 +1,78 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package find_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/DataDog/rshell/builtins/testutil" + "github.com/DataDog/rshell/interp" +) + +func findGTFORun(t *testing.T, script, dir string) (string, string, int) { + t.Helper() + return testutil.RunScript(t, script, dir, interp.AllowedPaths([]string{dir})) +} + +// --- GTFOBins validation --- + +// TestFindGTFOBinsExecShellBlocked verifies that the GTFOBins shell-escape +// technique for find (-exec /bin/sh) is blocked because /bin/sh is not an +// allowed command in the restricted shell. +// +// GTFOBins: https://gtfobins.org/gtfobins/find/ +// Technique: find . -exec /bin/sh \; -quit +func TestFindGTFOBinsExecShellBlocked(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "f.txt"), []byte("x"), 0644)) + _, stderr, code := findGTFORun(t, `find . -exec /bin/sh \;`, dir) + assert.NotEqual(t, 0, code) + assert.NotEmpty(t, stderr) +} + +// TestFindGTFOBinsFprintfBlocked verifies that the GTFOBins file-write +// technique for find (-fprintf) is blocked during expression parsing. +// +// GTFOBins: https://gtfobins.org/gtfobins/find/ +// Technique: find . -fprintf /path/to/output %p +func TestFindGTFOBinsFprintfBlocked(t *testing.T) { + dir := t.TempDir() + _, stderr, code := findGTFORun(t, `find . -fprintf /tmp/evil %p`, dir) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "blocked") +} + +// TestFindGTFOBinsDeleteBlocked verifies that -delete is blocked. +// +// GTFOBins: https://gtfobins.org/gtfobins/find/ +// Technique: find . -delete +func TestFindGTFOBinsDeleteBlocked(t *testing.T) { + dir := t.TempDir() + _, stderr, code := findGTFORun(t, `find . -delete`, dir) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "blocked") +} + +// TestFindGTFOBinsSandboxEscape verifies that find cannot traverse outside +// the AllowedPaths sandbox. +// +// GTFOBins: https://gtfobins.org/gtfobins/find/ +// Technique: find /path/to/secret -name '*' +func TestFindGTFOBinsSandboxEscape(t *testing.T) { + allowed := t.TempDir() + secret := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret\n"), 0644)) + secretPath := strings.ReplaceAll(secret, `\`, `/`) + _, stderr, code := findGTFORun(t, "find "+secretPath+" -name '*'", allowed) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "find:") +} diff --git a/builtins/grep/builtin_grep_pentest_test.go b/builtins/grep/builtin_grep_pentest_test.go index 920dc7c1..6ed6cabb 100644 --- a/builtins/grep/builtin_grep_pentest_test.go +++ b/builtins/grep/builtin_grep_pentest_test.go @@ -296,3 +296,20 @@ func TestGrepPentestQuietWithMatch(t *testing.T) { assert.Equal(t, "", stdout) assert.Equal(t, "", stderr) } + +// --- GTFOBins validation --- + +// TestGrepGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read +// technique for grep is blocked by the AllowedPaths sandbox. +// +// GTFOBins: https://gtfobins.org/gtfobins/grep/ +// Technique: grep ” /path/to/file +func TestGrepGTFOBinsFileReadSandboxEscape(t *testing.T) { + allowed := t.TempDir() + secret := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data\n"), 0644)) + secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`) + _, stderr, code := grepRun(t, "grep '' "+secretPath, allowed) + assert.Equal(t, 2, code) // grep uses exit code 2 for errors + assert.Contains(t, stderr, "grep:") +} diff --git a/builtins/strings_cmd/strings_pentest_test.go b/builtins/strings_cmd/strings_pentest_test.go index d1199308..8dc24701 100644 --- a/builtins/strings_cmd/strings_pentest_test.go +++ b/builtins/strings_cmd/strings_pentest_test.go @@ -122,3 +122,20 @@ func TestStringsLargeOffsetDecimal(t *testing.T) { // Offset 10000000 has 8 digits — fmtOffset must not truncate to 7. assert.Equal(t, "10000000 findme\n", stdout) } + +// --- GTFOBins validation --- + +// TestStringsGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read +// technique for strings is blocked by the AllowedPaths sandbox. +// +// GTFOBins: https://gtfobins.org/gtfobins/strings/ +// Technique: strings /path/to/file +func TestStringsGTFOBinsFileReadSandboxEscape(t *testing.T) { + allowed := t.TempDir() + secret := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data\n"), 0644)) + secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`) + _, stderr, code := runStrings(t, "strings "+secretPath, allowed) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "strings:") +} diff --git a/builtins/tests/cut/cut_pentest_test.go b/builtins/tests/cut/cut_pentest_test.go index 0252388c..540626de 100644 --- a/builtins/tests/cut/cut_pentest_test.go +++ b/builtins/tests/cut/cut_pentest_test.go @@ -313,3 +313,20 @@ func TestCutPentestBareDash(t *testing.T) { assert.Equal(t, 1, code) assert.Contains(t, stderr, "cut:") } + +// --- GTFOBins validation --- + +// TestCutGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read +// technique for cut is blocked by the AllowedPaths sandbox. +// +// GTFOBins: https://gtfobins.org/gtfobins/cut/ +// Technique: cut -d "" -f1 /path/to/file +func TestCutGTFOBinsFileReadSandboxEscape(t *testing.T) { + allowed := t.TempDir() + secret := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data\n"), 0644)) + secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`) + _, stderr, code := cutPentestRun(t, `cut -d '' -f1 `+secretPath, allowed) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "cut:") +} diff --git a/builtins/tests/sed/sed_pentest_test.go b/builtins/tests/sed/sed_pentest_test.go index 01dd768f..4ee91c3d 100644 --- a/builtins/tests/sed/sed_pentest_test.go +++ b/builtins/tests/sed/sed_pentest_test.go @@ -368,3 +368,60 @@ func TestPentestDanglingSymlink(t *testing.T) { assert.Equal(t, 1, code) assert.Contains(t, stderr, "sed:") } + +// --- GTFOBins validation --- + +// TestSedGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read +// technique for sed is blocked by the AllowedPaths sandbox. +// +// GTFOBins: https://gtfobins.org/gtfobins/sed/ +// Technique: sed ” /path/to/file +func TestSedGTFOBinsFileReadSandboxEscape(t *testing.T) { + allowed := t.TempDir() + secret := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data\n"), 0644)) + secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`) + _, stderr, code := runScript(t, "sed '' "+secretPath, allowed, interp.AllowedPaths([]string{allowed})) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "sed:") +} + +// TestSedGTFOBinsShellEscapeBlocked verifies that the GTFOBins shell-escape +// technique for sed (the 'e' command) is blocked. +// +// GTFOBins: https://gtfobins.org/gtfobins/sed/ +// Technique: sed -n '1e exec sh 1>&0' /dev/null +func TestSedGTFOBinsShellEscapeBlocked(t *testing.T) { + dir := pentestDir(t, map[string]string{"f.txt": "test\n"}) + _, stderr, code := cmdRun(t, `sed 'e' f.txt`, dir) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "blocked") +} + +// TestSedGTFOBinsFileWriteBlocked verifies that the GTFOBins file-write +// technique for sed (the 'w' command) is blocked. +// +// GTFOBins: https://gtfobins.org/gtfobins/sed/ +// Technique: sed -n "s/.*/$data/w /path/to/output" /dev/null +func TestSedGTFOBinsFileWriteBlocked(t *testing.T) { + dir := pentestDir(t, map[string]string{"f.txt": "test\n"}) + _, stderr, code := cmdRun(t, `sed 'w /tmp/evil' f.txt`, dir) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "blocked") +} + +// TestSedGTFOBinsInPlaceBlocked verifies that the GTFOBins in-place edit +// technique for sed (-i flag) is blocked. +// +// GTFOBins: https://gtfobins.org/gtfobins/sed/ +// Technique: sed -i ” /path/to/file +func TestSedGTFOBinsInPlaceBlocked(t *testing.T) { + dir := pentestDir(t, map[string]string{"f.txt": "hello\n"}) + _, stderr, code := cmdRun(t, `sed -i 's/hello/bye/' f.txt`, dir) + assert.NotEqual(t, 0, code) + assert.Contains(t, stderr, "sed:") + // Verify file was NOT modified. + data, err := os.ReadFile(filepath.Join(dir, "f.txt")) + require.NoError(t, err) + assert.Equal(t, "hello\n", string(data)) +} diff --git a/builtins/uniq/uniq_pentest_test.go b/builtins/uniq/uniq_pentest_test.go index 86bda6db..523acc32 100644 --- a/builtins/uniq/uniq_pentest_test.go +++ b/builtins/uniq/uniq_pentest_test.go @@ -250,3 +250,20 @@ func TestUniqPentestBinaryContent(t *testing.T) { assert.Equal(t, 0, code) assert.Equal(t, "\xfc\x80\x80\n", stdout) } + +// --- GTFOBins validation --- + +// TestUniqGTFOBinsFileReadSandboxEscape verifies that the GTFOBins file-read +// technique for uniq is blocked by the AllowedPaths sandbox. +// +// GTFOBins: https://gtfobins.org/gtfobins/uniq/ +// Technique: uniq /path/to/file +func TestUniqGTFOBinsFileReadSandboxEscape(t *testing.T) { + allowed := t.TempDir() + secret := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(secret, "secret.txt"), []byte("secret data\n"), 0644)) + secretPath := strings.ReplaceAll(filepath.Join(secret, "secret.txt"), `\`, `/`) + _, stderr, code := runScript(t, "uniq "+secretPath, allowed, interp.AllowedPaths([]string{allowed})) + assert.Equal(t, 1, code) + assert.Contains(t, stderr, "uniq:") +}