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:") +}